Compare commits
299 Commits
extension-
...
scheduler-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56429f37c7 | ||
|
|
a5f39ec024 | ||
|
|
c1cea51e2a | ||
|
|
b245db699a | ||
|
|
96996950f4 | ||
|
|
1b29725a60 | ||
|
|
79dfae2464 | ||
|
|
e1063743e8 | ||
|
|
3cc21a01ef | ||
|
|
dcc9988844 | ||
|
|
0a5955a464 | ||
|
|
34122aeb21 | ||
|
|
6401ac0725 | ||
|
|
c20cbba0eb | ||
|
|
f2f3d9faf6 | ||
|
|
b922019221 | ||
|
|
d52defe35a | ||
|
|
79a8985a8e | ||
|
|
03216c9800 | ||
|
|
632bd378ba | ||
|
|
b71ef540fc | ||
|
|
158ebdc580 | ||
|
|
d8ebd8101f | ||
|
|
f4c3a6c236 | ||
|
|
6eb198cabf | ||
|
|
07bf685fee | ||
|
|
a6b7af3cbd | ||
|
|
7889aaf3fb | ||
|
|
3bf57dc779 | ||
|
|
a3ac595737 | ||
|
|
63bfb6131f | ||
|
|
5fe7fd97bd | ||
|
|
a61c14cf3b | ||
|
|
c996934b57 | ||
|
|
5805f62f18 | ||
|
|
bd481dea48 | ||
|
|
59b01651e1 | ||
|
|
3e8d55739c | ||
|
|
8fb2bde2c9 | ||
|
|
886832281d | ||
|
|
b633de66f7 | ||
|
|
2f63543380 | ||
|
|
79d4f7d33d | ||
|
|
693b978c8d | ||
|
|
dd13c95158 | ||
|
|
a78ffdafa9 | ||
|
|
c952de4bfb | ||
|
|
75c71a9fc5 | ||
|
|
213c1b210b | ||
|
|
be57307a6f | ||
|
|
38f4e21fe8 | ||
|
|
6067436e9b | ||
|
|
54c4302cdb | ||
|
|
3db2d03bb3 | ||
|
|
63918b8955 | ||
|
|
82535a5481 | ||
|
|
c2c8b4b9fb | ||
|
|
6cab835003 | ||
|
|
0c47984a19 | ||
|
|
b8e40e6fdb | ||
|
|
d7da5d3efd | ||
|
|
86aa9abc90 | ||
|
|
02230acf19 | ||
|
|
849266d949 | ||
|
|
075a318a89 | ||
|
|
ebb6533612 | ||
|
|
9bc8ffd96b | ||
|
|
9d4f73cc2f | ||
|
|
8f1cc9275e | ||
|
|
b68973ed25 | ||
|
|
5d59ace54e | ||
|
|
f7e89a3442 | ||
|
|
f70960b3ce | ||
|
|
5633366cc5 | ||
|
|
f9d4ac0b4f | ||
|
|
9a2a95e321 | ||
|
|
3d387f4bcf | ||
|
|
99eb28785f | ||
|
|
e007b3227f | ||
|
|
a51585d2da | ||
|
|
26b261a336 | ||
|
|
f80ef9a3c5 | ||
|
|
550da8756f | ||
|
|
13594bd97e | ||
|
|
1acd3a72c4 | ||
|
|
064ec8c52c | ||
|
|
280cf662a1 | ||
|
|
76e8715434 | ||
|
|
be0444e039 | ||
|
|
aa2ecef1cf | ||
|
|
a22bc65573 | ||
|
|
328b5cb969 | ||
|
|
d02575cf2b | ||
|
|
fc620bba43 | ||
|
|
a788c91de9 | ||
|
|
7676004a96 | ||
|
|
e248cb14e5 | ||
|
|
7b08b685a2 | ||
|
|
e9073eceeb | ||
|
|
1fe806a34c | ||
|
|
8d4ac155e2 | ||
|
|
31270d41c8 | ||
|
|
f0e1a76877 | ||
|
|
56493370a4 | ||
|
|
00169e0ae2 | ||
|
|
5e41d48512 | ||
|
|
5c3b5a1d60 | ||
|
|
02925409f0 | ||
|
|
c39581d52e | ||
|
|
52e2650270 | ||
|
|
d5e1509b53 | ||
|
|
5b07e2b242 | ||
|
|
9272b90672 | ||
|
|
8d714f0bc4 | ||
|
|
6cc947f654 | ||
|
|
f2cc24c5fa | ||
|
|
488fa02547 | ||
|
|
dad6481e02 | ||
|
|
0283bfb049 | ||
|
|
56daba28d4 | ||
|
|
6e0ecbcb07 | ||
|
|
4754422ef4 | ||
|
|
e860252185 | ||
|
|
fad06dd00c | ||
|
|
329ec645da | ||
|
|
e1d236eaf0 | ||
|
|
60f4aa333b | ||
|
|
a698f1bf63 | ||
|
|
636d11ebec | ||
|
|
4d0e760b04 | ||
|
|
8bd4d866b9 | ||
|
|
47c30b6da7 | ||
|
|
18d344e118 | ||
|
|
610cc1b138 | ||
|
|
a07ea1a272 | ||
|
|
e03fa114a7 | ||
|
|
17db7b0e99 | ||
|
|
1afe29422b | ||
|
|
a8aa7622b7 | ||
|
|
a66854e435 | ||
|
|
12073e10f8 | ||
|
|
1186b50ca4 | ||
|
|
65130a9ca9 | ||
|
|
23d18fde8c | ||
|
|
332c0d03d1 | ||
|
|
b871130220 | ||
|
|
0a1e5f93a0 | ||
|
|
8d0fff688f | ||
|
|
717d898692 | ||
|
|
1cd7563f04 | ||
|
|
fc6ca38989 | ||
|
|
1029a8fbaf | ||
|
|
07748b7bae | ||
|
|
37f2ac24b8 | ||
|
|
b5a0a3322d | ||
|
|
eb7da26d19 | ||
|
|
9c099e7ed3 | ||
|
|
7669b05268 | ||
|
|
2098b67304 | ||
|
|
5a6198cc39 | ||
|
|
cda78c12ab | ||
|
|
f4378672b8 | ||
|
|
ecb8d3d4dd | ||
|
|
95dbc0efc2 | ||
|
|
8572c19a02 | ||
|
|
045c14593f | ||
|
|
0ff3b68a5e | ||
|
|
a6b9524d78 | ||
|
|
7ed5d42696 | ||
|
|
25d74480aa | ||
|
|
37077a8ebb | ||
|
|
7c4a85f5f1 | ||
|
|
d21628c349 | ||
|
|
9e628505f3 | ||
|
|
3a84ec38ac | ||
|
|
a61bf33fb0 | ||
|
|
d83201256d | ||
|
|
8ee85eab3c | ||
|
|
5b309ef986 | ||
|
|
326ebb5230 | ||
|
|
f5babf96e1 | ||
|
|
f48aa252f8 | ||
|
|
4106c8a188 | ||
|
|
21f7e6a9e6 | ||
|
|
dd431631b4 | ||
|
|
511e51c80e | ||
|
|
0a816cbc87 | ||
|
|
b1333b53ad | ||
|
|
30597a0cba | ||
|
|
a8e2dc2f25 | ||
|
|
fd2094fa19 | ||
|
|
22f1655f8f | ||
|
|
7cbe25fda5 | ||
|
|
728f09f3f4 | ||
|
|
4353b8ecd5 | ||
|
|
736a712387 | ||
|
|
3180f44477 | ||
|
|
5dd8561b06 | ||
|
|
bfab0b71e0 | ||
|
|
04d920016f | ||
|
|
20fa9983ad | ||
|
|
dd57d97bb6 | ||
|
|
d5a437d22f | ||
|
|
a524071dd9 | ||
|
|
1471105643 | ||
|
|
f05ee8a24d | ||
|
|
4d0cada8f4 | ||
|
|
abf90cc274 | ||
|
|
b79d92d1c6 | ||
|
|
660234fed2 | ||
|
|
2b02b60317 | ||
|
|
9d49c1ffda | ||
|
|
6253b1d220 | ||
|
|
4e75f0f3ab | ||
|
|
0b4f72e549 | ||
|
|
dc5f54eaf9 | ||
|
|
ba807a3c46 | ||
|
|
45829b3380 | ||
|
|
631e3dd272 | ||
|
|
8d44bcd4f9 | ||
|
|
1888106664 | ||
|
|
c005adb09c | ||
|
|
6b2d1f153d | ||
|
|
22e1bcccad | ||
|
|
bb591f1e65 | ||
|
|
3d6cc3dc79 | ||
|
|
464d4f72eb | ||
|
|
f4892559f0 | ||
|
|
387059c6b2 | ||
|
|
4a382b2797 | ||
|
|
b948d8b9e7 | ||
|
|
bc17491527 | ||
|
|
f6a6630171 | ||
|
|
18421845eb | ||
|
|
21439426a0 | ||
|
|
044f7b5583 | ||
|
|
12dba5edbe | ||
|
|
216a3a60f5 | ||
|
|
66789607c9 | ||
|
|
62c312b35f | ||
|
|
066dd5c9d5 | ||
|
|
bdba6fd069 | ||
|
|
2260b87ea8 | ||
|
|
e6214b2b71 | ||
|
|
7ef45914e8 | ||
|
|
00e6cbc4fc | ||
|
|
9e0a4c2a9c | ||
|
|
a8b61c5ffa | ||
|
|
d312d59ace | ||
|
|
b4083ec47b | ||
|
|
e75a7aecd6 | ||
|
|
03cf7ddb53 | ||
|
|
9364d39487 | ||
|
|
f16913400a | ||
|
|
5bfc0baa4c | ||
|
|
d7b99a5b12 | ||
|
|
7691cf341c | ||
|
|
63cc90cd2c | ||
|
|
d1e45e27de | ||
|
|
9da0d40694 | ||
|
|
9f344f093e | ||
|
|
ef76f07b1e | ||
|
|
4577e1bf8f | ||
|
|
a574ae8779 | ||
|
|
16666f5357 | ||
|
|
b2e35b5f99 | ||
|
|
9e33243015 | ||
|
|
a0848daab4 | ||
|
|
d72746773f | ||
|
|
0565992d7a | ||
|
|
e1d8c1a6a1 | ||
|
|
f08fd732a7 | ||
|
|
51b7d06a27 | ||
|
|
66c7bdf037 | ||
|
|
363fbbf0d4 | ||
|
|
9860884217 | ||
|
|
4cef8eb47b | ||
|
|
e5f87735d3 | ||
|
|
f4b8b0f471 | ||
|
|
5cd30e5106 | ||
|
|
a350438a21 | ||
|
|
bd6ca841ad | ||
|
|
f9cea5af29 | ||
|
|
3bb6c2546a | ||
|
|
37b0cdf94b | ||
|
|
d76dd86272 | ||
|
|
b558be7ec6 | ||
|
|
07fe8e9bb1 | ||
|
|
b776178b52 | ||
|
|
1d0aef6b22 | ||
|
|
c7ef3025e4 | ||
|
|
822fc7ef16 | ||
|
|
126d708fa1 | ||
|
|
a5ab5c7d5d | ||
|
|
35da6d000a | ||
|
|
d6241b17d3 | ||
|
|
42583c1141 | ||
|
|
76167109db | ||
|
|
cd8679e81a |
16
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
16
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
@@ -75,6 +75,22 @@ body:
|
||||
</details>
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Relevant Keymap
|
||||
description: |
|
||||
Open the command palette in Zed, then type “zed: open keymap file” and copy/paste the file's contents.
|
||||
value: |
|
||||
<details><summary>keymap.json</summary>
|
||||
|
||||
<!-- Paste your keymap file inside the code block. -->
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: (for AI issues) Model provider details
|
||||
|
||||
25
.github/workflows/after_release.yml
vendored
25
.github/workflows/after_release.yml
vendored
@@ -5,13 +5,27 @@ on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
description: tag_name
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: prerelease
|
||||
required: true
|
||||
type: boolean
|
||||
body:
|
||||
description: body
|
||||
type: string
|
||||
default: ''
|
||||
jobs:
|
||||
rebuild_releases_page:
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: after_release::rebuild_releases_page::refresh_cloud_releases
|
||||
run: curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag=${{ github.event.release.tag_name }}
|
||||
run: curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag=${{ github.event.release.tag_name || inputs.tag_name }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: after_release::rebuild_releases_page::redeploy_zed_dev
|
||||
run: npm exec --yes -- vercel@37 --token="$VERCEL_TOKEN" --scope zed-industries redeploy https://zed.dev
|
||||
@@ -27,7 +41,7 @@ jobs:
|
||||
- id: get-release-url
|
||||
name: after_release::post_to_discord::get_release_url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
if [ "${{ github.event.release.prerelease || inputs.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview"
|
||||
else
|
||||
URL="https://zed.dev/releases/stable"
|
||||
@@ -40,9 +54,9 @@ jobs:
|
||||
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
|
||||
📣 Zed [${{ github.event.release.tag_name || inputs.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
|
||||
|
||||
${{ github.event.release.body }}
|
||||
${{ github.event.release.body || inputs.body }}
|
||||
maxLength: 2000
|
||||
truncationSymbol: '...'
|
||||
- name: after_release::post_to_discord::discord_webhook_action
|
||||
@@ -56,7 +70,7 @@ jobs:
|
||||
- id: set-package-name
|
||||
name: after_release::publish_winget::set_package_name
|
||||
run: |
|
||||
if ("${{ github.event.release.prerelease }}" -eq "true") {
|
||||
if ("${{ github.event.release.prerelease || inputs.prerelease }}" -eq "true") {
|
||||
$PACKAGE_NAME = "ZedIndustries.Zed.Preview"
|
||||
} else {
|
||||
$PACKAGE_NAME = "ZedIndustries.Zed"
|
||||
@@ -68,6 +82,7 @@ jobs:
|
||||
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
|
||||
with:
|
||||
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
|
||||
release-tag: ${{ github.event.release.tag_name || inputs.tag_name }}
|
||||
max-versions-to-keep: 5
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
create_sentry_release:
|
||||
|
||||
2
.github/workflows/run_tests.yml
vendored
2
.github/workflows/run_tests.yml
vendored
@@ -497,6 +497,8 @@ jobs:
|
||||
env:
|
||||
GIT_AUTHOR_NAME: Protobuf Action
|
||||
GIT_AUTHOR_EMAIL: ci@zed.dev
|
||||
GIT_COMMITTER_NAME: Protobuf Action
|
||||
GIT_COMMITTER_EMAIL: ci@zed.dev
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -39,3 +39,6 @@ xcuserdata/
|
||||
# Don't commit any secrets to the repo.
|
||||
.env
|
||||
.env.secret.toml
|
||||
|
||||
# `nix build` output
|
||||
/result
|
||||
|
||||
143
.rules
143
.rules
@@ -1,143 +0,0 @@
|
||||
# Rust coding guidelines
|
||||
|
||||
* Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
|
||||
* Do not write organizational or comments that summarize the code. Comments should only be written in order to explain "why" the code is written in some way in the case there is a reason that is tricky / non-obvious.
|
||||
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
|
||||
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
|
||||
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
|
||||
* Never silently discard errors with `let _ =` on fallible operations. Always handle errors appropriately:
|
||||
- Propagate errors with `?` when the calling function should handle them
|
||||
- Use `.log_err()` or similar when you need to ignore errors but want visibility
|
||||
- Use explicit error handling with `match` or `if let Err(...)` when you need custom logic
|
||||
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
|
||||
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
|
||||
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
||||
* When creating new crates, prefer specifying the library root path in `Cargo.toml` using `[lib] path = "...rs"` instead of the default `lib.rs`, to maintain consistent and descriptive naming (e.g., `gpui.rs` or `main.rs`).
|
||||
* Avoid creative additions unless explicitly requested
|
||||
* Use full words for variable names (no abbreviations like "q" for "queue")
|
||||
* Use variable shadowing to scope clones in async contexts for clarity, minimizing the lifetime of borrowed references.
|
||||
Example:
|
||||
```rust
|
||||
executor.spawn({
|
||||
let task_ran = task_ran.clone();
|
||||
async move {
|
||||
*task_ran.borrow_mut() = true;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
# GPUI
|
||||
|
||||
GPUI is a UI framework which also provides primitives for state and concurrency management.
|
||||
|
||||
## Context
|
||||
|
||||
Context types allow interaction with global state, windows, entities, and system services. They are typically passed to functions as the argument named `cx`. When a function takes callbacks they come after the `cx` parameter.
|
||||
|
||||
* `App` is the root context type, providing access to global state and read and update of entities.
|
||||
* `Context<T>` is provided when updating an `Entity<T>`. This context dereferences into `App`, so functions which take `&App` can also take `&Context<T>`.
|
||||
* `AsyncApp` and `AsyncWindowContext` are provided by `cx.spawn` and `cx.spawn_in`. These can be held across await points.
|
||||
|
||||
## `Window`
|
||||
|
||||
`Window` provides access to the state of an application window. It is passed to functions as an argument named `window` and comes before `cx` when present. It is used for managing focus, dispatching actions, directly drawing, getting user input state, etc.
|
||||
|
||||
## Entities
|
||||
|
||||
An `Entity<T>` is a handle to state of type `T`. With `thing: Entity<T>`:
|
||||
|
||||
* `thing.entity_id()` returns `EntityId`
|
||||
* `thing.downgrade()` returns `WeakEntity<T>`
|
||||
* `thing.read(cx: &App)` returns `&T`.
|
||||
* `thing.read_with(cx, |thing: &T, cx: &App| ...)` returns the closure's return value.
|
||||
* `thing.update(cx, |thing: &mut T, cx: &mut Context<T>| ...)` allows the closure to mutate the state, and provides a `Context<T>` for interacting with the entity. It returns the closure's return value.
|
||||
* `thing.update_in(cx, |thing: &mut T, window: &mut Window, cx: &mut Context<T>| ...)` takes a `AsyncWindowContext` or `VisualTestContext`. It's the same as `update` while also providing the `Window`.
|
||||
|
||||
Within the closures, the inner `cx` provided to the closure must be used instead of the outer `cx` to avoid issues with multiple borrows.
|
||||
|
||||
Trying to update an entity while it's already being updated must be avoided as this will cause a panic.
|
||||
|
||||
When `read_with`, `update`, or `update_in` are used with an async context, the closure's return value is wrapped in an `anyhow::Result`.
|
||||
|
||||
`WeakEntity<T>` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to each other they will never be dropped.
|
||||
|
||||
## Concurrency
|
||||
|
||||
All use of entities and UI rendering occurs on a single foreground thread.
|
||||
|
||||
`cx.spawn(async move |cx| ...)` runs an async closure on the foreground thread. Within the closure, `cx` is an async context like `AsyncApp` or `AsyncWindowContext`.
|
||||
|
||||
When the outer cx is a `Context<T>`, the use of `spawn` instead looks like `cx.spawn(async move |handle, cx| ...)`, where `handle: WeakEntity<T>`.
|
||||
|
||||
To do work on other threads, `cx.background_spawn(async move { ... })` is used. Often this background task is awaited on by a foreground task which uses the results to update state.
|
||||
|
||||
Both `cx.spawn` and `cx.background_spawn` return a `Task<R>`, which is a future that can be awaited upon. If this task is dropped, then its work is cancelled. To prevent this one of the following must be done:
|
||||
|
||||
* Awaiting the task in some other async context.
|
||||
* Detaching the task via `task.detach()` or `task.detach_and_log_err(cx)`, allowing it to run indefinitely.
|
||||
* Storing the task in a field, if the work should be halted when the struct is dropped.
|
||||
|
||||
A task which doesn't do anything but provide a value can be created with `Task::ready(value)`.
|
||||
|
||||
## Elements
|
||||
|
||||
The `Render` trait is used to render some state into an element tree that is laid out using flexbox layout. An `Entity<T>` where `T` implements `Render` is sometimes called a "view".
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
struct TextWithBorder(SharedString);
|
||||
|
||||
impl Render for TextWithBorder {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().border_1().child(self.0.clone())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Since `impl IntoElement for SharedString` exists, it can be used as an argument to `child`. `SharedString` is used to avoid copying strings, and is either an `&'static str` or `Arc<str>`.
|
||||
|
||||
UI components that are constructed just to be turned into elements can instead implement the `RenderOnce` trait, which is similar to `Render`, but its `render` method takes ownership of `self`. Types that implement this trait can use `#[derive(IntoElement)]` to use them directly as children.
|
||||
|
||||
The style methods on elements are similar to those used by Tailwind CSS.
|
||||
|
||||
If some attributes or children of an element tree are conditional, `.when(condition, |this| ...)` can be used to run the closure only when `condition` is true. Similarly, `.when_some(option, |this, value| ...)` runs the closure when the `Option` has a value.
|
||||
|
||||
## Input events
|
||||
|
||||
Input event handlers can be registered on an element via methods like `.on_click(|event, window, cx: &mut App| ...)`.
|
||||
|
||||
Often event handlers will want to update the entity that's in the current `Context<T>`. The `cx.listener` method provides this - its use looks like `.on_click(cx.listener(|this: &mut T, event, window, cx: &mut Context<T>| ...)`.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are dispatched via user keyboard interaction or in code via `window.dispatch_action(SomeAction.boxed_clone(), cx)` or `focus_handle.dispatch_action(&SomeAction, window, cx)`.
|
||||
|
||||
Actions with no data defined with the `actions!(some_namespace, [SomeAction, AnotherAction])` macro call. Otherwise the `Action` derive macro is used. Doc comments on actions are displayed to the user.
|
||||
|
||||
Action handlers can be registered on an element via the event handler `.on_action(|action, window, cx| ...)`. Like other event handlers, this is often used with `cx.listener`.
|
||||
|
||||
## Notify
|
||||
|
||||
When a view's state has changed in a way that may affect its rendering, it should call `cx.notify()`. This will cause the view to be rerendered. It will also cause any observe callbacks registered for the entity with `cx.observe` to be called.
|
||||
|
||||
## Entity events
|
||||
|
||||
While updating an entity (`cx: Context<T>`), it can emit an event using `cx.emit(event)`. Entities register which events they can emit by declaring `impl EventEmittor<EventType> for EntityType {}`.
|
||||
|
||||
Other entities can then register a callback to handle these events by doing `cx.subscribe(other_entity, |this, other_entity, event, cx| ...)`. This will return a `Subscription` which deregisters the callback when dropped. Typically `cx.subscribe` happens when creating a new entity and the subscriptions are stored in a `_subscriptions: Vec<Subscription>` field.
|
||||
|
||||
## Recent API changes
|
||||
|
||||
GPUI has had some changes to its APIs. Always write code using the new APIs:
|
||||
|
||||
* `spawn` methods now take async closures (`AsyncFn`), and so should be called like `cx.spawn(async move |cx| ...)`.
|
||||
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which no longer exist and should NEVER be used.
|
||||
* Use `App` references. This replaces `AppContext` which no longer exists and should NEVER be used.
|
||||
* Use `Context<T>` references. This replaces `ModelContext<T>` which no longer exists and should NEVER be used.
|
||||
* `Window` is now passed around explicitly. The new interface adds a `Window` reference parameter to some methods, and adds some new "*_in" methods for plumbing `Window`. The old types `WindowContext` and `ViewContext<T>` should NEVER be used.
|
||||
|
||||
|
||||
## General guidelines
|
||||
|
||||
- Use `./script/clippy` instead of `cargo clippy`
|
||||
1498
Cargo.lock
generated
1498
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
58
Cargo.toml
58
Cargo.toml
@@ -9,6 +9,7 @@ members = [
|
||||
"crates/agent_servers",
|
||||
"crates/agent_settings",
|
||||
"crates/agent_ui",
|
||||
"crates/agent_ui_v2",
|
||||
"crates/ai_onboarding",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
@@ -32,7 +33,6 @@ members = [
|
||||
"crates/cloud_api_client",
|
||||
"crates/cloud_api_types",
|
||||
"crates/cloud_llm_client",
|
||||
"crates/cloud_zeta2_prompt",
|
||||
"crates/collab",
|
||||
"crates/collab_ui",
|
||||
"crates/collections",
|
||||
@@ -54,9 +54,9 @@ members = [
|
||||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/edit_prediction",
|
||||
"crates/edit_prediction_button",
|
||||
"crates/edit_prediction_types",
|
||||
"crates/edit_prediction_ui",
|
||||
"crates/edit_prediction_context",
|
||||
"crates/zeta2_tools",
|
||||
"crates/editor",
|
||||
"crates/eval",
|
||||
"crates/eval_utils",
|
||||
@@ -201,10 +201,12 @@ members = [
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zed_env_vars",
|
||||
"crates/zeta",
|
||||
"crates/zeta_cli",
|
||||
"crates/edit_prediction_cli",
|
||||
"crates/zeta_prompt",
|
||||
"crates/zlog",
|
||||
"crates/zlog_settings",
|
||||
"crates/ztracing",
|
||||
"crates/ztracing_macro",
|
||||
|
||||
#
|
||||
# Extensions
|
||||
@@ -241,9 +243,9 @@ action_log = { path = "crates/action_log" }
|
||||
agent = { path = "crates/agent" }
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
agent_ui = { path = "crates/agent_ui" }
|
||||
agent_ui_v2 = { path = "crates/agent_ui_v2" }
|
||||
agent_settings = { path = "crates/agent_settings" }
|
||||
agent_servers = { path = "crates/agent_servers" }
|
||||
ai = { path = "crates/ai" }
|
||||
ai_onboarding = { path = "crates/ai_onboarding" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
@@ -253,7 +255,6 @@ assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
assistant_slash_commands = { path = "crates/assistant_slash_commands" }
|
||||
audio = { path = "crates/audio" }
|
||||
auto_update = { path = "crates/auto_update" }
|
||||
auto_update_helper = { path = "crates/auto_update_helper" }
|
||||
auto_update_ui = { path = "crates/auto_update_ui" }
|
||||
aws_http_client = { path = "crates/aws_http_client" }
|
||||
bedrock = { path = "crates/bedrock" }
|
||||
@@ -267,8 +268,6 @@ clock = { path = "crates/clock" }
|
||||
cloud_api_client = { path = "crates/cloud_api_client" }
|
||||
cloud_api_types = { path = "crates/cloud_api_types" }
|
||||
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||
cloud_zeta2_prompt = { path = "crates/cloud_zeta2_prompt" }
|
||||
collab = { path = "crates/collab" }
|
||||
collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections", version = "0.1.0" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
@@ -313,10 +312,9 @@ http_client = { path = "crates/http_client" }
|
||||
http_client_tls = { path = "crates/http_client_tls" }
|
||||
icons = { path = "crates/icons" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
edit_prediction = { path = "crates/edit_prediction" }
|
||||
edit_prediction_button = { path = "crates/edit_prediction_button" }
|
||||
edit_prediction_types = { path = "crates/edit_prediction_types" }
|
||||
edit_prediction_ui = { path = "crates/edit_prediction_ui" }
|
||||
edit_prediction_context = { path = "crates/edit_prediction_context" }
|
||||
zeta2_tools = { path = "crates/zeta2_tools" }
|
||||
inspector_ui = { path = "crates/inspector_ui" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
journal = { path = "crates/journal" }
|
||||
@@ -358,8 +356,6 @@ panel = { path = "crates/panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
perf = { path = "tooling/perf" }
|
||||
picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
prettier = { path = "crates/prettier" }
|
||||
settings_profile_selector = { path = "crates/settings_profile_selector" }
|
||||
project = { path = "crates/project" }
|
||||
@@ -370,16 +366,15 @@ proto = { path = "crates/proto" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
scheduler = { path = "crates/scheduler" }
|
||||
remote = { path = "crates/remote" }
|
||||
remote_server = { path = "crates/remote_server" }
|
||||
repl = { path = "crates/repl" }
|
||||
reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rodio = { git = "https://github.com/RustAudio/rodio", rev ="e2074c6c2acf07b57cf717e076bdda7a9ac6e70b", features = ["wav", "playback", "wav_output", "recording"] }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
scheduler = { path = "crates/scheduler" }
|
||||
search = { path = "crates/search" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
@@ -392,7 +387,6 @@ snippets_ui = { path = "crates/snippets_ui" }
|
||||
sqlez = { path = "crates/sqlez" }
|
||||
sqlez_macros = { path = "crates/sqlez_macros" }
|
||||
story = { path = "crates/story" }
|
||||
storybook = { path = "crates/storybook" }
|
||||
streaming_diff = { path = "crates/streaming_diff" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
@@ -409,7 +403,6 @@ 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" }
|
||||
title_bar = { path = "crates/title_bar" }
|
||||
@@ -433,15 +426,18 @@ x_ai = { path = "crates/x_ai" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
zed_env_vars = { path = "crates/zed_env_vars" }
|
||||
zeta = { path = "crates/zeta" }
|
||||
edit_prediction = { path = "crates/edit_prediction" }
|
||||
zeta_prompt = { path = "crates/zeta_prompt" }
|
||||
zlog = { path = "crates/zlog" }
|
||||
zlog_settings = { path = "crates/zlog_settings" }
|
||||
ztracing = { path = "crates/ztracing" }
|
||||
ztracing_macro = { path = "crates/ztracing_macro" }
|
||||
|
||||
#
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "=0.8.0", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
@@ -508,13 +504,11 @@ exec = "0.3.1"
|
||||
fancy-regex = "0.16.0"
|
||||
fork = "0.4.0"
|
||||
futures = "0.3"
|
||||
futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "09acfdf2bd5c1d6254abefd609c808ff73547b2c" }
|
||||
git2 = { version = "0.20.1", default-features = false }
|
||||
globset = "0.4"
|
||||
handlebars = "4.3"
|
||||
hashbrown = "0.15.3"
|
||||
heck = "0.5"
|
||||
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
@@ -550,7 +544,6 @@ nanoid = "0.4"
|
||||
nbformat = "0.15.0"
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
num-traits = "0.2"
|
||||
objc = "0.2"
|
||||
objc2-foundation = { version = "=0.3.1", default-features = false, features = [
|
||||
"NSArray",
|
||||
@@ -589,7 +582,6 @@ pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev =
|
||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
|
||||
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
|
||||
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
|
||||
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
|
||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
|
||||
pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" }
|
||||
@@ -629,7 +621,6 @@ scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = { version = "1.0", features = ["serde"] }
|
||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||
serde_derive = "1.0.221"
|
||||
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.2", features = [
|
||||
"preserve_order",
|
||||
@@ -641,10 +632,9 @@ serde_urlencoded = "0.7"
|
||||
sha2 = "0.10"
|
||||
shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
similar = "2.6"
|
||||
simplelog = "0.12.2"
|
||||
slotmap = "1.0.6"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smallvec = { version = "1.6", features = ["union", "const_new"] }
|
||||
smol = "2.0"
|
||||
sqlformat = "0.2"
|
||||
stacksafe = "0.1"
|
||||
@@ -670,6 +660,7 @@ time = { version = "0.3", features = [
|
||||
tiny_http = "0.8"
|
||||
tokio = { version = "1" }
|
||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
|
||||
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io", "tokio"] }
|
||||
toml = "0.8"
|
||||
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
|
||||
tower-http = "0.4.4"
|
||||
@@ -696,6 +687,7 @@ tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.24"
|
||||
tree-sitter-typescript = { git = "https://github.com/zed-industries/tree-sitter-typescript", rev = "e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" } # https://github.com/tree-sitter/tree-sitter-typescript/pull/347
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
|
||||
tracing = "0.1.40"
|
||||
unicase = "2.6"
|
||||
unicode-script = "0.5.7"
|
||||
unicode-segmentation = "1.10"
|
||||
@@ -719,7 +711,6 @@ wasmtime-wasi = "29"
|
||||
wax = "0.6"
|
||||
which = "6.0.0"
|
||||
windows-core = "0.61"
|
||||
wit-component = "0.221"
|
||||
yawc = "0.2.5"
|
||||
zeroize = "1.8"
|
||||
zstd = "0.11"
|
||||
@@ -801,20 +792,13 @@ settings_macros = { opt-level = 3 }
|
||||
sqlez_macros = { opt-level = 3, codegen-units = 1 }
|
||||
ui_macros = { opt-level = 3 }
|
||||
util_macros = { opt-level = 3 }
|
||||
serde_derive = { opt-level = 3 }
|
||||
quote = { opt-level = 3 }
|
||||
syn = { opt-level = 3 }
|
||||
proc-macro2 = { opt-level = 3 }
|
||||
# proc-macros end
|
||||
|
||||
taffy = { opt-level = 3 }
|
||||
cranelift-codegen = { opt-level = 3 }
|
||||
cranelift-codegen-meta = { opt-level = 3 }
|
||||
cranelift-codegen-shared = { opt-level = 3 }
|
||||
resvg = { opt-level = 3 }
|
||||
rustybuzz = { opt-level = 3 }
|
||||
ttf-parser = { opt-level = 3 }
|
||||
wasmtime-cranelift = { opt-level = 3 }
|
||||
wasmtime = { opt-level = 3 }
|
||||
# Build single-source-file crates with cg=1 as it helps make `cargo build` of a whole workspace a bit faster
|
||||
activity_indicator = { codegen-units = 1 }
|
||||
@@ -823,12 +807,11 @@ breadcrumbs = { codegen-units = 1 }
|
||||
collections = { codegen-units = 1 }
|
||||
command_palette = { codegen-units = 1 }
|
||||
command_palette_hooks = { codegen-units = 1 }
|
||||
extension_cli = { codegen-units = 1 }
|
||||
feature_flags = { codegen-units = 1 }
|
||||
file_icons = { codegen-units = 1 }
|
||||
fsevent = { codegen-units = 1 }
|
||||
image_viewer = { codegen-units = 1 }
|
||||
edit_prediction_button = { codegen-units = 1 }
|
||||
edit_prediction_ui = { codegen-units = 1 }
|
||||
install_cli = { codegen-units = 1 }
|
||||
journal = { codegen-units = 1 }
|
||||
json_schema_store = { codegen-units = 1 }
|
||||
@@ -843,7 +826,6 @@ project_symbols = { codegen-units = 1 }
|
||||
refineable = { codegen-units = 1 }
|
||||
release_channel = { codegen-units = 1 }
|
||||
reqwest_client = { codegen-units = 1 }
|
||||
rich_text = { codegen-units = 1 }
|
||||
session = { codegen-units = 1 }
|
||||
snippet = { codegen-units = 1 }
|
||||
snippets_ui = { codegen-units = 1 }
|
||||
|
||||
@@ -34,8 +34,4 @@ RUN apt-get update; \
|
||||
linux-perf binutils
|
||||
WORKDIR app
|
||||
COPY --from=builder /app/collab /app/collab
|
||||
COPY --from=builder /app/crates/collab/migrations /app/migrations
|
||||
COPY --from=builder /app/crates/collab/migrations_llm /app/migrations_llm
|
||||
ENV MIGRATIONS_PATH=/app/migrations
|
||||
ENV LLM_DATABASE_MIGRATIONS_PATH=/app/migrations_llm
|
||||
ENTRYPOINT ["/app/collab"]
|
||||
|
||||
144
STATUS.md
Normal file
144
STATUS.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Scheduler Integration - Debugging Status
|
||||
|
||||
## Problem
|
||||
|
||||
PR #44810 causes Zed to hang on startup on Linux/Windows, but works fine on Mac.
|
||||
|
||||
From PR comment by @yara-blue and @localcc:
|
||||
> "With it applied zed hangs without ever responding when you open it"
|
||||
|
||||
## What Was Cleaned Up
|
||||
|
||||
Removed unrelated changes that were accidentally committed:
|
||||
- `ConfiguredApiCard`, `InstructionListItem`, `api_key.rs` (UI components)
|
||||
- Debug instrumentation in `terminal_tool.rs`, `capability_granter.rs`, `wasm_host/wit/since_v0_8_0.rs`, `lsp_store.rs`
|
||||
- Planning docs (`.rules`, `PLAN.md`, old `STATUS.md`)
|
||||
|
||||
Kept `language_registry.rs` changes as they may be relevant to debugging.
|
||||
|
||||
## Analysis So Far
|
||||
|
||||
### Code paths verified as correct:
|
||||
|
||||
1. **Priority queue algorithm** - The weighted random selection in `crates/gpui/src/queue.rs` is mathematically sound. When the last non-empty queue is checked, the probability is always 100%.
|
||||
|
||||
2. **`async_task::Task` implements `Unpin`** - So `Pin::new(task).poll(cx)` is valid.
|
||||
|
||||
3. **`parking::Parker` semantics** - If `unpark()` is called before `park()`, the `park()` returns immediately. This is correct.
|
||||
|
||||
4. **Waker creation** - `waker_fn` with `Unparker` (which is `Clone + Send + Sync`) should work correctly.
|
||||
|
||||
5. **`PlatformScheduler::block` implementation** - Identical logic to the old `block_internal` for production builds.
|
||||
|
||||
### The blocking flow:
|
||||
|
||||
1. Task spawned → runnable scheduled → sent to priority queue
|
||||
2. Background thread waiting on condvar in `PriorityQueueReceiver::recv()`
|
||||
3. `send()` pushes to queue and calls `condvar.notify_one()`
|
||||
4. Background thread wakes, pops item, runs runnable
|
||||
5. When task completes, `async_task` wakes the registered waker
|
||||
6. Waker calls `unparker.unpark()`
|
||||
7. `parker.park()` returns
|
||||
8. Future is polled again, returns `Ready`
|
||||
|
||||
### Files involved:
|
||||
|
||||
- `crates/gpui/src/platform_scheduler.rs` - `PlatformScheduler::block()` implementation
|
||||
- `crates/gpui/src/executor.rs` - `BackgroundExecutor::block()` wraps futures
|
||||
- `crates/gpui/src/queue.rs` - Priority queue with `parking_lot::Condvar`
|
||||
- `crates/gpui/src/platform/linux/dispatcher.rs` - Background thread pool
|
||||
- `crates/scheduler/src/executor.rs` - `scheduler::BackgroundExecutor::spawn_with_priority`
|
||||
|
||||
## What to investigate next
|
||||
|
||||
### 1. Verify background threads are actually running
|
||||
|
||||
Add logging at the start of background worker threads in `LinuxDispatcher::new()`:
|
||||
```rust
|
||||
.spawn(move || {
|
||||
log::info!("[LinuxDispatcher] background worker {} started", i);
|
||||
for runnable in receiver.iter() {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Verify tasks are being dispatched
|
||||
|
||||
Add logging in `PlatformScheduler::schedule_background_with_priority`:
|
||||
```rust
|
||||
fn schedule_background_with_priority(&self, runnable: Runnable<RunnableMeta>, priority: Priority) {
|
||||
log::info!("[PlatformScheduler] dispatching task priority={:?}", priority);
|
||||
self.dispatcher.dispatch(runnable, priority);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Verify the priority queue send/receive
|
||||
|
||||
In `crates/gpui/src/queue.rs`, add logging to `send()` and `recv()`:
|
||||
```rust
|
||||
fn send(&self, priority: Priority, item: T) -> Result<(), SendError<T>> {
|
||||
// ...
|
||||
self.condvar.notify_one();
|
||||
log::debug!("[PriorityQueue] sent item, notified condvar");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv(&self) -> Result<...> {
|
||||
log::debug!("[PriorityQueue] recv() waiting...");
|
||||
while queues.is_empty() {
|
||||
self.condvar.wait(&mut queues);
|
||||
}
|
||||
log::debug!("[PriorityQueue] recv() got item");
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Check timing of dispatcher creation vs task spawning
|
||||
|
||||
Trace when `LinuxDispatcher::new()` is called vs when the first `spawn()` happens. If tasks are spawned before background threads are ready, they might be lost.
|
||||
|
||||
### 5. Check for platform-specific differences in `parking` or `parking_lot`
|
||||
|
||||
The `parking` crate (used for `Parker`/`Unparker`) and `parking_lot` (used for `Condvar` in the priority queue) may have platform-specific behavior. Check their GitHub issues for Linux-specific bugs.
|
||||
|
||||
### 6. Verify the startup sequence
|
||||
|
||||
The hang happens during startup. Key calls in `crates/zed/src/main.rs`:
|
||||
```rust
|
||||
// Line ~292: Tasks spawned BEFORE app.run()
|
||||
let system_id = app.background_executor().spawn(system_id());
|
||||
let installation_id = app.background_executor().spawn(installation_id());
|
||||
let session = app.background_executor().spawn(Session::new(session_id.clone()));
|
||||
|
||||
// Line ~513-515: Inside app.run() callback, these BLOCK waiting for the tasks
|
||||
let system_id = cx.background_executor().block(system_id).ok();
|
||||
let installation_id = cx.background_executor().block(installation_id).ok();
|
||||
let session = cx.background_executor().block(session);
|
||||
```
|
||||
|
||||
If background threads aren't running yet when `block()` is called, or if the tasks never got dispatched, it will hang forever.
|
||||
|
||||
## Hypotheses to test
|
||||
|
||||
1. **Background threads not started yet** - Race condition where tasks are dispatched before threads are listening on the queue.
|
||||
|
||||
2. **Condvar notification lost** - `notify_one()` called but no thread was waiting yet, and subsequent waits miss it.
|
||||
|
||||
3. **Platform-specific parking behavior** - `parking::Parker` or `parking_lot::Condvar` behaves differently on Linux.
|
||||
|
||||
4. **Priority queue never releases items** - Something in the weighted random selection is wrong on Linux (different RNG behavior?).
|
||||
|
||||
## Running tests
|
||||
|
||||
To get logs, set `RUST_LOG=info` or `RUST_LOG=debug` when running Zed.
|
||||
|
||||
For the extension_host test hang (separate issue):
|
||||
```bash
|
||||
cargo test -p extension_host extension_store_test::test_extension_store_with_test_extension -- --nocapture
|
||||
```
|
||||
|
||||
## Key commits
|
||||
|
||||
- `5b07e2b242` - "WIP: scheduler integration debugging" - This accidentally added unrelated UI components
|
||||
- `d8ebd8101f` - "WIP: scheduler integration debugging + agent terminal diagnostics" - Added debug instrumentation (now removed)
|
||||
5
assets/icons/box.svg
Normal file
5
assets/icons/box.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3996 5.59852C13.3994 5.3881 13.3439 5.18144 13.2386 4.99926C13.1333 4.81709 12.9819 4.66581 12.7997 4.56059L8.59996 2.16076C8.41755 2.05544 8.21063 2 8 2C7.78937 2 7.58246 2.05544 7.40004 2.16076L3.20033 4.56059C3.0181 4.66581 2.86674 4.81709 2.76144 4.99926C2.65613 5.18144 2.60059 5.3881 2.60037 5.59852V10.3982C2.60059 10.6086 2.65613 10.8153 2.76144 10.9975C2.86674 11.1796 3.0181 11.3309 3.20033 11.4361L7.40004 13.836C7.58246 13.9413 7.78937 13.9967 8 13.9967C8.21063 13.9967 8.41755 13.9413 8.59996 13.836L12.7997 11.4361C12.9819 11.3309 13.1333 11.1796 13.2386 10.9975C13.3439 10.8153 13.3994 10.6086 13.3996 10.3982V5.59852Z" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.78033 4.99857L7.99998 7.99836L13.2196 4.99857" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 13.9979V7.99829" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
8
assets/icons/git_branch_plus.svg
Normal file
8
assets/icons/git_branch_plus.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 2V10" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 6C12.5304 6 13.0391 5.78929 13.4142 5.41421C13.7893 5.03914 14 4.53043 14 4C14 3.46957 13.7893 2.96086 13.4142 2.58579C13.0391 2.21071 12.5304 2 12 2C11.4696 2 10.9609 2.21071 10.5858 2.58579C10.2107 2.96086 10 3.46957 10 4C10 4.53043 10.2107 5.03914 10.5858 5.41421C10.9609 5.78929 11.4696 6 12 6Z" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 14C4.53043 14 5.03914 13.7893 5.41421 13.4142C5.78929 13.0391 6 12.5304 6 12C6 11.4696 5.78929 10.9609 5.41421 10.5858C5.03914 10.2107 4.53043 10 4 10C3.46957 10 2.96086 10.2107 2.58579 10.5858C2.21071 10.9609 2 11.4696 2 12C2 12.5304 2.21071 13.0391 2.58579 13.4142C2.96086 13.7893 3.46957 14 4 14Z" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 4C8.4087 4 6.88258 4.63214 5.75736 5.75736C4.63214 6.88258 4 8.4087 4 10" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 10V14" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 12H10" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
11
assets/icons/inception.svg
Normal file
11
assets/icons/inception.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" id="svg1378540956_510">
|
||||
<g clip-path="url(#svg1378540956_510_clip0_1_1506)" transform="translate(4, 4) scale(0.857)">
|
||||
<path d="M17.0547 0.372066H8.52652L-0.00165176 8.90024V17.4284H8.52652V8.90024H17.0547V0.372066Z" fill="#1A1C20"></path>
|
||||
<path d="M10.1992 27.6279H18.7274L27.2556 19.0998V10.5716H18.7274V19.0998H10.1992V27.6279Z" fill="#1A1C20"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="svg1378540956_510_clip0_1_1506">
|
||||
<rect width="27.2559" height="27.2559" fill="white" transform="translate(0 0.37207)"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 593 B |
5
assets/icons/zed_agent_two.svg
Normal file
5
assets/icons/zed_agent_two.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.2224 1.32129L5.2036 4.41875C5.15145 4.57727 5.06282 4.72134 4.94481 4.83934C4.82681 4.95735 4.68274 5.04598 4.52422 5.09813L1.42676 6.11693L4.52422 7.13574C4.68274 7.18788 4.82681 7.27652 4.94481 7.39453C5.06282 7.51253 5.15145 7.6566 5.2036 7.81512L6.2224 10.9126L7.24121 7.81512C7.29335 7.6566 7.38199 7.51253 7.5 7.39453C7.618 7.27652 7.76207 7.18788 7.9206 7.13574L11.018 6.11693L7.9206 5.09813C7.76207 5.04598 7.618 4.95735 7.5 4.83934C7.38199 4.72134 7.29335 4.57727 7.24121 4.41875L6.2224 1.32129Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.76681 13.9373C9.76681 13.6048 9.95997 13.3083 10.5126 12.7917L11.8872 11.4978C12.3545 11.0575 12.5612 10.77 12.5612 10.4735C12.5612 10.1411 12.3185 9.91643 11.9681 9.91643C11.6986 9.91643 11.5054 10.0242 11.2673 10.3208C10.9933 10.6622 10.7956 10.779 10.4946 10.779C10.0633 10.779 9.75781 10.4915 9.75781 10.0916C9.75781 9.21559 10.8136 8.44287 12.067 8.44287C13.3743 8.44287 14.3492 9.22907 14.3492 10.2848C14.3492 10.9452 13.9988 11.5742 13.2845 12.2077L12.2242 13.1511V13.223H13.7292C14.2503 13.223 14.5738 13.5015 14.5738 13.9552C14.5738 14.4089 14.2593 14.6785 13.7292 14.6785H10.5979C10.1037 14.6785 9.76681 14.3775 9.76681 13.9373Z" fill="black"/>
|
||||
<path d="M12.8994 1.32129V4.00482M11.5576 2.66302H14.2412" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,12 @@
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
// "shift shift": "file_finder::Toggle"
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"bindings": {
|
||||
// "j k": "vim::NormalBefore"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
"bindings": {
|
||||
"ctrl-shift-f5": "workspace::Reload", // window:reload
|
||||
"ctrl-k ctrl-n": "workspace::ActivatePreviousPane", // window:focus-next-pane
|
||||
"ctrl-k ctrl-p": "workspace::ActivateNextPane" // window:focus-previous-pane
|
||||
}
|
||||
"ctrl-k ctrl-p": "workspace::ActivateNextPane", // window:focus-previous-pane
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase" // editor:lower-case
|
||||
}
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase", // editor:lower-case
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
@@ -32,8 +32,8 @@
|
||||
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
|
||||
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide", // markdown-preview:toggle
|
||||
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
|
||||
}
|
||||
"ctrl-r": "outline::Toggle", // symbols-view:toggle-project-symbols
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
@@ -41,8 +41,8 @@
|
||||
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
|
||||
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
|
||||
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
|
||||
"ctrl-shift-f3": "search::SelectPreviousMatch" // find-and-replace:find-previous-selected
|
||||
}
|
||||
"ctrl-shift-f3": "search::SelectPreviousMatch", // find-and-replace:find-previous-selected
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
@@ -50,8 +50,8 @@
|
||||
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-k ctrl-b": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-t": "file_finder::Toggle", // fuzzy-finder:toggle-file-finder
|
||||
"ctrl-r": "project_symbols::Toggle" // symbols-view:toggle-project-symbols
|
||||
}
|
||||
"ctrl-r": "project_symbols::Toggle", // symbols-view:toggle-project-symbols
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
@@ -65,8 +65,8 @@
|
||||
"ctrl-6": ["pane::ActivateItem", 5], // tree-view:open-selected-entry-in-pane-6
|
||||
"ctrl-7": ["pane::ActivateItem", 6], // tree-view:open-selected-entry-in-pane-7
|
||||
"ctrl-8": ["pane::ActivateItem", 7], // tree-view:open-selected-entry-in-pane-8
|
||||
"ctrl-9": ["pane::ActivateItem", 8] // tree-view:open-selected-entry-in-pane-9
|
||||
}
|
||||
"ctrl-9": ["pane::ActivateItem", 8], // tree-view:open-selected-entry-in-pane-9
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
@@ -75,8 +75,8 @@
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"ctrl-x": "project_panel::Cut", // tree-view:cut
|
||||
"ctrl-c": "project_panel::Copy", // tree-view:copy
|
||||
"ctrl-v": "project_panel::Paste" // tree-view:paste
|
||||
}
|
||||
"ctrl-v": "project_panel::Paste", // tree-view:paste
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
@@ -90,7 +90,7 @@
|
||||
"d": "project_panel::Duplicate", // tree-view:duplicate
|
||||
"home": "menu::SelectFirst", // core:move-to-top
|
||||
"end": "menu::SelectLast", // core:move-to-bottom
|
||||
"shift-a": "project_panel::NewDirectory" // tree-view:add-folder
|
||||
}
|
||||
}
|
||||
"shift-a": "project_panel::NewDirectory", // tree-view:add-folder
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"ctrl-shift-i": "agent::ToggleFocus",
|
||||
"ctrl-l": "agent::ToggleFocus",
|
||||
"ctrl-shift-l": "agent::ToggleFocus",
|
||||
"ctrl-shift-j": "agent::OpenSettings"
|
||||
}
|
||||
"ctrl-shift-j": "agent::OpenSettings",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
@@ -20,18 +20,18 @@
|
||||
"ctrl-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
|
||||
"ctrl-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
|
||||
"ctrl-k": "assistant::InlineAssist",
|
||||
"ctrl-shift-k": "assistant::InsertIntoEditor"
|
||||
}
|
||||
"ctrl-shift-k": "assistant::InsertIntoEditor",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "editor::Cancel"
|
||||
"ctrl-shift-backspace": "editor::Cancel",
|
||||
// "alt-enter": // Quick Question
|
||||
// "ctrl-shift-enter": // Full File Context
|
||||
// "ctrl-shift-k": // Toggle input focus (editor <> inline assist)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
|
||||
@@ -47,7 +47,7 @@
|
||||
"ctrl-shift-backspace": "editor::Cancel",
|
||||
"ctrl-r": "agent::NewThread",
|
||||
"ctrl-shift-v": "editor::Paste",
|
||||
"ctrl-shift-k": "assistant::InsertIntoEditor"
|
||||
"ctrl-shift-k": "assistant::InsertIntoEditor",
|
||||
// "escape": "agent::ToggleFocus"
|
||||
///// Enable when Zed supports multiple thread tabs
|
||||
// "ctrl-t": // new thread tab
|
||||
@@ -56,28 +56,28 @@
|
||||
///// Enable if Zed adds support for keyboard navigation of thread elements
|
||||
// "tab": // cycle to next message
|
||||
// "shift-tab": // cycle to previous message
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && editor_agent_diff",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::KeepAll",
|
||||
"ctrl-backspace": "agent::RejectAll"
|
||||
}
|
||||
"ctrl-backspace": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && edit_prediction",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-right": "editor::AcceptPartialEditPrediction"
|
||||
}
|
||||
"ctrl-right": "editor::AcceptPartialEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-k": "assistant::InlineAssist"
|
||||
}
|
||||
}
|
||||
"ctrl-k": "assistant::InlineAssist",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-g": "menu::Cancel"
|
||||
}
|
||||
"ctrl-g": "menu::Cancel",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Workaround to avoid falling back to default bindings.
|
||||
@@ -18,8 +18,8 @@
|
||||
"ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel
|
||||
"ctrl-x": null, // currently activates `editor::Cut` if no following key is pressed for 1 second
|
||||
"ctrl-p": null, // currently activates `file_finder::Toggle` when the cursor is on the first character of the buffer
|
||||
"ctrl-n": null // currently activates `workspace::NewFile` when the cursor is on the last character of the buffer
|
||||
}
|
||||
"ctrl-n": null, // currently activates `workspace::NewFile` when the cursor is on the last character of the buffer
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -82,8 +82,8 @@
|
||||
"ctrl-s": "buffer_search::Deploy", // isearch-forward
|
||||
"ctrl-r": "buffer_search::Deploy", // isearch-backward
|
||||
"alt-^": "editor::JoinLines", // join-line
|
||||
"alt-q": "editor::Rewrap" // fill-paragraph
|
||||
}
|
||||
"alt-q": "editor::Rewrap", // fill-paragraph
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && selection_mode", // region selection
|
||||
@@ -119,22 +119,22 @@
|
||||
"alt->": "editor::SelectToEnd",
|
||||
"ctrl-home": "editor::SelectToBeginning",
|
||||
"ctrl-end": "editor::SelectToEnd",
|
||||
"ctrl-g": "editor::Cancel"
|
||||
}
|
||||
"ctrl-g": "editor::Cancel",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && (showing_code_actions || showing_completions)",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::ContextMenuPrevious",
|
||||
"ctrl-n": "editor::ContextMenuNext"
|
||||
}
|
||||
"ctrl-n": "editor::ContextMenuNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_signature_help && !showing_completions",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::SignatureHelpPrevious",
|
||||
"ctrl-n": "editor::SignatureHelpNext"
|
||||
}
|
||||
"ctrl-n": "editor::SignatureHelpNext",
|
||||
},
|
||||
},
|
||||
// Example setting for using emacs-style tab
|
||||
// (i.e. indent the current line / selection or perform symbol completion depending on context)
|
||||
@@ -164,8 +164,8 @@
|
||||
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
|
||||
"ctrl-x ctrl-s": "workspace::Save", // save-buffer
|
||||
"ctrl-x ctrl-w": "workspace::SaveAs", // write-file
|
||||
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
|
||||
}
|
||||
"ctrl-x s": "workspace::SaveAll", // save-some-buffers
|
||||
},
|
||||
},
|
||||
{
|
||||
// Workaround to enable using native emacs from the Zed terminal.
|
||||
@@ -185,22 +185,22 @@
|
||||
"ctrl-x ctrl-f": null, // find-file
|
||||
"ctrl-x ctrl-s": null, // save-buffer
|
||||
"ctrl-x ctrl-w": null, // write-file
|
||||
"ctrl-x s": null // save-some-buffers
|
||||
}
|
||||
"ctrl-x s": null, // save-some-buffers
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-r": "search::SelectPreviousMatch",
|
||||
"ctrl-g": "buffer_search::Dismiss"
|
||||
}
|
||||
"ctrl-g": "buffer_search::Dismiss",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-alt-left": "pane::GoBack",
|
||||
"ctrl-alt-right": "pane::GoForward"
|
||||
}
|
||||
}
|
||||
"ctrl-alt-right": "pane::GoForward",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"shift-f8": "debugger::StepOut",
|
||||
"f9": "debugger::Continue",
|
||||
"shift-f9": "debugger::Start",
|
||||
"alt-shift-f9": "debugger::Start"
|
||||
}
|
||||
"alt-shift-f9": "debugger::Start",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -62,8 +62,8 @@
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-f8": "editor::ToggleBreakpoint",
|
||||
"ctrl-shift-f8": "editor::EditLogBreakpoint",
|
||||
"ctrl-shift-u": "editor::ToggleCase"
|
||||
}
|
||||
"ctrl-shift-u": "editor::ToggleCase",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
@@ -76,14 +76,14 @@
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"ctrl-q": "editor::Hover",
|
||||
"ctrl-p": "editor::ShowSignatureHelp",
|
||||
"ctrl-\\": "assistant::InlineAssist"
|
||||
}
|
||||
"ctrl-\\": "assistant::InlineAssist",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"shift-enter": "search::SelectPreviousMatch"
|
||||
}
|
||||
"shift-enter": "search::SelectPreviousMatch",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar || ProjectSearchBar",
|
||||
@@ -91,8 +91,8 @@
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-e": "search::ToggleSelection",
|
||||
"alt-x": "search::ToggleRegex",
|
||||
"alt-w": "search::ToggleWholeWord"
|
||||
}
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
@@ -114,8 +114,8 @@
|
||||
"alt-1": "project_panel::ToggleFocus",
|
||||
"alt-5": "debug_panel::ToggleFocus",
|
||||
"alt-6": "diagnostics::Deploy",
|
||||
"alt-7": "outline_panel::ToggleFocus"
|
||||
}
|
||||
"alt-7": "outline_panel::ToggleFocus",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane", // this is to override the default Pane mappings to switch tabs
|
||||
@@ -129,15 +129,15 @@
|
||||
"alt-7": "outline_panel::ToggleFocus",
|
||||
"alt-8": null, // Services (bottom dock)
|
||||
"alt-9": null, // Git History (bottom dock)
|
||||
"alt-0": "git_panel::ToggleFocus"
|
||||
}
|
||||
"alt-0": "git_panel::ToggleFocus",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
"alt-f12": "terminal_panel::Toggle",
|
||||
"ctrl-shift-k": "git::Push"
|
||||
}
|
||||
"ctrl-shift-k": "git::Push",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
@@ -145,8 +145,8 @@
|
||||
"ctrl-alt-left": "pane::GoBack",
|
||||
"ctrl-alt-right": "pane::GoForward",
|
||||
"alt-left": "pane::ActivatePreviousItem",
|
||||
"alt-right": "pane::ActivateNextItem"
|
||||
}
|
||||
"alt-right": "pane::ActivateNextItem",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
@@ -156,8 +156,8 @@
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"shift-f6": "project_panel::Rename"
|
||||
}
|
||||
"shift-f6": "project_panel::Rename",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
@@ -167,8 +167,8 @@
|
||||
"ctrl-up": "terminal::ScrollLineUp",
|
||||
"ctrl-down": "terminal::ScrollLineDown",
|
||||
"shift-pageup": "terminal::ScrollPageUp",
|
||||
"shift-pagedown": "terminal::ScrollPageDown"
|
||||
}
|
||||
"shift-pagedown": "terminal::ScrollPageDown",
|
||||
},
|
||||
},
|
||||
{ "context": "GitPanel", "bindings": { "alt-0": "workspace::CloseActiveDock" } },
|
||||
{ "context": "ProjectPanel", "bindings": { "alt-1": "workspace::CloseActiveDock" } },
|
||||
@@ -179,7 +179,7 @@
|
||||
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||
"bindings": {
|
||||
"escape": "editor::ToggleFocus",
|
||||
"shift-escape": "workspace::CloseActiveDock"
|
||||
}
|
||||
}
|
||||
"shift-escape": "workspace::CloseActiveDock",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
"ctrl-^": ["workspace::MoveItemToPane", { "destination": 5 }],
|
||||
"ctrl-&": ["workspace::MoveItemToPane", { "destination": 6 }],
|
||||
"ctrl-*": ["workspace::MoveItemToPane", { "destination": 7 }],
|
||||
"ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }]
|
||||
}
|
||||
"ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -55,20 +55,20 @@
|
||||
"alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
"alt-shift-left": "editor::SelectToPreviousSubwordStart"
|
||||
}
|
||||
"alt-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-r": "outline::Toggle"
|
||||
}
|
||||
"ctrl-r": "outline::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-z": "git::Restore"
|
||||
}
|
||||
"ctrl-k ctrl-z": "git::Restore",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
@@ -83,15 +83,15 @@
|
||||
"alt-6": ["pane::ActivateItem", 5],
|
||||
"alt-7": ["pane::ActivateItem", 6],
|
||||
"alt-8": ["pane::ActivateItem", 7],
|
||||
"alt-9": "pane::ActivateLastItem"
|
||||
}
|
||||
"alt-9": "pane::ActivateLastItem",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-b": "workspace::ToggleLeftDock",
|
||||
// "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom
|
||||
"shift-ctrl-r": "project_symbols::Toggle"
|
||||
}
|
||||
}
|
||||
"shift-ctrl-r": "project_symbols::Toggle",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
"bindings": {
|
||||
"ctrl-alt-cmd-l": "workspace::Reload",
|
||||
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
|
||||
"cmd-k cmd-n": "workspace::ActivateNextPane"
|
||||
}
|
||||
"cmd-k cmd-n": "workspace::ActivateNextPane",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase"
|
||||
}
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
@@ -33,8 +33,8 @@
|
||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||
"cmd-\\": "workspace::ToggleLeftDock",
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-r": "outline::Toggle"
|
||||
}
|
||||
"cmd-r": "outline::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
@@ -42,8 +42,8 @@
|
||||
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"cmd-f3": "search::SelectNextMatch",
|
||||
"cmd-shift-f3": "search::SelectPreviousMatch"
|
||||
}
|
||||
"cmd-shift-f3": "search::SelectPreviousMatch",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
@@ -51,8 +51,8 @@
|
||||
"cmd-\\": "workspace::ToggleLeftDock",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftDock",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-r": "project_symbols::Toggle"
|
||||
}
|
||||
"cmd-shift-r": "project_symbols::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
@@ -67,8 +67,8 @@
|
||||
"cmd-6": ["pane::ActivateItem", 5],
|
||||
"cmd-7": ["pane::ActivateItem", 6],
|
||||
"cmd-8": ["pane::ActivateItem", 7],
|
||||
"cmd-9": "pane::ActivateLastItem"
|
||||
}
|
||||
"cmd-9": "pane::ActivateLastItem",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
@@ -77,8 +77,8 @@
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"cmd-x": "project_panel::Cut",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
"cmd-v": "project_panel::Paste"
|
||||
}
|
||||
"cmd-v": "project_panel::Paste",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
@@ -92,7 +92,7 @@
|
||||
"d": "project_panel::Duplicate",
|
||||
"home": "menu::SelectFirst",
|
||||
"end": "menu::SelectLast",
|
||||
"shift-a": "project_panel::NewDirectory"
|
||||
}
|
||||
}
|
||||
"shift-a": "project_panel::NewDirectory",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"cmd-shift-i": "agent::ToggleFocus",
|
||||
"cmd-l": "agent::ToggleFocus",
|
||||
"cmd-shift-l": "agent::ToggleFocus",
|
||||
"cmd-shift-j": "agent::OpenSettings"
|
||||
}
|
||||
"cmd-shift-j": "agent::OpenSettings",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
@@ -20,19 +20,19 @@
|
||||
"cmd-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode
|
||||
"cmd-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode
|
||||
"cmd-k": "assistant::InlineAssist",
|
||||
"cmd-shift-k": "assistant::InsertIntoEditor"
|
||||
}
|
||||
"cmd-shift-k": "assistant::InsertIntoEditor",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-backspace": "editor::Cancel",
|
||||
"cmd-enter": "menu::Confirm"
|
||||
"cmd-enter": "menu::Confirm",
|
||||
// "alt-enter": // Quick Question
|
||||
// "cmd-shift-enter": // Full File Context
|
||||
// "cmd-shift-k": // Toggle input focus (editor <> inline assist)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel || ContextEditor || (MessageEditor > Editor)",
|
||||
@@ -48,7 +48,7 @@
|
||||
"cmd-shift-backspace": "editor::Cancel",
|
||||
"cmd-r": "agent::NewThread",
|
||||
"cmd-shift-v": "editor::Paste",
|
||||
"cmd-shift-k": "assistant::InsertIntoEditor"
|
||||
"cmd-shift-k": "assistant::InsertIntoEditor",
|
||||
// "escape": "agent::ToggleFocus"
|
||||
///// Enable when Zed supports multiple thread tabs
|
||||
// "cmd-t": // new thread tab
|
||||
@@ -57,28 +57,28 @@
|
||||
///// Enable if Zed adds support for keyboard navigation of thread elements
|
||||
// "tab": // cycle to next message
|
||||
// "shift-tab": // cycle to previous message
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && editor_agent_diff",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "agent::KeepAll",
|
||||
"cmd-backspace": "agent::RejectAll"
|
||||
}
|
||||
"cmd-backspace": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full && edit_prediction",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-right": "editor::AcceptPartialEditPrediction"
|
||||
}
|
||||
"cmd-right": "editor::AcceptPartialEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k": "assistant::InlineAssist"
|
||||
}
|
||||
}
|
||||
"cmd-k": "assistant::InlineAssist",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
{
|
||||
"context": "!GitPanel",
|
||||
"bindings": {
|
||||
"ctrl-g": "menu::Cancel"
|
||||
}
|
||||
"ctrl-g": "menu::Cancel",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Workaround to avoid falling back to default bindings.
|
||||
@@ -15,8 +15,8 @@
|
||||
// NOTE: must be declared before the `Editor` override.
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-g": null // currently activates `go_to_line::Toggle` when there is nothing to cancel
|
||||
}
|
||||
"ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -79,8 +79,8 @@
|
||||
"ctrl-s": "buffer_search::Deploy", // isearch-forward
|
||||
"ctrl-r": "buffer_search::Deploy", // isearch-backward
|
||||
"alt-^": "editor::JoinLines", // join-line
|
||||
"alt-q": "editor::Rewrap" // fill-paragraph
|
||||
}
|
||||
"alt-q": "editor::Rewrap", // fill-paragraph
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && selection_mode", // region selection
|
||||
@@ -116,22 +116,22 @@
|
||||
"alt->": "editor::SelectToEnd",
|
||||
"ctrl-home": "editor::SelectToBeginning",
|
||||
"ctrl-end": "editor::SelectToEnd",
|
||||
"ctrl-g": "editor::Cancel"
|
||||
}
|
||||
"ctrl-g": "editor::Cancel",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && (showing_code_actions || showing_completions)",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::ContextMenuPrevious",
|
||||
"ctrl-n": "editor::ContextMenuNext"
|
||||
}
|
||||
"ctrl-n": "editor::ContextMenuNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_signature_help && !showing_completions",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::SignatureHelpPrevious",
|
||||
"ctrl-n": "editor::SignatureHelpNext"
|
||||
}
|
||||
"ctrl-n": "editor::SignatureHelpNext",
|
||||
},
|
||||
},
|
||||
// Example setting for using emacs-style tab
|
||||
// (i.e. indent the current line / selection or perform symbol completion depending on context)
|
||||
@@ -161,8 +161,8 @@
|
||||
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
|
||||
"ctrl-x ctrl-s": "workspace::Save", // save-buffer
|
||||
"ctrl-x ctrl-w": "workspace::SaveAs", // write-file
|
||||
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
|
||||
}
|
||||
"ctrl-x s": "workspace::SaveAll", // save-some-buffers
|
||||
},
|
||||
},
|
||||
{
|
||||
// Workaround to enable using native emacs from the Zed terminal.
|
||||
@@ -182,22 +182,22 @@
|
||||
"ctrl-x ctrl-f": null, // find-file
|
||||
"ctrl-x ctrl-s": null, // save-buffer
|
||||
"ctrl-x ctrl-w": null, // write-file
|
||||
"ctrl-x s": null // save-some-buffers
|
||||
}
|
||||
"ctrl-x s": null, // save-some-buffers
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-r": "search::SelectPreviousMatch",
|
||||
"ctrl-g": "buffer_search::Dismiss"
|
||||
}
|
||||
"ctrl-g": "buffer_search::Dismiss",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-alt-left": "pane::GoBack",
|
||||
"ctrl-alt-right": "pane::GoForward"
|
||||
}
|
||||
}
|
||||
"ctrl-alt-right": "pane::GoForward",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"shift-f8": "debugger::StepOut",
|
||||
"f9": "debugger::Continue",
|
||||
"shift-f9": "debugger::Start",
|
||||
"alt-shift-f9": "debugger::Start"
|
||||
}
|
||||
"alt-shift-f9": "debugger::Start",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -60,8 +60,8 @@
|
||||
"cmd-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-f8": "editor::ToggleBreakpoint",
|
||||
"ctrl-shift-f8": "editor::EditLogBreakpoint",
|
||||
"cmd-shift-u": "editor::ToggleCase"
|
||||
}
|
||||
"cmd-shift-u": "editor::ToggleCase",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
@@ -74,14 +74,14 @@
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"cmd-j": "editor::Hover",
|
||||
"cmd-p": "editor::ShowSignatureHelp",
|
||||
"cmd-\\": "assistant::InlineAssist"
|
||||
}
|
||||
"cmd-\\": "assistant::InlineAssist",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"shift-enter": "search::SelectPreviousMatch"
|
||||
}
|
||||
"shift-enter": "search::SelectPreviousMatch",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar || ProjectSearchBar",
|
||||
@@ -93,8 +93,8 @@
|
||||
"ctrl-alt-c": "search::ToggleCaseSensitive",
|
||||
"ctrl-alt-e": "search::ToggleSelection",
|
||||
"ctrl-alt-w": "search::ToggleWholeWord",
|
||||
"ctrl-alt-x": "search::ToggleRegex"
|
||||
}
|
||||
"ctrl-alt-x": "search::ToggleRegex",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
@@ -116,8 +116,8 @@
|
||||
"cmd-1": "project_panel::ToggleFocus",
|
||||
"cmd-5": "debug_panel::ToggleFocus",
|
||||
"cmd-6": "diagnostics::Deploy",
|
||||
"cmd-7": "outline_panel::ToggleFocus"
|
||||
}
|
||||
"cmd-7": "outline_panel::ToggleFocus",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane", // this is to override the default Pane mappings to switch tabs
|
||||
@@ -131,15 +131,15 @@
|
||||
"cmd-7": "outline_panel::ToggleFocus",
|
||||
"cmd-8": null, // Services (bottom dock)
|
||||
"cmd-9": null, // Git History (bottom dock)
|
||||
"cmd-0": "git_panel::ToggleFocus"
|
||||
}
|
||||
"cmd-0": "git_panel::ToggleFocus",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
"alt-f12": "terminal_panel::Toggle",
|
||||
"cmd-shift-k": "git::Push"
|
||||
}
|
||||
"cmd-shift-k": "git::Push",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
@@ -147,8 +147,8 @@
|
||||
"cmd-alt-left": "pane::GoBack",
|
||||
"cmd-alt-right": "pane::GoForward",
|
||||
"alt-left": "pane::ActivatePreviousItem",
|
||||
"alt-right": "pane::ActivateNextItem"
|
||||
}
|
||||
"alt-right": "pane::ActivateNextItem",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
@@ -159,8 +159,8 @@
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"shift-f6": "project_panel::Rename"
|
||||
}
|
||||
"shift-f6": "project_panel::Rename",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
@@ -170,8 +170,8 @@
|
||||
"cmd-up": "terminal::ScrollLineUp",
|
||||
"cmd-down": "terminal::ScrollLineDown",
|
||||
"shift-pageup": "terminal::ScrollPageUp",
|
||||
"shift-pagedown": "terminal::ScrollPageDown"
|
||||
}
|
||||
"shift-pagedown": "terminal::ScrollPageDown",
|
||||
},
|
||||
},
|
||||
{ "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } },
|
||||
{ "context": "ProjectPanel", "bindings": { "cmd-1": "workspace::CloseActiveDock" } },
|
||||
@@ -182,7 +182,7 @@
|
||||
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||
"bindings": {
|
||||
"escape": "editor::ToggleFocus",
|
||||
"shift-escape": "workspace::CloseActiveDock"
|
||||
}
|
||||
}
|
||||
"shift-escape": "workspace::CloseActiveDock",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
"ctrl-^": ["workspace::MoveItemToPane", { "destination": 5 }],
|
||||
"ctrl-&": ["workspace::MoveItemToPane", { "destination": 6 }],
|
||||
"ctrl-*": ["workspace::MoveItemToPane", { "destination": 7 }],
|
||||
"ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }]
|
||||
}
|
||||
"ctrl-(": ["workspace::MoveItemToPane", { "destination": 8 }],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -57,20 +57,20 @@
|
||||
"ctrl-right": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart"
|
||||
}
|
||||
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"cmd-r": "outline::Toggle"
|
||||
}
|
||||
"cmd-r": "outline::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"cmd-k cmd-z": "git::Restore"
|
||||
}
|
||||
"cmd-k cmd-z": "git::Restore",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
@@ -85,8 +85,8 @@
|
||||
"cmd-6": ["pane::ActivateItem", 5],
|
||||
"cmd-7": ["pane::ActivateItem", 6],
|
||||
"cmd-8": ["pane::ActivateItem", 7],
|
||||
"cmd-9": "pane::ActivateLastItem"
|
||||
}
|
||||
"cmd-9": "pane::ActivateLastItem",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
@@ -95,7 +95,7 @@
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"shift-cmd-r": "project_symbols::Toggle",
|
||||
// Currently busted: https://github.com/zed-industries/feedback/issues/898
|
||||
"ctrl-0": "project_panel::ToggleFocus"
|
||||
}
|
||||
}
|
||||
"ctrl-0": "project_panel::ToggleFocus",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-o": "projects::OpenRecent",
|
||||
"cmd-alt-tab": "project_panel::ToggleFocus"
|
||||
}
|
||||
"cmd-alt-tab": "project_panel::ToggleFocus",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
@@ -15,8 +15,8 @@
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"cmd-alt-enter": "editor::NewlineAbove",
|
||||
"cmd-shift-l": "editor::SelectLine",
|
||||
"cmd-shift-t": "outline::Toggle"
|
||||
}
|
||||
"cmd-shift-t": "outline::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
@@ -41,30 +41,30 @@
|
||||
"ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-shift-u": "editor::ConvertToLowerCase",
|
||||
"ctrl-alt-u": "editor::ConvertToUpperCamelCase",
|
||||
"ctrl-_": "editor::ConvertToSnakeCase"
|
||||
}
|
||||
"ctrl-_": "editor::ConvertToSnakeCase",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-shift-s": "search::SelectPreviousMatch"
|
||||
}
|
||||
"ctrl-shift-s": "search::SelectPreviousMatch",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-alt-ctrl-d": "workspace::ToggleLeftDock",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-t": "project_symbols::Toggle"
|
||||
}
|
||||
"cmd-shift-t": "project_symbols::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"alt-cmd-r": "search::ToggleRegex",
|
||||
"ctrl-tab": "project_panel::ToggleFocus"
|
||||
}
|
||||
"ctrl-tab": "project_panel::ToggleFocus",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
@@ -75,11 +75,11 @@
|
||||
"return": "project_panel::Rename",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
"cmd-v": "project_panel::Paste",
|
||||
"cmd-alt-c": "project_panel::CopyPath"
|
||||
}
|
||||
"cmd-alt-c": "project_panel::CopyPath",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
"bindings": {},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"backspace": "editor::Backspace",
|
||||
"delete": "editor::Delete",
|
||||
"left": "editor::MoveLeft",
|
||||
"right": "editor::MoveRight"
|
||||
}
|
||||
}
|
||||
"right": "editor::MoveRight",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -180,10 +180,9 @@
|
||||
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
|
||||
"ctrl-w space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-w g space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-6": "pane::AlternateFile",
|
||||
"ctrl-^": "pane::AlternateFile",
|
||||
".": "vim::Repeat"
|
||||
}
|
||||
".": "vim::Repeat",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal || vim_mode == visual || vim_mode == operator",
|
||||
@@ -224,8 +223,8 @@
|
||||
"] r": "vim::GoToNextReference",
|
||||
// tree-sitter related commands
|
||||
"[ x": "vim::SelectLargerSyntaxNode",
|
||||
"] x": "vim::SelectSmallerSyntaxNode"
|
||||
}
|
||||
"] x": "vim::SelectSmallerSyntaxNode",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
@@ -262,16 +261,16 @@
|
||||
"[ d": "editor::GoToPreviousDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
"[ c": "editor::GoToPreviousHunk",
|
||||
"g c": "vim::PushToggleComments"
|
||||
}
|
||||
"g c": "vim::PushToggleComments",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "VimControl && VimCount",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0],
|
||||
":": "vim::CountCommand",
|
||||
"%": "vim::GoToPercentage"
|
||||
}
|
||||
"%": "vim::GoToPercentage",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == visual",
|
||||
@@ -323,8 +322,8 @@
|
||||
"g w": "vim::Rewrap",
|
||||
"g ?": "vim::ConvertToRot13",
|
||||
// "g ?": "vim::ConvertToRot47",
|
||||
"\"": "vim::PushRegister"
|
||||
}
|
||||
"\"": "vim::PushRegister",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_select",
|
||||
@@ -344,8 +343,8 @@
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
".": "vim::Repeat",
|
||||
"alt-.": "vim::RepeatFind"
|
||||
}
|
||||
"alt-.": "vim::RepeatFind",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == insert",
|
||||
@@ -375,8 +374,8 @@
|
||||
"ctrl-r": "vim::PushRegister",
|
||||
"insert": "vim::ToggleReplace",
|
||||
"ctrl-o": "vim::TemporaryNormal",
|
||||
"ctrl-s": "editor::ShowSignatureHelp"
|
||||
}
|
||||
"ctrl-s": "editor::ShowSignatureHelp",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "showing_completions",
|
||||
@@ -384,8 +383,8 @@
|
||||
"ctrl-d": "vim::ScrollDown",
|
||||
"ctrl-u": "vim::ScrollUp",
|
||||
"ctrl-e": "vim::LineDown",
|
||||
"ctrl-y": "vim::LineUp"
|
||||
}
|
||||
"ctrl-y": "vim::LineUp",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
|
||||
@@ -410,23 +409,31 @@
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
"\"": "vim::PushRegister",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem"
|
||||
}
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "VimControl && vim_mode == helix_normal && !menu",
|
||||
"bindings": {
|
||||
"j": ["vim::Down", { "display_lines": true }],
|
||||
"down": ["vim::Down", { "display_lines": true }],
|
||||
"k": ["vim::Up", { "display_lines": true }],
|
||||
"up": ["vim::Up", { "display_lines": true }],
|
||||
"g j": "vim::Down",
|
||||
"g down": "vim::Down",
|
||||
"g k": "vim::Up",
|
||||
"g up": "vim::Up",
|
||||
"escape": "vim::SwitchToHelixNormalMode",
|
||||
"i": "vim::HelixInsert",
|
||||
"a": "vim::HelixAppend",
|
||||
"ctrl-[": "editor::Cancel"
|
||||
}
|
||||
"ctrl-[": "editor::Cancel",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_select && !menu",
|
||||
"bindings": {
|
||||
"escape": "vim::SwitchToHelixNormalMode"
|
||||
}
|
||||
"escape": "vim::SwitchToHelixNormalMode",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
|
||||
@@ -446,9 +453,9 @@
|
||||
"shift-r": "editor::Paste",
|
||||
"`": "vim::ConvertToLowerCase",
|
||||
"alt-`": "vim::ConvertToUpperCase",
|
||||
"insert": "vim::InsertBefore",
|
||||
"insert": "vim::InsertBefore", // not a helix default
|
||||
"shift-u": "editor::Redo",
|
||||
"ctrl-r": "vim::Redo",
|
||||
"ctrl-r": "vim::Redo", // not a helix default
|
||||
"y": "vim::HelixYank",
|
||||
"p": "vim::HelixPaste",
|
||||
"shift-p": ["vim::HelixPaste", { "before": true }],
|
||||
@@ -477,6 +484,7 @@
|
||||
"alt-p": "editor::SelectPreviousSyntaxNode",
|
||||
"alt-n": "editor::SelectNextSyntaxNode",
|
||||
|
||||
// Search
|
||||
"n": "vim::HelixSelectNext",
|
||||
"shift-n": "vim::HelixSelectPrevious",
|
||||
|
||||
@@ -484,27 +492,27 @@
|
||||
"g e": "vim::EndOfDocument",
|
||||
"g h": "vim::StartOfLine",
|
||||
"g l": "vim::EndOfLine",
|
||||
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
||||
"g s": "vim::FirstNonWhitespace",
|
||||
"g t": "vim::WindowTop",
|
||||
"g c": "vim::WindowMiddle",
|
||||
"g b": "vim::WindowBottom",
|
||||
"g r": "editor::FindAllReferences", // zed specific
|
||||
"g r": "editor::FindAllReferences",
|
||||
"g n": "pane::ActivateNextItem",
|
||||
"shift-l": "pane::ActivateNextItem",
|
||||
"shift-l": "pane::ActivateNextItem", // not a helix default
|
||||
"g p": "pane::ActivatePreviousItem",
|
||||
"shift-h": "pane::ActivatePreviousItem",
|
||||
"g .": "vim::HelixGotoLastModification", // go to last modification
|
||||
"shift-h": "pane::ActivatePreviousItem", // not a helix default
|
||||
"g .": "vim::HelixGotoLastModification",
|
||||
|
||||
// Window mode
|
||||
"space w h": "workspace::ActivatePaneLeft",
|
||||
"space w l": "workspace::ActivatePaneRight",
|
||||
"space w k": "workspace::ActivatePaneUp",
|
||||
"space w j": "workspace::ActivatePaneDown",
|
||||
"space w q": "pane::CloseActiveItem",
|
||||
"space w s": "pane::SplitRight",
|
||||
"space w r": "pane::SplitRight",
|
||||
"space w v": "pane::SplitDown",
|
||||
"space w d": "pane::SplitDown",
|
||||
"space w s": "pane::SplitRight",
|
||||
"space w h": "workspace::ActivatePaneLeft",
|
||||
"space w j": "workspace::ActivatePaneDown",
|
||||
"space w k": "workspace::ActivatePaneUp",
|
||||
"space w l": "workspace::ActivatePaneRight",
|
||||
"space w q": "pane::CloseActiveItem",
|
||||
"space w r": "pane::SplitRight", // not a helix default
|
||||
"space w d": "pane::SplitDown", // not a helix default
|
||||
|
||||
// Space mode
|
||||
"space f": "file_finder::Toggle",
|
||||
@@ -518,6 +526,7 @@
|
||||
"space c": "editor::ToggleComments",
|
||||
"space p": "editor::Paste",
|
||||
"space y": "editor::Copy",
|
||||
"space /": "pane::DeploySearch",
|
||||
|
||||
// Other
|
||||
":": "command_palette::Toggle",
|
||||
@@ -525,24 +534,22 @@
|
||||
"]": ["vim::PushHelixNext", { "around": true }],
|
||||
"[": ["vim::PushHelixPrevious", { "around": true }],
|
||||
"g q": "vim::PushRewrap",
|
||||
"g w": "vim::PushRewrap"
|
||||
// "tab": "pane::ActivateNextItem",
|
||||
// "shift-tab": "pane::ActivatePrevItem",
|
||||
}
|
||||
"g w": "vim::PushRewrap", // not a helix default & clashes with helix `goto_word`
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::ShowWordCompletions",
|
||||
"ctrl-n": "editor::ShowWordCompletions"
|
||||
}
|
||||
"ctrl-n": "editor::ShowWordCompletions",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "(vim_mode == insert || vim_mode == normal) && showing_signature_help && !showing_completions",
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::SignatureHelpPrevious",
|
||||
"ctrl-n": "editor::SignatureHelpNext"
|
||||
}
|
||||
"ctrl-n": "editor::SignatureHelpNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == replace",
|
||||
@@ -558,8 +565,8 @@
|
||||
"backspace": "vim::UndoReplace",
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"insert": "vim::InsertBefore"
|
||||
}
|
||||
"insert": "vim::InsertBefore",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == waiting",
|
||||
@@ -571,14 +578,14 @@
|
||||
"escape": "vim::ClearOperators",
|
||||
"ctrl-k": ["vim::PushDigraph", {}],
|
||||
"ctrl-v": ["vim::PushLiteral", {}],
|
||||
"ctrl-q": ["vim::PushLiteral", {}]
|
||||
}
|
||||
"ctrl-q": ["vim::PushLiteral", {}],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == waiting && (vim_operator == ys || vim_operator == cs)",
|
||||
"bindings": {
|
||||
"escape": "vim::SwitchToNormalMode"
|
||||
}
|
||||
"escape": "vim::SwitchToNormalMode",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == operator",
|
||||
@@ -586,8 +593,8 @@
|
||||
"ctrl-c": "vim::ClearOperators",
|
||||
"ctrl-[": "vim::ClearOperators",
|
||||
"escape": "vim::ClearOperators",
|
||||
"g c": "vim::Comment"
|
||||
}
|
||||
"g c": "vim::Comment",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous",
|
||||
@@ -624,14 +631,14 @@
|
||||
"shift-i": ["vim::IndentObj", { "include_below": true }],
|
||||
"f": "vim::Method",
|
||||
"c": "vim::Class",
|
||||
"e": "vim::EntireFile"
|
||||
}
|
||||
"e": "vim::EntireFile",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == helix_m",
|
||||
"bindings": {
|
||||
"m": "vim::Matching"
|
||||
}
|
||||
"m": "vim::Matching",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == helix_next",
|
||||
@@ -648,8 +655,8 @@
|
||||
"x": "editor::SelectSmallerSyntaxNode",
|
||||
"d": "editor::GoToDiagnostic",
|
||||
"c": "editor::GoToHunk",
|
||||
"space": "vim::InsertEmptyLineBelow"
|
||||
}
|
||||
"space": "vim::InsertEmptyLineBelow",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == helix_previous",
|
||||
@@ -666,8 +673,8 @@
|
||||
"x": "editor::SelectLargerSyntaxNode",
|
||||
"d": "editor::GoToPreviousDiagnostic",
|
||||
"c": "editor::GoToPreviousHunk",
|
||||
"space": "vim::InsertEmptyLineAbove"
|
||||
}
|
||||
"space": "vim::InsertEmptyLineAbove",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == c",
|
||||
@@ -675,8 +682,8 @@
|
||||
"c": "vim::CurrentLine",
|
||||
"x": "vim::Exchange",
|
||||
"d": "editor::Rename", // zed specific
|
||||
"s": ["vim::PushChangeSurrounds", {}]
|
||||
}
|
||||
"s": ["vim::PushChangeSurrounds", {}],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == d",
|
||||
@@ -688,36 +695,36 @@
|
||||
"shift-o": "git::ToggleStaged",
|
||||
"p": "git::Restore", // "d p"
|
||||
"u": "git::StageAndNext", // "d u"
|
||||
"shift-u": "git::UnstageAndNext" // "d shift-u"
|
||||
}
|
||||
"shift-u": "git::UnstageAndNext", // "d shift-u"
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gu",
|
||||
"bindings": {
|
||||
"g u": "vim::CurrentLine",
|
||||
"u": "vim::CurrentLine"
|
||||
}
|
||||
"u": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gU",
|
||||
"bindings": {
|
||||
"g shift-u": "vim::CurrentLine",
|
||||
"shift-u": "vim::CurrentLine"
|
||||
}
|
||||
"shift-u": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == g~",
|
||||
"bindings": {
|
||||
"g ~": "vim::CurrentLine",
|
||||
"~": "vim::CurrentLine"
|
||||
}
|
||||
"~": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == g?",
|
||||
"bindings": {
|
||||
"g ?": "vim::CurrentLine",
|
||||
"?": "vim::CurrentLine"
|
||||
}
|
||||
"?": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gq",
|
||||
@@ -725,66 +732,66 @@
|
||||
"g q": "vim::CurrentLine",
|
||||
"q": "vim::CurrentLine",
|
||||
"g w": "vim::CurrentLine",
|
||||
"w": "vim::CurrentLine"
|
||||
}
|
||||
"w": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == y",
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine",
|
||||
"v": "vim::PushForcedMotion",
|
||||
"s": ["vim::PushAddSurrounds", {}]
|
||||
}
|
||||
"s": ["vim::PushAddSurrounds", {}],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == ys",
|
||||
"bindings": {
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
"s": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == >",
|
||||
"bindings": {
|
||||
">": "vim::CurrentLine"
|
||||
}
|
||||
">": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == <",
|
||||
"bindings": {
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
"<": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == eq",
|
||||
"bindings": {
|
||||
"=": "vim::CurrentLine"
|
||||
}
|
||||
"=": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == sh",
|
||||
"bindings": {
|
||||
"!": "vim::CurrentLine"
|
||||
}
|
||||
"!": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gc",
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine"
|
||||
}
|
||||
"c": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gR",
|
||||
"bindings": {
|
||||
"r": "vim::CurrentLine",
|
||||
"shift-r": "vim::CurrentLine"
|
||||
}
|
||||
"shift-r": "vim::CurrentLine",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == cx",
|
||||
"bindings": {
|
||||
"x": "vim::CurrentLine",
|
||||
"c": "vim::ClearExchange"
|
||||
}
|
||||
"c": "vim::ClearExchange",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == literal",
|
||||
@@ -826,15 +833,15 @@
|
||||
"tab": ["vim::Literal", ["tab", "\u0009"]],
|
||||
// zed extensions:
|
||||
"backspace": ["vim::Literal", ["backspace", "\u0008"]],
|
||||
"delete": ["vim::Literal", ["delete", "\u007F"]]
|
||||
}
|
||||
"delete": ["vim::Literal", ["delete", "\u007F"]],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
"enter": "vim::SearchSubmit",
|
||||
"escape": "buffer_search::Dismiss"
|
||||
}
|
||||
"escape": "buffer_search::Dismiss",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "VimControl && !menu || !Editor && !Terminal",
|
||||
@@ -895,15 +902,19 @@
|
||||
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
|
||||
"ctrl-w n": "workspace::NewFileSplitHorizontal",
|
||||
"g t": "vim::GoToTab",
|
||||
"g shift-t": "vim::GoToPreviousTab"
|
||||
}
|
||||
"g shift-t": "vim::GoToPreviousTab",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "!Editor && !Terminal",
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
"g /": "pane::DeploySearch"
|
||||
}
|
||||
"g /": "pane::DeploySearch",
|
||||
"] b": "pane::ActivateNextItem",
|
||||
"[ b": "pane::ActivatePreviousItem",
|
||||
"] shift-b": "pane::ActivateLastItem",
|
||||
"[ shift-b": ["pane::ActivateItem", 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
// netrw compatibility
|
||||
@@ -953,17 +964,45 @@
|
||||
"6": ["vim::Number", 6],
|
||||
"7": ["vim::Number", 7],
|
||||
"8": ["vim::Number", 8],
|
||||
"9": ["vim::Number", 9]
|
||||
}
|
||||
"9": ["vim::Number", 9],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel && not_editing",
|
||||
"bindings": {
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrevious",
|
||||
"h": "outline_panel::CollapseSelectedEntry",
|
||||
"j": "vim::MenuSelectNext",
|
||||
"k": "vim::MenuSelectPrevious",
|
||||
"down": "vim::MenuSelectNext",
|
||||
"up": "vim::MenuSelectPrevious",
|
||||
"l": "outline_panel::ExpandSelectedEntry",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst"
|
||||
}
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "outline_panel::SelectParent",
|
||||
"enter": "editor::ToggleFocus",
|
||||
"/": "menu::Cancel",
|
||||
"ctrl-u": "outline_panel::ScrollUp",
|
||||
"ctrl-d": "outline_panel::ScrollDown",
|
||||
"z t": "outline_panel::ScrollCursorTop",
|
||||
"z z": "outline_panel::ScrollCursorCenter",
|
||||
"z b": "outline_panel::ScrollCursorBottom",
|
||||
"0": ["vim::Number", 0],
|
||||
"1": ["vim::Number", 1],
|
||||
"2": ["vim::Number", 2],
|
||||
"3": ["vim::Number", 3],
|
||||
"4": ["vim::Number", 4],
|
||||
"5": ["vim::Number", 5],
|
||||
"6": ["vim::Number", 6],
|
||||
"7": ["vim::Number", 7],
|
||||
"8": ["vim::Number", 8],
|
||||
"9": ["vim::Number", 9],
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel && editing",
|
||||
"bindings": {
|
||||
"enter": "menu::Cancel",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && ChangesList",
|
||||
@@ -978,8 +1017,8 @@
|
||||
"x": "git::ToggleStaged",
|
||||
"shift-x": "git::StageAll",
|
||||
"g x": "git::StageRange",
|
||||
"shift-u": "git::UnstageAll"
|
||||
}
|
||||
"shift-u": "git::UnstageAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == auto_height && VimControl",
|
||||
@@ -990,8 +1029,8 @@
|
||||
"#": null,
|
||||
"*": null,
|
||||
"n": null,
|
||||
"shift-n": null
|
||||
}
|
||||
"shift-n": null,
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Picker > Editor",
|
||||
@@ -1000,29 +1039,29 @@
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-p": "menu::SelectPrevious",
|
||||
"ctrl-n": "menu::SelectNext"
|
||||
}
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor && VimControl && vim_mode == normal",
|
||||
"bindings": {
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
"escape": "menu::Cancel",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && edit_prediction",
|
||||
"bindings": {
|
||||
// This is identical to the binding in the base keymap, but the vim bindings above to
|
||||
// "vim::Tab" shadow it, so it needs to be bound again.
|
||||
"tab": "editor::AcceptEditPrediction"
|
||||
}
|
||||
"tab": "editor::AcceptEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor && VimControl",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat"
|
||||
}
|
||||
"enter": "agent::Chat",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "os != macos && Editor && edit_prediction_conflict",
|
||||
@@ -1030,8 +1069,8 @@
|
||||
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This
|
||||
// is because alt-tab may not be available, as it is often used for window switching on Linux
|
||||
// and Windows.
|
||||
"alt-l": "editor::AcceptEditPrediction"
|
||||
}
|
||||
"alt-l": "editor::AcceptEditPrediction",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "SettingsWindow > NavigationMenu && !search",
|
||||
@@ -1041,7 +1080,16 @@
|
||||
"k": "settings_editor::FocusPreviousNavEntry",
|
||||
"j": "settings_editor::FocusNextNavEntry",
|
||||
"g g": "settings_editor::FocusFirstNavEntry",
|
||||
"shift-g": "settings_editor::FocusLastNavEntry"
|
||||
}
|
||||
}
|
||||
"shift-g": "settings_editor::FocusLastNavEntry",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MarkdownPreview",
|
||||
"bindings": {
|
||||
"ctrl-u": "markdown::ScrollPageUp",
|
||||
"ctrl-d": "markdown::ScrollPageDown",
|
||||
"ctrl-y": "markdown::ScrollUp",
|
||||
"ctrl-e": "markdown::ScrollDown",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
43
assets/prompts/content_prompt_v2.hbs
Normal file
43
assets/prompts/content_prompt_v2.hbs
Normal file
@@ -0,0 +1,43 @@
|
||||
{{#if language_name}}
|
||||
Here's a file of {{language_name}} that the user is going to ask you to make an edit to.
|
||||
{{else}}
|
||||
Here's a file of text that the user is going to ask you to make an edit to.
|
||||
{{/if}}
|
||||
|
||||
The section you'll need to rewrite is marked with <rewrite_this></rewrite_this> tags.
|
||||
|
||||
<document>
|
||||
{{{document_content}}}
|
||||
</document>
|
||||
|
||||
{{#if is_truncated}}
|
||||
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
|
||||
{{/if}}
|
||||
|
||||
{{#if rewrite_section}}
|
||||
And here's the section to rewrite based on that prompt again for reference:
|
||||
|
||||
<rewrite_this>
|
||||
{{{rewrite_section}}}
|
||||
</rewrite_this>
|
||||
|
||||
{{#if diagnostic_errors}}
|
||||
Below are the diagnostic errors visible to the user. If the user requests problems to be fixed, use this information, but do not try to fix these errors if the user hasn't asked you to.
|
||||
|
||||
{{#each diagnostic_errors}}
|
||||
<diagnostic_error>
|
||||
<line_number>{{line_number}}</line_number>
|
||||
<error_message>{{error_message}}</error_message>
|
||||
<code_content>{{code_content}}</code_content>
|
||||
</diagnostic_error>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
|
||||
|
||||
Start at the indentation level in the original file in the rewritten {{content_type}}.
|
||||
|
||||
IMPORTANT: You MUST use one of the provided tools to make the rewrite or to provide an explanation as to why the user's request cannot be fulfilled. You MUST NOT send back unstructured text. If you need to make a statement or ask a question you MUST use one of the tools to do so.
|
||||
It is an error if you try to make a change that cannot be made simply by editing the rewrite_section.
|
||||
@@ -12,7 +12,7 @@
|
||||
"theme": {
|
||||
"mode": "system",
|
||||
"light": "One Light",
|
||||
"dark": "One Dark"
|
||||
"dark": "One Dark",
|
||||
},
|
||||
"icon_theme": "Zed (Default)",
|
||||
// The name of a base set of key bindings to use.
|
||||
@@ -29,7 +29,7 @@
|
||||
// Features that can be globally enabled or disabled
|
||||
"features": {
|
||||
// Which edit prediction provider to use.
|
||||
"edit_prediction_provider": "zed"
|
||||
"edit_prediction_provider": "zed",
|
||||
},
|
||||
// The name of a font to use for rendering text in the editor
|
||||
// ".ZedMono" currently aliases to Lilex
|
||||
@@ -69,7 +69,7 @@
|
||||
// The OpenType features to enable for text in the UI
|
||||
"ui_font_features": {
|
||||
// Disable ligatures:
|
||||
"calt": false
|
||||
"calt": false,
|
||||
},
|
||||
// The weight of the UI font in standard CSS units from 100 to 900.
|
||||
"ui_font_weight": 400,
|
||||
@@ -87,7 +87,7 @@
|
||||
"border_size": 0.0,
|
||||
// Opacity of the inactive panes. 0 means transparent, 1 means opaque.
|
||||
// Values are clamped to the [0.0, 1.0] range.
|
||||
"inactive_opacity": 1.0
|
||||
"inactive_opacity": 1.0,
|
||||
},
|
||||
// Layout mode of the bottom dock. Defaults to "contained"
|
||||
// choices: contained, full, left_aligned, right_aligned
|
||||
@@ -103,12 +103,12 @@
|
||||
"left_padding": 0.2,
|
||||
// The relative width of the right padding of the central pane from the
|
||||
// workspace when the centered layout is used.
|
||||
"right_padding": 0.2
|
||||
"right_padding": 0.2,
|
||||
},
|
||||
// Image viewer settings
|
||||
"image_viewer": {
|
||||
// The unit for image file sizes: "binary" (KiB, MiB) or decimal (KB, MB)
|
||||
"unit": "binary"
|
||||
"unit": "binary",
|
||||
},
|
||||
// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
|
||||
//
|
||||
@@ -296,7 +296,7 @@
|
||||
// When true, enables drag and drop text selection in buffer.
|
||||
"enabled": true,
|
||||
// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created.
|
||||
"delay": 300
|
||||
"delay": 300,
|
||||
},
|
||||
// What to do when go to definition yields no results.
|
||||
//
|
||||
@@ -400,14 +400,14 @@
|
||||
// Visible characters used to render whitespace when show_whitespaces is enabled.
|
||||
"whitespace_map": {
|
||||
"space": "•",
|
||||
"tab": "→"
|
||||
"tab": "→",
|
||||
},
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
// Join calls with the microphone live by default
|
||||
"mute_on_join": false,
|
||||
// Share your project when you are the first to join a channel
|
||||
"share_on_join": false
|
||||
"share_on_join": false,
|
||||
},
|
||||
// Toolbar related settings
|
||||
"toolbar": {
|
||||
@@ -420,7 +420,7 @@
|
||||
// Whether to show agent review buttons in the editor toolbar.
|
||||
"agent_review": true,
|
||||
// Whether to show code action buttons in the editor toolbar.
|
||||
"code_actions": false
|
||||
"code_actions": false,
|
||||
},
|
||||
// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only).
|
||||
"use_system_window_tabs": false,
|
||||
@@ -436,10 +436,12 @@
|
||||
"show_onboarding_banner": true,
|
||||
// Whether to show user picture in the titlebar.
|
||||
"show_user_picture": true,
|
||||
// Whether to show the user menu in the titlebar.
|
||||
"show_user_menu": true,
|
||||
// Whether to show the sign in button in the titlebar.
|
||||
"show_sign_in": true,
|
||||
// Whether to show the menus in the titlebar.
|
||||
"show_menus": false
|
||||
"show_menus": false,
|
||||
},
|
||||
"audio": {
|
||||
// Opt into the new audio system.
|
||||
@@ -472,7 +474,7 @@
|
||||
// the future we will migrate by setting this to false
|
||||
//
|
||||
// You need to rejoin a call for this setting to apply
|
||||
"experimental.legacy_audio_compatible": true
|
||||
"experimental.legacy_audio_compatible": true,
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
@@ -511,8 +513,8 @@
|
||||
// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
|
||||
"horizontal": true,
|
||||
// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
|
||||
"vertical": true
|
||||
}
|
||||
"vertical": true,
|
||||
},
|
||||
},
|
||||
// Minimap related settings
|
||||
"minimap": {
|
||||
@@ -560,7 +562,7 @@
|
||||
// 3. "gutter" or "none" to not highlight the current line in the minimap.
|
||||
"current_line_highlight": null,
|
||||
// Maximum number of columns to display in the minimap.
|
||||
"max_width_columns": 80
|
||||
"max_width_columns": 80,
|
||||
},
|
||||
// Enable middle-click paste on Linux.
|
||||
"middle_click_paste": true,
|
||||
@@ -583,7 +585,7 @@
|
||||
// Whether to show fold buttons in the gutter.
|
||||
"folds": true,
|
||||
// Minimum number of characters to reserve space for in the gutter.
|
||||
"min_line_number_digits": 4
|
||||
"min_line_number_digits": 4,
|
||||
},
|
||||
"indent_guides": {
|
||||
// Whether to show indent guides in the editor.
|
||||
@@ -604,7 +606,7 @@
|
||||
//
|
||||
// 1. "disabled"
|
||||
// 2. "indent_aware"
|
||||
"background_coloring": "disabled"
|
||||
"background_coloring": "disabled",
|
||||
},
|
||||
// Whether the editor will scroll beyond the last line.
|
||||
"scroll_beyond_last_line": "one_page",
|
||||
@@ -623,7 +625,7 @@
|
||||
"fast_scroll_sensitivity": 4.0,
|
||||
"sticky_scroll": {
|
||||
// Whether to stick scopes to the top of the editor.
|
||||
"enabled": false
|
||||
"enabled": false,
|
||||
},
|
||||
"relative_line_numbers": "disabled",
|
||||
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
|
||||
@@ -641,7 +643,7 @@
|
||||
// Whether to interpret the search query as a regular expression.
|
||||
"regex": false,
|
||||
// Whether to center the cursor on each search match when navigating.
|
||||
"center_on_match": false
|
||||
"center_on_match": false,
|
||||
},
|
||||
// When to populate a new search's query based on the text under the cursor.
|
||||
// This setting can take the following three values:
|
||||
@@ -684,8 +686,8 @@
|
||||
"shift": false,
|
||||
"alt": false,
|
||||
"platform": false,
|
||||
"function": false
|
||||
}
|
||||
"function": false,
|
||||
},
|
||||
},
|
||||
// Whether to resize all the panels in a dock when resizing the dock.
|
||||
// Can be a combination of "left", "right" and "bottom".
|
||||
@@ -733,7 +735,7 @@
|
||||
// "always"
|
||||
// 5. Never show the scrollbar:
|
||||
// "never"
|
||||
"show": null
|
||||
"show": null,
|
||||
},
|
||||
// Which files containing diagnostic errors/warnings to mark in the project panel.
|
||||
// This setting can take the following three values:
|
||||
@@ -756,7 +758,7 @@
|
||||
// "always"
|
||||
// 2. Never show indent guides:
|
||||
// "never"
|
||||
"show": "always"
|
||||
"show": "always",
|
||||
},
|
||||
// Sort order for entries in the project panel.
|
||||
// This setting can take three values:
|
||||
@@ -781,8 +783,8 @@
|
||||
// Whether to automatically open files after pasting or duplicating them.
|
||||
"on_paste": true,
|
||||
// Whether to automatically open files dropped from external sources.
|
||||
"on_drop": true
|
||||
}
|
||||
"on_drop": true,
|
||||
},
|
||||
},
|
||||
"outline_panel": {
|
||||
// Whether to show the outline panel button in the status bar
|
||||
@@ -815,7 +817,7 @@
|
||||
// "always"
|
||||
// 2. Never show indent guides:
|
||||
// "never"
|
||||
"show": "always"
|
||||
"show": "always",
|
||||
},
|
||||
// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
@@ -832,11 +834,11 @@
|
||||
// "always"
|
||||
// 5. Never show the scrollbar:
|
||||
// "never"
|
||||
"show": null
|
||||
"show": null,
|
||||
},
|
||||
// Default depth to expand outline items in the current file.
|
||||
// Set to 0 to collapse all items that have children, 1 or higher to collapse items at that depth or deeper.
|
||||
"expand_outlines_with_depth": 100
|
||||
"expand_outlines_with_depth": 100,
|
||||
},
|
||||
"collaboration_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
@@ -844,7 +846,7 @@
|
||||
// Where to dock the collaboration panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Default width of the collaboration panel.
|
||||
"default_width": 240
|
||||
"default_width": 240,
|
||||
},
|
||||
"git_panel": {
|
||||
// Whether to show the git panel button in the status bar.
|
||||
@@ -870,18 +872,22 @@
|
||||
//
|
||||
// Default: false
|
||||
"collapse_untracked_diff": false,
|
||||
/// Whether to show entries with tree or flat view in the panel
|
||||
///
|
||||
/// Default: false
|
||||
"tree_view": false,
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the git panel.
|
||||
//
|
||||
// Choices: always, auto, never, system
|
||||
// Default: inherits editor scrollbar settings
|
||||
// "show": null
|
||||
}
|
||||
},
|
||||
},
|
||||
"message_editor": {
|
||||
// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
// For example: typing `:wave:` gets replaced with `👋`.
|
||||
"auto_replace_emoji_shortcode": true
|
||||
"auto_replace_emoji_shortcode": true,
|
||||
},
|
||||
"notification_panel": {
|
||||
// Whether to show the notification panel button in the status bar.
|
||||
@@ -889,9 +895,11 @@
|
||||
// Where to dock the notification panel. Can be 'left' or 'right'.
|
||||
"dock": "right",
|
||||
// Default width of the notification panel.
|
||||
"default_width": 380
|
||||
"default_width": 380,
|
||||
},
|
||||
"agent": {
|
||||
// Whether the inline assistant should use streaming tools, when available
|
||||
"inline_assistant_use_streaming_tools": true,
|
||||
// Whether the agent is enabled.
|
||||
"enabled": true,
|
||||
// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
|
||||
@@ -900,6 +908,8 @@
|
||||
"button": true,
|
||||
// Where to dock the agent panel. Can be 'left', 'right' or 'bottom'.
|
||||
"dock": "right",
|
||||
// Where to dock the agents panel. Can be 'left' or 'right'.
|
||||
"agents_panel_dock": "left",
|
||||
// Default width when the agent panel is docked to the left or right.
|
||||
"default_width": 640,
|
||||
// Default height when the agent panel is docked to the bottom.
|
||||
@@ -911,7 +921,7 @@
|
||||
// The provider to use.
|
||||
"provider": "zed.dev",
|
||||
// The model to use.
|
||||
"model": "claude-sonnet-4"
|
||||
"model": "claude-sonnet-4",
|
||||
},
|
||||
// Additional parameters for language model requests. When making a request to a model, parameters will be taken
|
||||
// from the last entry in this list that matches the model's provider and name. In each entry, both provider
|
||||
@@ -966,8 +976,8 @@
|
||||
"grep": true,
|
||||
"terminal": true,
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
"web_search": true,
|
||||
},
|
||||
},
|
||||
"ask": {
|
||||
"name": "Ask",
|
||||
@@ -984,14 +994,14 @@
|
||||
"open": true,
|
||||
"grep": true,
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
"web_search": true,
|
||||
},
|
||||
},
|
||||
"minimal": {
|
||||
"name": "Minimal",
|
||||
"enable_all_context_servers": false,
|
||||
"tools": {}
|
||||
}
|
||||
"tools": {},
|
||||
},
|
||||
},
|
||||
// Where to show notifications when the agent has either completed
|
||||
// its response, or else needs confirmation before it can run a
|
||||
@@ -1020,7 +1030,7 @@
|
||||
// Minimum number of lines to display in the agent message editor.
|
||||
//
|
||||
// Default: 4
|
||||
"message_editor_min_lines": 4
|
||||
"message_editor_min_lines": 4,
|
||||
},
|
||||
// Whether the screen sharing icon is shown in the os status bar.
|
||||
"show_call_status_icon": true,
|
||||
@@ -1055,7 +1065,7 @@
|
||||
// Whether or not to show the navigation history buttons.
|
||||
"show_nav_history_buttons": true,
|
||||
// Whether or not to show the tab bar buttons.
|
||||
"show_tab_bar_buttons": true
|
||||
"show_tab_bar_buttons": true,
|
||||
},
|
||||
// Settings related to the editor's tabs
|
||||
"tabs": {
|
||||
@@ -1094,7 +1104,7 @@
|
||||
// "errors"
|
||||
// 3. Mark files with errors and warnings:
|
||||
// "all"
|
||||
"show_diagnostics": "off"
|
||||
"show_diagnostics": "off",
|
||||
},
|
||||
// Settings related to preview tabs.
|
||||
"preview_tabs": {
|
||||
@@ -1115,7 +1125,7 @@
|
||||
"enable_preview_file_from_code_navigation": true,
|
||||
// Whether to keep tabs in preview mode when code navigation is used to navigate away from them.
|
||||
// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
|
||||
"enable_keep_preview_on_code_navigation": false
|
||||
"enable_keep_preview_on_code_navigation": false,
|
||||
},
|
||||
// Settings related to the file finder.
|
||||
"file_finder": {
|
||||
@@ -1159,7 +1169,7 @@
|
||||
// * "all": Use all gitignored files
|
||||
// * "indexed": Use only the files Zed had indexed
|
||||
// * "smart": Be smart and search for ignored when called from a gitignored worktree
|
||||
"include_ignored": "smart"
|
||||
"include_ignored": "smart",
|
||||
},
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
@@ -1230,7 +1240,7 @@
|
||||
// Send debug info like crash reports.
|
||||
"diagnostics": true,
|
||||
// Send anonymized usage data like what languages you're using Zed with.
|
||||
"metrics": true
|
||||
"metrics": true,
|
||||
},
|
||||
// Whether to disable all AI features in Zed.
|
||||
//
|
||||
@@ -1264,7 +1274,7 @@
|
||||
"enabled": true,
|
||||
// Minimum time to wait before pulling diagnostics from the language server(s).
|
||||
// 0 turns the debounce off.
|
||||
"debounce_ms": 50
|
||||
"debounce_ms": 50,
|
||||
},
|
||||
// Settings for inline diagnostics
|
||||
"inline": {
|
||||
@@ -1282,8 +1292,8 @@
|
||||
"min_column": 0,
|
||||
// The minimum severity of the diagnostics to show inline.
|
||||
// Inherits editor's diagnostics' max severity settings when `null`.
|
||||
"max_severity": null
|
||||
}
|
||||
"max_severity": null,
|
||||
},
|
||||
},
|
||||
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
|
||||
// scans, file searches, and not be displayed in the project file tree. Takes precedence over `file_scan_inclusions`.
|
||||
@@ -1297,7 +1307,7 @@
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
"**/.classpath",
|
||||
"**/.settings"
|
||||
"**/.settings",
|
||||
],
|
||||
// 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
|
||||
@@ -1332,14 +1342,14 @@
|
||||
// Whether or not to display the git commit summary on the same line.
|
||||
"show_commit_summary": false,
|
||||
// The minimum column number to show the inline blame information at
|
||||
"min_column": 0
|
||||
"min_column": 0,
|
||||
},
|
||||
"blame": {
|
||||
"show_avatar": true
|
||||
"show_avatar": true,
|
||||
},
|
||||
// Control which information is shown in the branch picker.
|
||||
"branch_picker": {
|
||||
"show_author_name": true
|
||||
"show_author_name": true,
|
||||
},
|
||||
// How git hunks are displayed visually in the editor.
|
||||
// This setting can take two values:
|
||||
@@ -1351,7 +1361,7 @@
|
||||
"hunk_style": "staged_hollow",
|
||||
// Should the name or path be displayed first in the git view.
|
||||
// "path_style": "file_name_first" or "file_path_first"
|
||||
"path_style": "file_name_first"
|
||||
"path_style": "file_name_first",
|
||||
},
|
||||
// The list of custom Git hosting providers.
|
||||
"git_hosting_providers": [
|
||||
@@ -1385,7 +1395,7 @@
|
||||
"**/secrets.yml",
|
||||
"**/.zed/settings.json", // zed project settings
|
||||
"/**/zed/settings.json", // zed user settings
|
||||
"/**/zed/keymap.json"
|
||||
"/**/zed/keymap.json",
|
||||
],
|
||||
// When to show edit predictions previews in buffer.
|
||||
// This setting takes two possible values:
|
||||
@@ -1403,15 +1413,16 @@
|
||||
"copilot": {
|
||||
"enterprise_uri": null,
|
||||
"proxy": null,
|
||||
"proxy_no_verify": null
|
||||
"proxy_no_verify": null,
|
||||
},
|
||||
"codestral": {
|
||||
"model": null,
|
||||
"max_tokens": null
|
||||
"api_url": "https://codestral.mistral.ai",
|
||||
"model": "codestral-latest",
|
||||
"max_tokens": 150,
|
||||
},
|
||||
// Whether edit predictions are enabled when editing text threads in the agent panel.
|
||||
// This setting has no effect if globally disabled.
|
||||
"enabled_in_text_threads": true
|
||||
"enabled_in_text_threads": true,
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
@@ -1421,7 +1432,7 @@
|
||||
// May take 2 values:
|
||||
// 1. hour12
|
||||
// 2. hour24
|
||||
"hour_format": "hour12"
|
||||
"hour_format": "hour12",
|
||||
},
|
||||
// Status bar-related settings.
|
||||
"status_bar": {
|
||||
@@ -1432,7 +1443,7 @@
|
||||
// Whether to show the cursor position button in the status bar.
|
||||
"cursor_position_button": true,
|
||||
// Whether to show active line endings button in the status bar.
|
||||
"line_endings_button": false
|
||||
"line_endings_button": false,
|
||||
},
|
||||
// Settings specific to the terminal
|
||||
"terminal": {
|
||||
@@ -1553,8 +1564,8 @@
|
||||
// Preferred Conda manager to use when activating Conda environments.
|
||||
// Values: "auto", "conda", "mamba", "micromamba"
|
||||
// Default: "auto"
|
||||
"conda_manager": "auto"
|
||||
}
|
||||
"conda_manager": "auto",
|
||||
},
|
||||
},
|
||||
"toolbar": {
|
||||
// Whether to display the terminal title in its toolbar's breadcrumbs.
|
||||
@@ -1562,7 +1573,7 @@
|
||||
//
|
||||
// The shell running in the terminal needs to be configured to emit the title.
|
||||
// Example: `echo -e "\e]2;New Title\007";`
|
||||
"breadcrumbs": false
|
||||
"breadcrumbs": false,
|
||||
},
|
||||
// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
@@ -1579,7 +1590,7 @@
|
||||
// "always"
|
||||
// 5. Never show the scrollbar:
|
||||
// "never"
|
||||
"show": null
|
||||
"show": null,
|
||||
},
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
@@ -1642,30 +1653,26 @@
|
||||
// surrounding symbols or quotes
|
||||
[
|
||||
"(?x)",
|
||||
"# optionally starts with 0-2 opening prefix symbols",
|
||||
"[({\\[<]{0,2}",
|
||||
"# which may be followed by an opening quote",
|
||||
"(?<quote>[\"'`])?",
|
||||
"# `path` is the shortest sequence of any non-space character",
|
||||
"(?<link>(?<path>[^ ]+?",
|
||||
" # which may end with a line and optionally a column,",
|
||||
" (?<line_column>:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?",
|
||||
"))",
|
||||
"# which must be followed by a matching quote",
|
||||
"(?(<quote>)\\k<quote>)",
|
||||
"# and optionally a single closing symbol",
|
||||
"[)}\\]>]?",
|
||||
"# if line/column matched, may be followed by a description",
|
||||
"(?(<line_column>):[^ 0-9][^ ]*)?",
|
||||
"# which may be followed by trailing punctuation",
|
||||
"[.,:)}\\]>]*",
|
||||
"# and always includes trailing whitespace or end of line",
|
||||
"([ ]+|$)"
|
||||
]
|
||||
"(?<path>",
|
||||
" (",
|
||||
" # multi-char path: first char (not opening delimiter or space)",
|
||||
" [^({\\[<\"'`\\ ]",
|
||||
" # middle chars: non-space, and colon/paren only if not followed by digit/paren",
|
||||
" ([^\\ :(]|[:(][^0-9()])*",
|
||||
" # last char: not closing delimiter or colon",
|
||||
" [^()}\\]>\"'`.,;:\\ ]",
|
||||
" |",
|
||||
" # single-char path: not delimiter, punctuation, or space",
|
||||
" [^(){}\\[\\]<>\"'`.,;:\\ ]",
|
||||
" )",
|
||||
" # optional line/column suffix (included in path for PathWithPosition::parse_str)",
|
||||
" (:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:]?[0-9]+)?\\))?",
|
||||
")",
|
||||
],
|
||||
],
|
||||
// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a
|
||||
// timeout of `0` will disable path hyperlinking in terminal.
|
||||
"path_hyperlink_timeout_ms": 1
|
||||
"path_hyperlink_timeout_ms": 1,
|
||||
},
|
||||
"code_actions_on_format": {},
|
||||
// Settings related to running tasks.
|
||||
@@ -1681,7 +1688,7 @@
|
||||
// * Zed task from history (e.g. one-off task was spawned before)
|
||||
//
|
||||
// Default: true
|
||||
"prefer_lsp": true
|
||||
"prefer_lsp": true,
|
||||
},
|
||||
// An object whose keys are language names, and whose values
|
||||
// are arrays of filenames or extensions of files that should
|
||||
@@ -1698,7 +1705,7 @@
|
||||
"file_types": {
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
|
||||
"Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
|
||||
"Shell Script": [".env.*"]
|
||||
"Shell Script": [".env.*"],
|
||||
},
|
||||
// Settings for which version of Node.js and NPM to use when installing
|
||||
// language servers and Copilot.
|
||||
@@ -1714,14 +1721,14 @@
|
||||
// `path`, but not `npm_path`, Zed will assume that `npm` is located at
|
||||
// `${path}/../npm`.
|
||||
"path": null,
|
||||
"npm_path": null
|
||||
"npm_path": null,
|
||||
},
|
||||
// The extensions that Zed should automatically install on startup.
|
||||
//
|
||||
// If you don't want any of these extensions, add this field to your settings
|
||||
// and change the value to `false`.
|
||||
"auto_install_extensions": {
|
||||
"html": true
|
||||
"html": true,
|
||||
},
|
||||
// The capabilities granted to extensions.
|
||||
//
|
||||
@@ -1729,7 +1736,7 @@
|
||||
"granted_extension_capabilities": [
|
||||
{ "kind": "process:exec", "command": "*", "args": ["**"] },
|
||||
{ "kind": "download_file", "host": "*", "path": ["**"] },
|
||||
{ "kind": "npm:install", "package": "*" }
|
||||
{ "kind": "npm:install", "package": "*" },
|
||||
],
|
||||
// Controls how completions are processed for this language.
|
||||
"completions": {
|
||||
@@ -1780,7 +1787,7 @@
|
||||
// 4. "replace_suffix"
|
||||
// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like
|
||||
// `"insert"` otherwise.
|
||||
"lsp_insert_mode": "replace_suffix"
|
||||
"lsp_insert_mode": "replace_suffix",
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
@@ -1788,113 +1795,116 @@
|
||||
"language_servers": ["astro-language-server", "..."],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-astro"]
|
||||
}
|
||||
"plugins": ["prettier-plugin-astro"],
|
||||
},
|
||||
},
|
||||
"Blade": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"C": {
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false,
|
||||
"prettier": {
|
||||
"allowed": false
|
||||
}
|
||||
"allowed": false,
|
||||
},
|
||||
},
|
||||
"C++": {
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false,
|
||||
"prettier": {
|
||||
"allowed": false
|
||||
}
|
||||
"allowed": false,
|
||||
},
|
||||
},
|
||||
"CSharp": {
|
||||
"language_servers": ["roslyn", "!omnisharp", "..."],
|
||||
},
|
||||
"CSS": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"Dart": {
|
||||
"tab_size": 2
|
||||
"tab_size": 2,
|
||||
},
|
||||
"Diff": {
|
||||
"show_edit_predictions": false,
|
||||
"remove_trailing_whitespace_on_save": false,
|
||||
"ensure_final_newline_on_save": false
|
||||
"ensure_final_newline_on_save": false,
|
||||
},
|
||||
"Elixir": {
|
||||
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
|
||||
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."],
|
||||
},
|
||||
"Elm": {
|
||||
"tab_size": 4
|
||||
"tab_size": 4,
|
||||
},
|
||||
"Erlang": {
|
||||
"language_servers": ["erlang-ls", "!elp", "..."]
|
||||
"language_servers": ["erlang-ls", "!elp", "..."],
|
||||
},
|
||||
"Git Commit": {
|
||||
"allow_rewrap": "anywhere",
|
||||
"soft_wrap": "editor_width",
|
||||
"preferred_line_length": 72
|
||||
"preferred_line_length": 72,
|
||||
},
|
||||
"Go": {
|
||||
"hard_tabs": true,
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
"source.organizeImports": true,
|
||||
},
|
||||
"debuggers": ["Delve"]
|
||||
"debuggers": ["Delve"],
|
||||
},
|
||||
"GraphQL": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"HEEX": {
|
||||
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
|
||||
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."],
|
||||
},
|
||||
"HTML": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"HTML+ERB": {
|
||||
"language_servers": ["herb", "!ruby-lsp", "..."]
|
||||
"language_servers": ["herb", "!ruby-lsp", "..."],
|
||||
},
|
||||
"Java": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-java"]
|
||||
}
|
||||
"plugins": ["prettier-plugin-java"],
|
||||
},
|
||||
},
|
||||
"JavaScript": {
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"JSON": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"JSONC": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"JS+ERB": {
|
||||
"language_servers": ["!ruby-lsp", "..."]
|
||||
"language_servers": ["!ruby-lsp", "..."],
|
||||
},
|
||||
"Kotlin": {
|
||||
"language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."]
|
||||
"language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."],
|
||||
},
|
||||
"LaTeX": {
|
||||
"formatter": "language_server",
|
||||
"language_servers": ["texlab", "..."],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-latex"]
|
||||
}
|
||||
"plugins": ["prettier-plugin-latex"],
|
||||
},
|
||||
},
|
||||
"Markdown": {
|
||||
"format_on_save": "off",
|
||||
@@ -1902,136 +1912,145 @@
|
||||
"remove_trailing_whitespace_on_save": false,
|
||||
"allow_rewrap": "anywhere",
|
||||
"soft_wrap": "editor_width",
|
||||
"completions": {
|
||||
"words": "disabled",
|
||||
},
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"PHP": {
|
||||
"language_servers": ["phpactor", "!intelephense", "!phptools", "..."],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-php"],
|
||||
"parser": "php"
|
||||
}
|
||||
"parser": "php",
|
||||
},
|
||||
},
|
||||
"Plain Text": {
|
||||
"allow_rewrap": "anywhere",
|
||||
"soft_wrap": "editor_width"
|
||||
"soft_wrap": "editor_width",
|
||||
"completions": {
|
||||
"words": "disabled",
|
||||
},
|
||||
},
|
||||
"Proto": {
|
||||
"language_servers": ["buf", "!protols", "!protobuf-language-server", "..."],
|
||||
},
|
||||
"Python": {
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports.ruff": true
|
||||
"source.organizeImports.ruff": true,
|
||||
},
|
||||
"formatter": {
|
||||
"language_server": {
|
||||
"name": "ruff"
|
||||
}
|
||||
"name": "ruff",
|
||||
},
|
||||
},
|
||||
"debuggers": ["Debugpy"],
|
||||
"language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."]
|
||||
"language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."],
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."],
|
||||
},
|
||||
"Rust": {
|
||||
"debuggers": ["CodeLLDB"]
|
||||
"debuggers": ["CodeLLDB"],
|
||||
},
|
||||
"SCSS": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"Starlark": {
|
||||
"language_servers": ["starpls", "!buck2-lsp", "..."]
|
||||
"language_servers": ["starpls", "!buck2-lsp", "..."],
|
||||
},
|
||||
"Svelte": {
|
||||
"language_servers": ["svelte-language-server", "..."],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-svelte"]
|
||||
}
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
},
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"Twig": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"TypeScript": {
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"SystemVerilog": {
|
||||
"format_on_save": "off",
|
||||
"language_servers": ["!slang", "..."],
|
||||
"use_on_type_format": false
|
||||
"use_on_type_format": false,
|
||||
},
|
||||
"Vue.js": {
|
||||
"language_servers": ["vue-language-server", "vtsls", "..."],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"XML": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-xml"]
|
||||
}
|
||||
"plugins": ["@prettier/plugin-xml"],
|
||||
},
|
||||
},
|
||||
"YAML": {
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
"allowed": true,
|
||||
},
|
||||
},
|
||||
"YAML+ERB": {
|
||||
"language_servers": ["!ruby-lsp", "..."]
|
||||
"language_servers": ["!ruby-lsp", "..."],
|
||||
},
|
||||
"Zig": {
|
||||
"language_servers": ["zls", "..."]
|
||||
}
|
||||
"language_servers": ["zls", "..."],
|
||||
},
|
||||
},
|
||||
// Different settings for specific language models.
|
||||
"language_models": {
|
||||
"anthropic": {
|
||||
"api_url": "https://api.anthropic.com"
|
||||
"api_url": "https://api.anthropic.com",
|
||||
},
|
||||
"bedrock": {},
|
||||
"google": {
|
||||
"api_url": "https://generativelanguage.googleapis.com"
|
||||
"api_url": "https://generativelanguage.googleapis.com",
|
||||
},
|
||||
"ollama": {
|
||||
"api_url": "http://localhost:11434"
|
||||
"api_url": "http://localhost:11434",
|
||||
},
|
||||
"openai": {
|
||||
"api_url": "https://api.openai.com/v1"
|
||||
"api_url": "https://api.openai.com/v1",
|
||||
},
|
||||
"openai_compatible": {},
|
||||
"open_router": {
|
||||
"api_url": "https://openrouter.ai/api/v1"
|
||||
"api_url": "https://openrouter.ai/api/v1",
|
||||
},
|
||||
"lmstudio": {
|
||||
"api_url": "http://localhost:1234/api/v0"
|
||||
"api_url": "http://localhost:1234/api/v0",
|
||||
},
|
||||
"deepseek": {
|
||||
"api_url": "https://api.deepseek.com/v1"
|
||||
"api_url": "https://api.deepseek.com/v1",
|
||||
},
|
||||
"mistral": {
|
||||
"api_url": "https://api.mistral.ai/v1"
|
||||
"api_url": "https://api.mistral.ai/v1",
|
||||
},
|
||||
"vercel": {
|
||||
"api_url": "https://api.v0.dev/v1"
|
||||
"api_url": "https://api.v0.dev/v1",
|
||||
},
|
||||
"x_ai": {
|
||||
"api_url": "https://api.x.ai/v1"
|
||||
"api_url": "https://api.x.ai/v1",
|
||||
},
|
||||
"zed.dev": {}
|
||||
"zed.dev": {},
|
||||
},
|
||||
"session": {
|
||||
// Whether or not to restore unsaved buffers on restart.
|
||||
@@ -2040,7 +2059,7 @@
|
||||
// dirty files when closing the application.
|
||||
//
|
||||
// Default: true
|
||||
"restore_unsaved_buffers": true
|
||||
"restore_unsaved_buffers": true,
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
@@ -2058,11 +2077,11 @@
|
||||
// "singleQuote": true
|
||||
// Forces Prettier integration to use a specific parser name when formatting files with the language
|
||||
// when set to a non-empty string.
|
||||
"parser": ""
|
||||
"parser": "",
|
||||
},
|
||||
// Settings for auto-closing of JSX tags.
|
||||
"jsx_tag_auto_close": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
@@ -2083,19 +2102,19 @@
|
||||
// Specify the DAP name as a key here.
|
||||
"CodeLLDB": {
|
||||
"env": {
|
||||
"RUST_LOG": "info"
|
||||
}
|
||||
}
|
||||
"RUST_LOG": "info",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Common language server settings.
|
||||
"global_lsp_settings": {
|
||||
// Whether to show the LSP servers button in the status bar.
|
||||
"button": true
|
||||
"button": true,
|
||||
},
|
||||
// Jupyter settings
|
||||
"jupyter": {
|
||||
"enabled": true,
|
||||
"kernel_selections": {}
|
||||
"kernel_selections": {},
|
||||
// Specify the language name as the key and the kernel name as the value.
|
||||
// "kernel_selections": {
|
||||
// "python": "conda-base"
|
||||
@@ -2109,7 +2128,7 @@
|
||||
"max_columns": 128,
|
||||
// Maximum number of lines to keep in REPL's scrollback buffer.
|
||||
// Clamped with [4, 256] range.
|
||||
"max_lines": 32
|
||||
"max_lines": 32,
|
||||
},
|
||||
// Vim settings
|
||||
"vim": {
|
||||
@@ -2123,7 +2142,7 @@
|
||||
// Specify the mode as the key and the shape as the value.
|
||||
// The mode can be one of the following: "normal", "replace", "insert", "visual".
|
||||
// The shape can be one of the following: "block", "bar", "underline", "hollow".
|
||||
"cursor_shape": {}
|
||||
"cursor_shape": {},
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
@@ -2156,9 +2175,9 @@
|
||||
"windows": {
|
||||
"languages": {
|
||||
"PHP": {
|
||||
"language_servers": ["intelephense", "!phpactor", "!phptools", "..."]
|
||||
}
|
||||
}
|
||||
"language_servers": ["intelephense", "!phpactor", "!phptools", "..."],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Whether to show full labels in line indicator or short ones
|
||||
//
|
||||
@@ -2217,7 +2236,7 @@
|
||||
"dock": "bottom",
|
||||
"log_dap_communications": true,
|
||||
"format_dap_log_messages": true,
|
||||
"button": true
|
||||
"button": true,
|
||||
},
|
||||
// Configures any number of settings profiles that are temporarily applied on
|
||||
// top of your existing user settings when selected from
|
||||
@@ -2244,5 +2263,5 @@
|
||||
// Useful for filtering out noisy logs or enabling more verbose logging.
|
||||
//
|
||||
// Example: {"log": {"client": "warn"}}
|
||||
"log": {}
|
||||
"log": {},
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"adapter": "Debugpy",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
},
|
||||
{
|
||||
"label": "Debug active JavaScript file",
|
||||
@@ -16,7 +16,7 @@
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"type": "pwa-node"
|
||||
"type": "pwa-node",
|
||||
},
|
||||
{
|
||||
"label": "JavaScript debug terminal",
|
||||
@@ -24,6 +24,6 @@
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"console": "integratedTerminal",
|
||||
"type": "pwa-node"
|
||||
}
|
||||
"type": "pwa-node",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
// For a full list of overridable settings, and general information on settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{
|
||||
"lsp": {}
|
||||
"lsp": {},
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
// Whether to show the task line in the output of the spawned task, defaults to `true`.
|
||||
"show_summary": true,
|
||||
// Whether to show the command line in the output of the spawned task, defaults to `true`.
|
||||
"show_command": true
|
||||
"show_command": true,
|
||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||
// "tags": []
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"theme": {
|
||||
"mode": "system",
|
||||
"light": "One Light",
|
||||
"dark": "One Dark"
|
||||
}
|
||||
"dark": "One Dark",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -71,33 +71,33 @@
|
||||
"editor.document_highlight.read_background": "#83a5981a",
|
||||
"editor.document_highlight.write_background": "#92847466",
|
||||
"terminal.background": "#282828ff",
|
||||
"terminal.foreground": "#fbf1c7ff",
|
||||
"terminal.foreground": "#ebdbb2ff",
|
||||
"terminal.bright_foreground": "#fbf1c7ff",
|
||||
"terminal.dim_foreground": "#282828ff",
|
||||
"terminal.dim_foreground": "#766b5dff",
|
||||
"terminal.ansi.black": "#282828ff",
|
||||
"terminal.ansi.bright_black": "#73675eff",
|
||||
"terminal.ansi.bright_black": "#928374ff",
|
||||
"terminal.ansi.dim_black": "#fbf1c7ff",
|
||||
"terminal.ansi.red": "#fb4a35ff",
|
||||
"terminal.ansi.bright_red": "#93201dff",
|
||||
"terminal.ansi.dim_red": "#ffaa95ff",
|
||||
"terminal.ansi.green": "#b7bb26ff",
|
||||
"terminal.ansi.bright_green": "#605c1bff",
|
||||
"terminal.ansi.dim_green": "#e0dc98ff",
|
||||
"terminal.ansi.yellow": "#f9bd2fff",
|
||||
"terminal.ansi.bright_yellow": "#91611bff",
|
||||
"terminal.ansi.dim_yellow": "#fedc9bff",
|
||||
"terminal.ansi.blue": "#83a598ff",
|
||||
"terminal.ansi.bright_blue": "#414f4aff",
|
||||
"terminal.ansi.dim_blue": "#c0d2cbff",
|
||||
"terminal.ansi.magenta": "#d3869bff",
|
||||
"terminal.ansi.bright_magenta": "#8e5868ff",
|
||||
"terminal.ansi.dim_magenta": "#ff9ebbff",
|
||||
"terminal.ansi.cyan": "#8ec07cff",
|
||||
"terminal.ansi.bright_cyan": "#45603eff",
|
||||
"terminal.ansi.dim_cyan": "#c7dfbdff",
|
||||
"terminal.ansi.white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"terminal.ansi.red": "#cc241dff",
|
||||
"terminal.ansi.bright_red": "#fb4934ff",
|
||||
"terminal.ansi.dim_red": "#8e1814ff",
|
||||
"terminal.ansi.green": "#98971aff",
|
||||
"terminal.ansi.bright_green": "#b8bb26ff",
|
||||
"terminal.ansi.dim_green": "#6a6912ff",
|
||||
"terminal.ansi.yellow": "#d79921ff",
|
||||
"terminal.ansi.bright_yellow": "#fabd2fff",
|
||||
"terminal.ansi.dim_yellow": "#966a17ff",
|
||||
"terminal.ansi.blue": "#458588ff",
|
||||
"terminal.ansi.bright_blue": "#83a598ff",
|
||||
"terminal.ansi.dim_blue": "#305d5fff",
|
||||
"terminal.ansi.magenta": "#b16286ff",
|
||||
"terminal.ansi.bright_magenta": "#d3869bff",
|
||||
"terminal.ansi.dim_magenta": "#7c455eff",
|
||||
"terminal.ansi.cyan": "#689d6aff",
|
||||
"terminal.ansi.bright_cyan": "#8ec07cff",
|
||||
"terminal.ansi.dim_cyan": "#496e4aff",
|
||||
"terminal.ansi.white": "#a89984ff",
|
||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||
"terminal.ansi.dim_white": "#766b5dff",
|
||||
"link_text.hover": "#83a598ff",
|
||||
"version_control.added": "#b7bb26ff",
|
||||
"version_control.modified": "#f9bd2fff",
|
||||
@@ -478,33 +478,33 @@
|
||||
"editor.document_highlight.read_background": "#83a5981a",
|
||||
"editor.document_highlight.write_background": "#92847466",
|
||||
"terminal.background": "#1d2021ff",
|
||||
"terminal.foreground": "#fbf1c7ff",
|
||||
"terminal.foreground": "#ebdbb2ff",
|
||||
"terminal.bright_foreground": "#fbf1c7ff",
|
||||
"terminal.dim_foreground": "#1d2021ff",
|
||||
"terminal.ansi.black": "#1d2021ff",
|
||||
"terminal.ansi.bright_black": "#73675eff",
|
||||
"terminal.dim_foreground": "#766b5dff",
|
||||
"terminal.ansi.black": "#282828ff",
|
||||
"terminal.ansi.bright_black": "#928374ff",
|
||||
"terminal.ansi.dim_black": "#fbf1c7ff",
|
||||
"terminal.ansi.red": "#fb4a35ff",
|
||||
"terminal.ansi.bright_red": "#93201dff",
|
||||
"terminal.ansi.dim_red": "#ffaa95ff",
|
||||
"terminal.ansi.green": "#b7bb26ff",
|
||||
"terminal.ansi.bright_green": "#605c1bff",
|
||||
"terminal.ansi.dim_green": "#e0dc98ff",
|
||||
"terminal.ansi.yellow": "#f9bd2fff",
|
||||
"terminal.ansi.bright_yellow": "#91611bff",
|
||||
"terminal.ansi.dim_yellow": "#fedc9bff",
|
||||
"terminal.ansi.blue": "#83a598ff",
|
||||
"terminal.ansi.bright_blue": "#414f4aff",
|
||||
"terminal.ansi.dim_blue": "#c0d2cbff",
|
||||
"terminal.ansi.magenta": "#d3869bff",
|
||||
"terminal.ansi.bright_magenta": "#8e5868ff",
|
||||
"terminal.ansi.dim_magenta": "#ff9ebbff",
|
||||
"terminal.ansi.cyan": "#8ec07cff",
|
||||
"terminal.ansi.bright_cyan": "#45603eff",
|
||||
"terminal.ansi.dim_cyan": "#c7dfbdff",
|
||||
"terminal.ansi.white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"terminal.ansi.red": "#cc241dff",
|
||||
"terminal.ansi.bright_red": "#fb4934ff",
|
||||
"terminal.ansi.dim_red": "#8e1814ff",
|
||||
"terminal.ansi.green": "#98971aff",
|
||||
"terminal.ansi.bright_green": "#b8bb26ff",
|
||||
"terminal.ansi.dim_green": "#6a6912ff",
|
||||
"terminal.ansi.yellow": "#d79921ff",
|
||||
"terminal.ansi.bright_yellow": "#fabd2fff",
|
||||
"terminal.ansi.dim_yellow": "#966a17ff",
|
||||
"terminal.ansi.blue": "#458588ff",
|
||||
"terminal.ansi.bright_blue": "#83a598ff",
|
||||
"terminal.ansi.dim_blue": "#305d5fff",
|
||||
"terminal.ansi.magenta": "#b16286ff",
|
||||
"terminal.ansi.bright_magenta": "#d3869bff",
|
||||
"terminal.ansi.dim_magenta": "#7c455eff",
|
||||
"terminal.ansi.cyan": "#689d6aff",
|
||||
"terminal.ansi.bright_cyan": "#8ec07cff",
|
||||
"terminal.ansi.dim_cyan": "#496e4aff",
|
||||
"terminal.ansi.white": "#a89984ff",
|
||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||
"terminal.ansi.dim_white": "#766b5dff",
|
||||
"link_text.hover": "#83a598ff",
|
||||
"version_control.added": "#b7bb26ff",
|
||||
"version_control.modified": "#f9bd2fff",
|
||||
@@ -885,33 +885,33 @@
|
||||
"editor.document_highlight.read_background": "#83a5981a",
|
||||
"editor.document_highlight.write_background": "#92847466",
|
||||
"terminal.background": "#32302fff",
|
||||
"terminal.foreground": "#fbf1c7ff",
|
||||
"terminal.foreground": "#ebdbb2ff",
|
||||
"terminal.bright_foreground": "#fbf1c7ff",
|
||||
"terminal.dim_foreground": "#32302fff",
|
||||
"terminal.ansi.black": "#32302fff",
|
||||
"terminal.ansi.bright_black": "#73675eff",
|
||||
"terminal.dim_foreground": "#766b5dff",
|
||||
"terminal.ansi.black": "#282828ff",
|
||||
"terminal.ansi.bright_black": "#928374ff",
|
||||
"terminal.ansi.dim_black": "#fbf1c7ff",
|
||||
"terminal.ansi.red": "#fb4a35ff",
|
||||
"terminal.ansi.bright_red": "#93201dff",
|
||||
"terminal.ansi.dim_red": "#ffaa95ff",
|
||||
"terminal.ansi.green": "#b7bb26ff",
|
||||
"terminal.ansi.bright_green": "#605c1bff",
|
||||
"terminal.ansi.dim_green": "#e0dc98ff",
|
||||
"terminal.ansi.yellow": "#f9bd2fff",
|
||||
"terminal.ansi.bright_yellow": "#91611bff",
|
||||
"terminal.ansi.dim_yellow": "#fedc9bff",
|
||||
"terminal.ansi.blue": "#83a598ff",
|
||||
"terminal.ansi.bright_blue": "#414f4aff",
|
||||
"terminal.ansi.dim_blue": "#c0d2cbff",
|
||||
"terminal.ansi.magenta": "#d3869bff",
|
||||
"terminal.ansi.bright_magenta": "#8e5868ff",
|
||||
"terminal.ansi.dim_magenta": "#ff9ebbff",
|
||||
"terminal.ansi.cyan": "#8ec07cff",
|
||||
"terminal.ansi.bright_cyan": "#45603eff",
|
||||
"terminal.ansi.dim_cyan": "#c7dfbdff",
|
||||
"terminal.ansi.white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"terminal.ansi.red": "#cc241dff",
|
||||
"terminal.ansi.bright_red": "#fb4934ff",
|
||||
"terminal.ansi.dim_red": "#8e1814ff",
|
||||
"terminal.ansi.green": "#98971aff",
|
||||
"terminal.ansi.bright_green": "#b8bb26ff",
|
||||
"terminal.ansi.dim_green": "#6a6912ff",
|
||||
"terminal.ansi.yellow": "#d79921ff",
|
||||
"terminal.ansi.bright_yellow": "#fabd2fff",
|
||||
"terminal.ansi.dim_yellow": "#966a17ff",
|
||||
"terminal.ansi.blue": "#458588ff",
|
||||
"terminal.ansi.bright_blue": "#83a598ff",
|
||||
"terminal.ansi.dim_blue": "#305d5fff",
|
||||
"terminal.ansi.magenta": "#b16286ff",
|
||||
"terminal.ansi.bright_magenta": "#d3869bff",
|
||||
"terminal.ansi.dim_magenta": "#7c455eff",
|
||||
"terminal.ansi.cyan": "#689d6aff",
|
||||
"terminal.ansi.bright_cyan": "#8ec07cff",
|
||||
"terminal.ansi.dim_cyan": "#496e4aff",
|
||||
"terminal.ansi.white": "#a89984ff",
|
||||
"terminal.ansi.bright_white": "#fbf1c7ff",
|
||||
"terminal.ansi.dim_white": "#766b5dff",
|
||||
"link_text.hover": "#83a598ff",
|
||||
"version_control.added": "#b7bb26ff",
|
||||
"version_control.modified": "#f9bd2fff",
|
||||
@@ -1295,30 +1295,30 @@
|
||||
"terminal.foreground": "#282828ff",
|
||||
"terminal.bright_foreground": "#282828ff",
|
||||
"terminal.dim_foreground": "#fbf1c7ff",
|
||||
"terminal.ansi.black": "#282828ff",
|
||||
"terminal.ansi.bright_black": "#0b6678ff",
|
||||
"terminal.ansi.dim_black": "#5f5650ff",
|
||||
"terminal.ansi.red": "#9d0308ff",
|
||||
"terminal.ansi.bright_red": "#db8b7aff",
|
||||
"terminal.ansi.dim_red": "#4e1207ff",
|
||||
"terminal.ansi.green": "#797410ff",
|
||||
"terminal.ansi.bright_green": "#bfb787ff",
|
||||
"terminal.ansi.dim_green": "#3e3a11ff",
|
||||
"terminal.ansi.yellow": "#b57615ff",
|
||||
"terminal.ansi.bright_yellow": "#e2b88bff",
|
||||
"terminal.ansi.dim_yellow": "#5c3a12ff",
|
||||
"terminal.ansi.blue": "#0b6678ff",
|
||||
"terminal.ansi.bright_blue": "#8fb0baff",
|
||||
"terminal.ansi.dim_blue": "#14333bff",
|
||||
"terminal.ansi.magenta": "#8f3e71ff",
|
||||
"terminal.ansi.bright_magenta": "#c76da0ff",
|
||||
"terminal.ansi.dim_magenta": "#5c2848ff",
|
||||
"terminal.ansi.cyan": "#437b59ff",
|
||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
||||
"terminal.ansi.white": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"terminal.ansi.black": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_black": "#928374ff",
|
||||
"terminal.ansi.dim_black": "#7c6f64ff",
|
||||
"terminal.ansi.red": "#cc241dff",
|
||||
"terminal.ansi.bright_red": "#9d0006ff",
|
||||
"terminal.ansi.dim_red": "#c31c16ff",
|
||||
"terminal.ansi.green": "#98971aff",
|
||||
"terminal.ansi.bright_green": "#79740eff",
|
||||
"terminal.ansi.dim_green": "#929015ff",
|
||||
"terminal.ansi.yellow": "#d79921ff",
|
||||
"terminal.ansi.bright_yellow": "#b57614ff",
|
||||
"terminal.ansi.dim_yellow": "#cf8e1aff",
|
||||
"terminal.ansi.blue": "#458588ff",
|
||||
"terminal.ansi.bright_blue": "#076678ff",
|
||||
"terminal.ansi.dim_blue": "#356f77ff",
|
||||
"terminal.ansi.magenta": "#b16286ff",
|
||||
"terminal.ansi.bright_magenta": "#8f3f71ff",
|
||||
"terminal.ansi.dim_magenta": "#a85580ff",
|
||||
"terminal.ansi.cyan": "#689d6aff",
|
||||
"terminal.ansi.bright_cyan": "#427b58ff",
|
||||
"terminal.ansi.dim_cyan": "#5f9166ff",
|
||||
"terminal.ansi.white": "#7c6f64ff",
|
||||
"terminal.ansi.bright_white": "#282828ff",
|
||||
"terminal.ansi.dim_white": "#282828ff",
|
||||
"link_text.hover": "#0b6678ff",
|
||||
"version_control.added": "#797410ff",
|
||||
"version_control.modified": "#b57615ff",
|
||||
@@ -1702,30 +1702,30 @@
|
||||
"terminal.foreground": "#282828ff",
|
||||
"terminal.bright_foreground": "#282828ff",
|
||||
"terminal.dim_foreground": "#f9f5d7ff",
|
||||
"terminal.ansi.black": "#282828ff",
|
||||
"terminal.ansi.bright_black": "#73675eff",
|
||||
"terminal.ansi.dim_black": "#f9f5d7ff",
|
||||
"terminal.ansi.red": "#9d0308ff",
|
||||
"terminal.ansi.bright_red": "#db8b7aff",
|
||||
"terminal.ansi.dim_red": "#4e1207ff",
|
||||
"terminal.ansi.green": "#797410ff",
|
||||
"terminal.ansi.bright_green": "#bfb787ff",
|
||||
"terminal.ansi.dim_green": "#3e3a11ff",
|
||||
"terminal.ansi.yellow": "#b57615ff",
|
||||
"terminal.ansi.bright_yellow": "#e2b88bff",
|
||||
"terminal.ansi.dim_yellow": "#5c3a12ff",
|
||||
"terminal.ansi.blue": "#0b6678ff",
|
||||
"terminal.ansi.bright_blue": "#8fb0baff",
|
||||
"terminal.ansi.dim_blue": "#14333bff",
|
||||
"terminal.ansi.magenta": "#8f3e71ff",
|
||||
"terminal.ansi.bright_magenta": "#c76da0ff",
|
||||
"terminal.ansi.dim_magenta": "#5c2848ff",
|
||||
"terminal.ansi.cyan": "#437b59ff",
|
||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
||||
"terminal.ansi.white": "#f9f5d7ff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"terminal.ansi.black": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_black": "#928374ff",
|
||||
"terminal.ansi.dim_black": "#7c6f64ff",
|
||||
"terminal.ansi.red": "#cc241dff",
|
||||
"terminal.ansi.bright_red": "#9d0006ff",
|
||||
"terminal.ansi.dim_red": "#c31c16ff",
|
||||
"terminal.ansi.green": "#98971aff",
|
||||
"terminal.ansi.bright_green": "#79740eff",
|
||||
"terminal.ansi.dim_green": "#929015ff",
|
||||
"terminal.ansi.yellow": "#d79921ff",
|
||||
"terminal.ansi.bright_yellow": "#b57614ff",
|
||||
"terminal.ansi.dim_yellow": "#cf8e1aff",
|
||||
"terminal.ansi.blue": "#458588ff",
|
||||
"terminal.ansi.bright_blue": "#076678ff",
|
||||
"terminal.ansi.dim_blue": "#356f77ff",
|
||||
"terminal.ansi.magenta": "#b16286ff",
|
||||
"terminal.ansi.bright_magenta": "#8f3f71ff",
|
||||
"terminal.ansi.dim_magenta": "#a85580ff",
|
||||
"terminal.ansi.cyan": "#689d6aff",
|
||||
"terminal.ansi.bright_cyan": "#427b58ff",
|
||||
"terminal.ansi.dim_cyan": "#5f9166ff",
|
||||
"terminal.ansi.white": "#7c6f64ff",
|
||||
"terminal.ansi.bright_white": "#282828ff",
|
||||
"terminal.ansi.dim_white": "#282828ff",
|
||||
"link_text.hover": "#0b6678ff",
|
||||
"version_control.added": "#797410ff",
|
||||
"version_control.modified": "#b57615ff",
|
||||
@@ -2109,30 +2109,30 @@
|
||||
"terminal.foreground": "#282828ff",
|
||||
"terminal.bright_foreground": "#282828ff",
|
||||
"terminal.dim_foreground": "#f2e5bcff",
|
||||
"terminal.ansi.black": "#282828ff",
|
||||
"terminal.ansi.bright_black": "#73675eff",
|
||||
"terminal.ansi.dim_black": "#f2e5bcff",
|
||||
"terminal.ansi.red": "#9d0308ff",
|
||||
"terminal.ansi.bright_red": "#db8b7aff",
|
||||
"terminal.ansi.dim_red": "#4e1207ff",
|
||||
"terminal.ansi.green": "#797410ff",
|
||||
"terminal.ansi.bright_green": "#bfb787ff",
|
||||
"terminal.ansi.dim_green": "#3e3a11ff",
|
||||
"terminal.ansi.yellow": "#b57615ff",
|
||||
"terminal.ansi.bright_yellow": "#e2b88bff",
|
||||
"terminal.ansi.dim_yellow": "#5c3a12ff",
|
||||
"terminal.ansi.blue": "#0b6678ff",
|
||||
"terminal.ansi.bright_blue": "#8fb0baff",
|
||||
"terminal.ansi.dim_blue": "#14333bff",
|
||||
"terminal.ansi.magenta": "#8f3e71ff",
|
||||
"terminal.ansi.bright_magenta": "#c76da0ff",
|
||||
"terminal.ansi.dim_magenta": "#5c2848ff",
|
||||
"terminal.ansi.cyan": "#437b59ff",
|
||||
"terminal.ansi.bright_cyan": "#9fbca8ff",
|
||||
"terminal.ansi.dim_cyan": "#253e2eff",
|
||||
"terminal.ansi.white": "#f2e5bcff",
|
||||
"terminal.ansi.bright_white": "#ffffffff",
|
||||
"terminal.ansi.dim_white": "#b0a189ff",
|
||||
"terminal.ansi.black": "#fbf1c7ff",
|
||||
"terminal.ansi.bright_black": "#928374ff",
|
||||
"terminal.ansi.dim_black": "#7c6f64ff",
|
||||
"terminal.ansi.red": "#cc241dff",
|
||||
"terminal.ansi.bright_red": "#9d0006ff",
|
||||
"terminal.ansi.dim_red": "#c31c16ff",
|
||||
"terminal.ansi.green": "#98971aff",
|
||||
"terminal.ansi.bright_green": "#79740eff",
|
||||
"terminal.ansi.dim_green": "#929015ff",
|
||||
"terminal.ansi.yellow": "#d79921ff",
|
||||
"terminal.ansi.bright_yellow": "#b57614ff",
|
||||
"terminal.ansi.dim_yellow": "#cf8e1aff",
|
||||
"terminal.ansi.blue": "#458588ff",
|
||||
"terminal.ansi.bright_blue": "#076678ff",
|
||||
"terminal.ansi.dim_blue": "#356f77ff",
|
||||
"terminal.ansi.magenta": "#b16286ff",
|
||||
"terminal.ansi.bright_magenta": "#8f3f71ff",
|
||||
"terminal.ansi.dim_magenta": "#a85580ff",
|
||||
"terminal.ansi.cyan": "#689d6aff",
|
||||
"terminal.ansi.bright_cyan": "#427b58ff",
|
||||
"terminal.ansi.dim_cyan": "#5f9166ff",
|
||||
"terminal.ansi.white": "#7c6f64ff",
|
||||
"terminal.ansi.bright_white": "#282828ff",
|
||||
"terminal.ansi.dim_white": "#282828ff",
|
||||
"link_text.hover": "#0b6678ff",
|
||||
"version_control.added": "#797410ff",
|
||||
"version_control.modified": "#b57615ff",
|
||||
|
||||
@@ -14,6 +14,7 @@ disallowed-methods = [
|
||||
{ path = "std::process::Command::stderr", reason = "`smol::process::Command::from()` does not preserve stdio configuration", replacement = "smol::process::Command::stderr" },
|
||||
{ path = "serde_json::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892. Use `serde_json::from_slice` instead." },
|
||||
{ path = "serde_json_lenient::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892, Use `serde_json_lenient::from_slice` instead." },
|
||||
{ path = "cocoa::foundation::NSString::alloc", reason = "NSString must be autoreleased to avoid memory leaks. Use `ns_string()` helper instead." },
|
||||
]
|
||||
disallowed-types = [
|
||||
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },
|
||||
|
||||
@@ -1372,7 +1372,7 @@ impl AcpThread {
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let id = update.tool_call_id.clone();
|
||||
|
||||
let agent = self.connection().telemetry_id();
|
||||
let agent_telemetry_id = self.connection().telemetry_id();
|
||||
let session = self.session_id();
|
||||
if let ToolCallStatus::Completed | ToolCallStatus::Failed = status {
|
||||
let status = if matches!(status, ToolCallStatus::Completed) {
|
||||
@@ -1380,7 +1380,12 @@ impl AcpThread {
|
||||
} else {
|
||||
"failed"
|
||||
};
|
||||
telemetry::event!("Agent Tool Call Completed", agent, session, status);
|
||||
telemetry::event!(
|
||||
"Agent Tool Call Completed",
|
||||
agent_telemetry_id,
|
||||
session,
|
||||
status
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ix) = self.index_for_tool_call(&id) {
|
||||
@@ -2929,7 +2934,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(err.code, acp::ErrorCode::RESOURCE_NOT_FOUND.code);
|
||||
assert_eq!(err.code, acp::ErrorCode::ResourceNotFound);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -3556,8 +3561,8 @@ mod tests {
|
||||
}
|
||||
|
||||
impl AgentConnection for FakeAgentConnection {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"fake"
|
||||
fn telemetry_id(&self) -> SharedString {
|
||||
"fake".into()
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
|
||||
@@ -20,7 +20,7 @@ impl UserMessageId {
|
||||
}
|
||||
|
||||
pub trait AgentConnection {
|
||||
fn telemetry_id(&self) -> &'static str;
|
||||
fn telemetry_id(&self) -> SharedString;
|
||||
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
@@ -322,8 +322,8 @@ mod test_support {
|
||||
}
|
||||
|
||||
impl AgentConnection for StubAgentConnection {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"stub"
|
||||
fn telemetry_id(&self) -> SharedString {
|
||||
"stub".into()
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
|
||||
@@ -166,7 +166,7 @@ impl Diff {
|
||||
}
|
||||
|
||||
pub fn has_revealed_range(&self, cx: &App) -> bool {
|
||||
self.multibuffer().read(cx).excerpt_paths().next().is_some()
|
||||
self.multibuffer().read(cx).paths().next().is_some()
|
||||
}
|
||||
|
||||
pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool {
|
||||
|
||||
@@ -75,15 +75,9 @@ impl Terminal {
|
||||
|
||||
let exit_status = exit_status.map(portable_pty::ExitStatus::from);
|
||||
|
||||
let mut status = acp::TerminalExitStatus::new();
|
||||
|
||||
if let Some(exit_status) = exit_status.as_ref() {
|
||||
status = status.exit_code(exit_status.exit_code());
|
||||
if let Some(signal) = exit_status.signal() {
|
||||
status = status.signal(signal);
|
||||
}
|
||||
}
|
||||
status
|
||||
acp::TerminalExitStatus::new()
|
||||
.exit_code(exit_status.as_ref().map(|e| e.exit_code()))
|
||||
.signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned)))
|
||||
})
|
||||
.shared(),
|
||||
}
|
||||
@@ -105,19 +99,17 @@ impl Terminal {
|
||||
|
||||
pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
|
||||
if let Some(output) = self.output.as_ref() {
|
||||
let mut exit_status = acp::TerminalExitStatus::new();
|
||||
if let Some(status) = output.exit_status.map(portable_pty::ExitStatus::from) {
|
||||
exit_status = exit_status.exit_code(status.exit_code());
|
||||
if let Some(signal) = status.signal() {
|
||||
exit_status = exit_status.signal(signal);
|
||||
}
|
||||
}
|
||||
let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
|
||||
|
||||
acp::TerminalOutputResponse::new(
|
||||
output.content.clone(),
|
||||
output.original_content_len > output.content.len(),
|
||||
)
|
||||
.exit_status(exit_status)
|
||||
.exit_status(
|
||||
acp::TerminalExitStatus::new()
|
||||
.exit_code(exit_status.as_ref().map(|e| e.exit_code()))
|
||||
.signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned))),
|
||||
)
|
||||
} else {
|
||||
let (current_content, original_len) = self.truncated_output(cx);
|
||||
let truncated = current_content.len() < original_len;
|
||||
@@ -195,8 +187,10 @@ pub async fn create_terminal_entity(
|
||||
Default::default()
|
||||
};
|
||||
|
||||
// Disables paging for `git` and hopefully other commands
|
||||
// Disable pagers so agent/terminal commands don't hang behind interactive UIs
|
||||
env.insert("PAGER".into(), "".into());
|
||||
// Override user core.pager (e.g. delta) which Git prefers over PAGER
|
||||
env.insert("GIT_PAGER".into(), "cat".into());
|
||||
env.extend(env_vars);
|
||||
|
||||
// Use remote shell or default system shell, as appropriate
|
||||
|
||||
@@ -371,13 +371,13 @@ impl AcpTools {
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
code_block_overflow_x_scroll: true,
|
||||
code_block: StyleRefinement {
|
||||
text: Some(TextStyleRefinement {
|
||||
text: TextStyleRefinement {
|
||||
font_family: Some(
|
||||
theme_settings.buffer_font.family.clone(),
|
||||
),
|
||||
font_size: Some((base_size * 0.8).into()),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
|
||||
@@ -777,7 +777,7 @@ impl ActionLog {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ActionLogTelemetry {
|
||||
pub agent_telemetry_id: &'static str,
|
||||
pub agent_telemetry_id: SharedString,
|
||||
pub session_id: Arc<str>,
|
||||
}
|
||||
|
||||
|
||||
@@ -947,8 +947,8 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
||||
}
|
||||
|
||||
impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"zed"
|
||||
fn telemetry_id(&self) -> SharedString {
|
||||
"zed".into()
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
|
||||
@@ -1337,12 +1337,13 @@ impl EvalAssertion {
|
||||
}
|
||||
|
||||
fn run_eval(eval: EvalInput) -> eval_utils::EvalOutput<EditEvalMetadata> {
|
||||
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
|
||||
let dispatcher = gpui::TestDispatcher::new(rand::random());
|
||||
let mut cx = TestAppContext::build(dispatcher, None);
|
||||
let result = cx.executor().block_test(async {
|
||||
let test = EditAgentTest::new(&mut cx).await;
|
||||
test.eval(eval, &mut cx).await
|
||||
});
|
||||
cx.quit();
|
||||
match result {
|
||||
Ok(output) => eval_utils::EvalOutput {
|
||||
data: output.to_string(),
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
- We're starting from a completely blank project
|
||||
- Like Aider/Claude Code you take the user's initial prompt and then call the LLM and perform tool calls in a loop until the ultimate goal is achieved.
|
||||
- Unlike Aider or Claude code, it's not intended to be interactive. Once the initial prompt is passed in, there will be no further input from the user.
|
||||
- The system you will build must reach the stated goal just by performing too calls and calling the LLM
|
||||
- The system you will build must reach the stated goal just by performing tool calls and calling the LLM
|
||||
- I want you to build this in python. Use the anthropic python sdk and the model context protocol sdk. Use a virtual env and pip to install dependencies
|
||||
- Follow the anthropic guidance on tool calls: https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview
|
||||
- Use this Anthropic model: `claude-3-7-sonnet-20250219`
|
||||
- Use this Anthropic API Key: `sk-ant-api03-qweeryiofdjsncmxquywefidopsugus`
|
||||
- One of the most important pieces to this is having good too calls. We will be using the tools provided by the Claude MCP server. You can start this server using `claude mcp serve` and then you will need to write code that acts as an MCP **client** to connect to this mcp server via MCP. Likely you want to start this using a subprocess. The JSON schema showing the tools available via this sdk are available below. Via this MCP server you have access to all the tools that zode needs: Bash, GlobTool, GrepTool, LS, View, Edit, Replace, WebFetchTool
|
||||
- One of the most important pieces to this is having good tool calls. We will be using the tools provided by the Claude MCP server. You can start this server using `claude mcp serve` and then you will need to write code that acts as an MCP **client** to connect to this mcp server via MCP. Likely you want to start this using a subprocess. The JSON schema showing the tools available via this sdk are available below. Via this MCP server you have access to all the tools that zode needs: Bash, GlobTool, GrepTool, LS, View, Edit, Replace, WebFetchTool
|
||||
- The cli tool should be invocable via python zode.py file.md where file.md is any possible file that contains the users prompt. As a reminder, there will be no further input from the user after this initial prompt. Zode must take it from there and call the LLM and tools until the user goal is accomplished
|
||||
- Try and keep all code in zode.py and make heavy use of the asks I mentioned
|
||||
- Once you’ve implemented this, you must run python zode.py eval/instructions.md to see how well our new agent tool does!
|
||||
|
||||
@@ -21,10 +21,6 @@ impl NativeAgentServer {
|
||||
}
|
||||
|
||||
impl AgentServer for NativeAgentServer {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"zed"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Zed Agent".into()
|
||||
}
|
||||
|
||||
@@ -2094,7 +2094,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
"1",
|
||||
acp::ToolCallUpdateFields::new()
|
||||
.status(acp::ToolCallStatus::Completed)
|
||||
.raw_output("Finished thinking.".into())
|
||||
.raw_output("Finished thinking.")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -766,20 +766,22 @@ impl Thread {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
let mut fields = acp::ToolCallUpdateFields::new().status(tool_result.as_ref().map_or(
|
||||
acp::ToolCallStatus::Failed,
|
||||
|result| {
|
||||
if result.is_error {
|
||||
acp::ToolCallStatus::Failed
|
||||
} else {
|
||||
acp::ToolCallStatus::Completed
|
||||
}
|
||||
},
|
||||
));
|
||||
if let Some(output) = output {
|
||||
fields = fields.raw_output(output);
|
||||
}
|
||||
stream.update_tool_call_fields(&tool_use.id, fields);
|
||||
stream.update_tool_call_fields(
|
||||
&tool_use.id,
|
||||
acp::ToolCallUpdateFields::new()
|
||||
.status(
|
||||
tool_result
|
||||
.as_ref()
|
||||
.map_or(acp::ToolCallStatus::Failed, |result| {
|
||||
if result.is_error {
|
||||
acp::ToolCallStatus::Failed
|
||||
} else {
|
||||
acp::ToolCallStatus::Completed
|
||||
}
|
||||
}),
|
||||
)
|
||||
.raw_output(output),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn from_db(
|
||||
@@ -1259,15 +1261,16 @@ impl Thread {
|
||||
while let Some(tool_result) = tool_results.next().await {
|
||||
log::debug!("Tool finished {:?}", tool_result);
|
||||
|
||||
let mut fields = acp::ToolCallUpdateFields::new().status(if tool_result.is_error {
|
||||
acp::ToolCallStatus::Failed
|
||||
} else {
|
||||
acp::ToolCallStatus::Completed
|
||||
});
|
||||
if let Some(output) = &tool_result.output {
|
||||
fields = fields.raw_output(output.clone());
|
||||
}
|
||||
event_stream.update_tool_call_fields(&tool_result.tool_use_id, fields);
|
||||
event_stream.update_tool_call_fields(
|
||||
&tool_result.tool_use_id,
|
||||
acp::ToolCallUpdateFields::new()
|
||||
.status(if tool_result.is_error {
|
||||
acp::ToolCallStatus::Failed
|
||||
} else {
|
||||
acp::ToolCallStatus::Completed
|
||||
})
|
||||
.raw_output(tool_result.output.clone()),
|
||||
);
|
||||
this.update(cx, |this, _cx| {
|
||||
this.pending_message()
|
||||
.tool_results
|
||||
@@ -1545,7 +1548,7 @@ impl Thread {
|
||||
event_stream.update_tool_call_fields(
|
||||
&tool_use.id,
|
||||
acp::ToolCallUpdateFields::new()
|
||||
.title(title)
|
||||
.title(title.as_str())
|
||||
.kind(kind)
|
||||
.raw_input(tool_use.input.clone()),
|
||||
);
|
||||
@@ -2461,7 +2464,7 @@ impl ToolCallEventStream {
|
||||
ToolCallAuthorization {
|
||||
tool_call: acp::ToolCallUpdate::new(
|
||||
self.tool_use_id.to_string(),
|
||||
acp::ToolCallUpdateFields::new().title(title),
|
||||
acp::ToolCallUpdateFields::new().title(title.into()),
|
||||
),
|
||||
options: vec![
|
||||
acp::PermissionOption::new(
|
||||
|
||||
@@ -4,6 +4,7 @@ mod create_directory_tool;
|
||||
mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_file_tool;
|
||||
|
||||
mod fetch_tool;
|
||||
mod find_path_tool;
|
||||
mod grep_tool;
|
||||
@@ -12,6 +13,7 @@ mod move_path_tool;
|
||||
mod now_tool;
|
||||
mod open_tool;
|
||||
mod read_file_tool;
|
||||
|
||||
mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
mod web_search_tool;
|
||||
@@ -25,6 +27,7 @@ pub use create_directory_tool::*;
|
||||
pub use delete_path_tool::*;
|
||||
pub use diagnostics_tool::*;
|
||||
pub use edit_file_tool::*;
|
||||
|
||||
pub use fetch_tool::*;
|
||||
pub use find_path_tool::*;
|
||||
pub use grep_tool::*;
|
||||
@@ -33,6 +36,7 @@ pub use move_path_tool::*;
|
||||
pub use now_tool::*;
|
||||
pub use open_tool::*;
|
||||
pub use read_file_tool::*;
|
||||
|
||||
pub use terminal_tool::*;
|
||||
pub use thinking_tool::*;
|
||||
pub use web_search_tool::*;
|
||||
|
||||
@@ -384,11 +384,7 @@ impl AgentTool for EditFileTool {
|
||||
range.start.to_point(&buffer.snapshot()).row
|
||||
}).ok();
|
||||
if let Some(abs_path) = abs_path.clone() {
|
||||
let mut location = ToolCallLocation::new(abs_path);
|
||||
if let Some(line) = line {
|
||||
location = location.line(line);
|
||||
}
|
||||
event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![location]));
|
||||
event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![ToolCallLocation::new(abs_path).line(line)]));
|
||||
}
|
||||
emitted_location = true;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ impl AgentTool for FindPathTool {
|
||||
)),
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -322,7 +322,6 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use gpui::{TestAppContext, UpdateGlobal};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
@@ -564,7 +563,7 @@ mod tests {
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
project.update(cx, |project, _cx| {
|
||||
project.languages().add(rust_lang().into())
|
||||
project.languages().add(language::rust_lang())
|
||||
});
|
||||
|
||||
project
|
||||
@@ -793,22 +792,6 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_outline_query(include_str!("../../../languages/src/rust/outline.scm"))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_security_boundaries(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -152,12 +152,11 @@ impl AgentTool for ReadFileTool {
|
||||
}
|
||||
|
||||
let file_path = input.path.clone();
|
||||
let mut location = acp::ToolCallLocation::new(&abs_path);
|
||||
if let Some(line) = input.start_line {
|
||||
location = location.line(line.saturating_sub(1));
|
||||
}
|
||||
|
||||
event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![location]));
|
||||
event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![
|
||||
acp::ToolCallLocation::new(&abs_path)
|
||||
.line(input.start_line.map(|line| line.saturating_sub(1))),
|
||||
]));
|
||||
|
||||
if image_store::is_image_file(&self.project, &project_path, cx) {
|
||||
return cx.spawn(async move |cx| {
|
||||
@@ -302,7 +301,6 @@ mod test {
|
||||
use super::*;
|
||||
use crate::{ContextServerRegistry, Templates, Thread};
|
||||
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::ProjectContext;
|
||||
@@ -406,7 +404,7 @@ mod test {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(Arc::new(rust_lang()));
|
||||
language_registry.add(language::rust_lang());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
@@ -596,49 +594,6 @@ mod test {
|
||||
});
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_outline_query(
|
||||
r#"
|
||||
(line_comment) @annotation
|
||||
|
||||
(struct_item
|
||||
"struct" @context
|
||||
name: (_) @name) @item
|
||||
(enum_item
|
||||
"enum" @context
|
||||
name: (_) @name) @item
|
||||
(enum_variant
|
||||
name: (_) @name) @item
|
||||
(field_declaration
|
||||
name: (_) @name) @item
|
||||
(impl_item
|
||||
"impl" @context
|
||||
trait: (_)? @name
|
||||
"for"? @context
|
||||
type: (_) @name
|
||||
body: (_ "{" (_)* "}")) @item
|
||||
(function_item
|
||||
"fn" @context
|
||||
name: (_) @name) @item
|
||||
(mod_item
|
||||
"mod" @context
|
||||
name: (_) @name) @item
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_read_file_security(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -121,7 +121,7 @@ fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream)
|
||||
),
|
||||
))
|
||||
})
|
||||
.collect(),
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings as _;
|
||||
use task::ShellBuilder;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -21,7 +23,7 @@ use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntit
|
||||
|
||||
use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent};
|
||||
use terminal::TerminalBuilder;
|
||||
use terminal::terminal_settings::{AlternateScroll, CursorShape};
|
||||
use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Unsupported version")]
|
||||
@@ -29,7 +31,7 @@ pub struct UnsupportedVersion;
|
||||
|
||||
pub struct AcpConnection {
|
||||
server_name: SharedString,
|
||||
telemetry_id: &'static str,
|
||||
telemetry_id: SharedString,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
@@ -54,7 +56,6 @@ pub struct AcpSession {
|
||||
|
||||
pub async fn connect(
|
||||
server_name: SharedString,
|
||||
telemetry_id: &'static str,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
@@ -64,7 +65,6 @@ pub async fn connect(
|
||||
) -> Result<Rc<dyn AgentConnection>> {
|
||||
let conn = AcpConnection::stdio(
|
||||
server_name,
|
||||
telemetry_id,
|
||||
command.clone(),
|
||||
root_dir,
|
||||
default_mode,
|
||||
@@ -81,7 +81,6 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1
|
||||
impl AcpConnection {
|
||||
pub async fn stdio(
|
||||
server_name: SharedString,
|
||||
telemetry_id: &'static str,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
@@ -89,9 +88,11 @@ impl AcpConnection {
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(&command.path);
|
||||
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?;
|
||||
let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive();
|
||||
let mut child =
|
||||
builder.build_command(Some(command.path.display().to_string()), &command.args);
|
||||
child
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.envs(command.env.iter().flatten())
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
@@ -173,10 +174,6 @@ impl AcpConnection {
|
||||
});
|
||||
})?;
|
||||
|
||||
let mut client_info = acp::Implementation::new("zed", version);
|
||||
if let Some(release_channel) = release_channel {
|
||||
client_info = client_info.title(release_channel);
|
||||
}
|
||||
let response = connection
|
||||
.initialize(
|
||||
acp::InitializeRequest::new(acp::ProtocolVersion::V1)
|
||||
@@ -192,7 +189,10 @@ impl AcpConnection {
|
||||
("terminal-auth".into(), true.into()),
|
||||
])),
|
||||
)
|
||||
.client_info(client_info),
|
||||
.client_info(
|
||||
acp::Implementation::new("zed", version)
|
||||
.title(release_channel.map(ToOwned::to_owned)),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -200,6 +200,13 @@ impl AcpConnection {
|
||||
return Err(UnsupportedVersion.into());
|
||||
}
|
||||
|
||||
let telemetry_id = response
|
||||
.agent_info
|
||||
// Use the one the agent provides if we have one
|
||||
.map(|info| info.name.into())
|
||||
// Otherwise, just use the name
|
||||
.unwrap_or_else(|| server_name.clone());
|
||||
|
||||
Ok(Self {
|
||||
auth_methods: response.auth_methods,
|
||||
root_dir: root_dir.to_owned(),
|
||||
@@ -234,8 +241,8 @@ impl Drop for AcpConnection {
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
self.telemetry_id
|
||||
fn telemetry_id(&self) -> SharedString {
|
||||
self.telemetry_id.clone()
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
@@ -302,10 +309,10 @@ impl AgentConnection for AcpConnection {
|
||||
.new_session(acp::NewSessionRequest::new(cwd).mcp_servers(mcp_servers))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
if err.code == acp::ErrorCode::AuthRequired {
|
||||
let mut error = AuthRequired::new();
|
||||
|
||||
if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
|
||||
if err.message != acp::ErrorCode::AuthRequired.to_string() {
|
||||
error = error.with_description(err.message);
|
||||
}
|
||||
|
||||
@@ -467,11 +474,11 @@ impl AgentConnection for AcpConnection {
|
||||
match result {
|
||||
Ok(response) => Ok(response),
|
||||
Err(err) => {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
if err.code == acp::ErrorCode::AuthRequired {
|
||||
return Err(anyhow!(acp::Error::auth_required()));
|
||||
}
|
||||
|
||||
if err.code != ErrorCode::INTERNAL_ERROR.code {
|
||||
if err.code != ErrorCode::InternalError {
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
|
||||
@@ -838,13 +845,18 @@ impl acp::Client for ClientDelegate {
|
||||
if let Some(term_exit) = meta.get("terminal_exit") {
|
||||
if let Some(id_str) = term_exit.get("terminal_id").and_then(|v| v.as_str()) {
|
||||
let terminal_id = acp::TerminalId::new(id_str);
|
||||
let mut status = acp::TerminalExitStatus::new();
|
||||
if let Some(code) = term_exit.get("exit_code").and_then(|v| v.as_u64()) {
|
||||
status = status.exit_code(code as u32)
|
||||
}
|
||||
if let Some(signal) = term_exit.get("signal").and_then(|v| v.as_str()) {
|
||||
status = status.signal(signal);
|
||||
}
|
||||
let status = acp::TerminalExitStatus::new()
|
||||
.exit_code(
|
||||
term_exit
|
||||
.get("exit_code")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|i| i as u32),
|
||||
)
|
||||
.signal(
|
||||
term_exit
|
||||
.get("signal")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string())),
|
||||
);
|
||||
|
||||
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
|
||||
@@ -56,7 +56,6 @@ impl AgentServerDelegate {
|
||||
pub trait AgentServer: Send {
|
||||
fn logo(&self) -> ui::IconName;
|
||||
fn name(&self) -> SharedString;
|
||||
fn telemetry_id(&self) -> &'static str;
|
||||
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -22,10 +22,6 @@ pub struct AgentServerLoginCommand {
|
||||
}
|
||||
|
||||
impl AgentServer for ClaudeCode {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"claude-code"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Claude Code".into()
|
||||
}
|
||||
@@ -83,7 +79,6 @@ impl AgentServer for ClaudeCode {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let telemetry_id = self.telemetry_id();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
@@ -108,7 +103,6 @@ impl AgentServer for ClaudeCode {
|
||||
.await?;
|
||||
let connection = crate::acp::connect(
|
||||
name,
|
||||
telemetry_id,
|
||||
command,
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
|
||||
@@ -23,10 +23,6 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
impl AgentServer for Codex {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"codex"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Codex".into()
|
||||
}
|
||||
@@ -84,7 +80,6 @@ impl AgentServer for Codex {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let telemetry_id = self.telemetry_id();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
@@ -110,7 +105,6 @@ impl AgentServer for Codex {
|
||||
|
||||
let connection = crate::acp::connect(
|
||||
name,
|
||||
telemetry_id,
|
||||
command,
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{AgentServerDelegate, load_proxy_env};
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result};
|
||||
@@ -20,11 +20,7 @@ impl CustomAgentServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::AgentServer for CustomAgentServer {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"custom"
|
||||
}
|
||||
|
||||
impl AgentServer for CustomAgentServer {
|
||||
fn name(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
@@ -112,14 +108,12 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let telemetry_id = self.telemetry_id();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
@@ -139,7 +133,6 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
.await?;
|
||||
let connection = crate::acp::connect(
|
||||
name,
|
||||
telemetry_id,
|
||||
command,
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
|
||||
@@ -12,10 +12,6 @@ use project::agent_server_store::GEMINI_NAME;
|
||||
pub struct Gemini;
|
||||
|
||||
impl AgentServer for Gemini {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"gemini-cli"
|
||||
}
|
||||
|
||||
fn name(&self) -> SharedString {
|
||||
"Gemini CLI".into()
|
||||
}
|
||||
@@ -31,7 +27,6 @@ impl AgentServer for Gemini {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let telemetry_id = self.telemetry_id();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
@@ -66,7 +61,6 @@ impl AgentServer for Gemini {
|
||||
|
||||
let connection = crate::acp::connect(
|
||||
name,
|
||||
telemetry_id,
|
||||
command,
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
|
||||
@@ -9,7 +9,7 @@ use project::DisableAiSettings;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{
|
||||
DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
|
||||
DefaultAgentView, DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection,
|
||||
NotifyWhenAgentWaiting, RegisterSetting, Settings,
|
||||
};
|
||||
|
||||
@@ -24,10 +24,12 @@ pub struct AgentSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub agents_panel_dock: DockSide,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: Option<LanguageModelSelection>,
|
||||
pub inline_assistant_model: Option<LanguageModelSelection>,
|
||||
pub inline_assistant_use_streaming_tools: bool,
|
||||
pub commit_message_model: Option<LanguageModelSelection>,
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
@@ -151,10 +153,14 @@ impl Settings for AgentSettings {
|
||||
enabled: agent.enabled.unwrap(),
|
||||
button: agent.button.unwrap(),
|
||||
dock: agent.dock.unwrap(),
|
||||
agents_panel_dock: agent.agents_panel_dock.unwrap(),
|
||||
default_width: px(agent.default_width.unwrap()),
|
||||
default_height: px(agent.default_height.unwrap()),
|
||||
default_model: Some(agent.default_model.unwrap()),
|
||||
inline_assistant_model: agent.inline_assistant_model,
|
||||
inline_assistant_use_streaming_tools: agent
|
||||
.inline_assistant_use_streaming_tools
|
||||
.unwrap_or(true),
|
||||
commit_message_model: agent.commit_message_model,
|
||||
thread_summary_model: agent.thread_summary_model,
|
||||
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
|
||||
|
||||
@@ -13,7 +13,7 @@ path = "src/agent_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["gpui/test-support", "language/test-support", "reqwest_client"]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"]
|
||||
unit-eval = []
|
||||
|
||||
[dependencies]
|
||||
@@ -40,6 +40,7 @@ component.workspace = true
|
||||
context_server.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
eval_utils = { workspace = true, optional = true }
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
feature_flags.workspace = true
|
||||
@@ -71,6 +72,7 @@ postage.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
rand.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
rules_library.workspace = true
|
||||
@@ -84,7 +86,6 @@ smol.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
task.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
@@ -95,6 +96,7 @@ ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
@@ -119,7 +121,6 @@ language_model = { workspace = true, "features" = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
semver.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
tree-sitter-md.workspace = true
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
|
||||
pub struct EntryViewState {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
project: WeakEntity<Project>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
entries: Vec<Entry>,
|
||||
@@ -34,7 +34,7 @@ pub struct EntryViewState {
|
||||
impl EntryViewState {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
project: WeakEntity<Project>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
@@ -328,7 +328,7 @@ impl Entry {
|
||||
|
||||
fn create_terminal(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
project: WeakEntity<Project>,
|
||||
terminal: Entity<acp_thread::Terminal>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -336,9 +336,9 @@ fn create_terminal(
|
||||
cx.new(|cx| {
|
||||
let mut view = TerminalView::new(
|
||||
terminal.read(cx).inner().clone(),
|
||||
workspace.clone(),
|
||||
workspace,
|
||||
None,
|
||||
project.downgrade(),
|
||||
project,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -458,7 +458,7 @@ mod tests {
|
||||
let view_state = cx.new(|_cx| {
|
||||
EntryViewState::new(
|
||||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store,
|
||||
None,
|
||||
Default::default(),
|
||||
|
||||
@@ -21,8 +21,8 @@ use editor::{
|
||||
};
|
||||
use futures::{FutureExt as _, future::join_all};
|
||||
use gpui::{
|
||||
AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, KeyContext,
|
||||
SharedString, Subscription, Task, TextStyle, WeakEntity,
|
||||
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat,
|
||||
KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,
|
||||
};
|
||||
use language::{Buffer, Language, language_settings::InlayHintKind};
|
||||
use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};
|
||||
@@ -39,7 +39,6 @@ use zed_actions::agent::Chat;
|
||||
pub struct MessageEditor {
|
||||
mention_set: Entity<MentionSet>,
|
||||
editor: Entity<Editor>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
@@ -98,7 +97,7 @@ impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
project: WeakEntity<Project>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
@@ -124,6 +123,7 @@ impl MessageEditor {
|
||||
let mut editor = Editor::new(mode, buffer, None, window, cx);
|
||||
editor.set_placeholder_text(placeholder, window, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_show_completions_on_input(Some(true));
|
||||
editor.set_soft_wrap();
|
||||
editor.set_use_modal_editing(true);
|
||||
editor.set_context_menu_options(ContextMenuOptions {
|
||||
@@ -134,13 +134,8 @@ impl MessageEditor {
|
||||
editor.register_addon(MessageEditorAddon::new());
|
||||
editor
|
||||
});
|
||||
let mention_set = cx.new(|_cx| {
|
||||
MentionSet::new(
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
prompt_store.clone(),
|
||||
)
|
||||
});
|
||||
let mention_set =
|
||||
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
|
||||
let completion_provider = Rc::new(PromptCompletionProvider::new(
|
||||
cx.entity(),
|
||||
editor.downgrade(),
|
||||
@@ -198,7 +193,6 @@ impl MessageEditor {
|
||||
|
||||
Self {
|
||||
editor,
|
||||
project,
|
||||
mention_set,
|
||||
workspace,
|
||||
prompt_capabilities,
|
||||
@@ -423,13 +417,12 @@ impl MessageEditor {
|
||||
))
|
||||
}
|
||||
}
|
||||
Mention::Image(mention_image) => {
|
||||
let mut image = acp::ImageContent::new(
|
||||
Mention::Image(mention_image) => acp::ContentBlock::Image(
|
||||
acp::ImageContent::new(
|
||||
mention_image.data.clone(),
|
||||
mention_image.format.mime_type(),
|
||||
);
|
||||
|
||||
if let Some(uri) = match uri {
|
||||
)
|
||||
.uri(match uri {
|
||||
MentionUri::File { .. } => Some(uri.to_uri().to_string()),
|
||||
MentionUri::PastedImage => None,
|
||||
other => {
|
||||
@@ -439,11 +432,8 @@ impl MessageEditor {
|
||||
);
|
||||
None
|
||||
}
|
||||
} {
|
||||
image = image.uri(uri)
|
||||
};
|
||||
acp::ContentBlock::Image(image)
|
||||
}
|
||||
}),
|
||||
),
|
||||
Mention::Link => acp::ContentBlock::ResourceLink(
|
||||
acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()),
|
||||
),
|
||||
@@ -553,6 +543,145 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
.and_then(|entry| match entry {
|
||||
ClipboardEntry::String(text) => {
|
||||
text.metadata_json::<Vec<editor::ClipboardSelection>>()
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if has_file_context {
|
||||
if let Some((workspace, selections)) =
|
||||
self.workspace.upgrade().zip(editor_clipboard_selections)
|
||||
{
|
||||
let Some(first_selection) = selections.first() else {
|
||||
return;
|
||||
};
|
||||
if let Some(file_path) = &first_selection.file_path {
|
||||
// In case someone pastes selections from another window
|
||||
// with a different project, we don't want to insert the
|
||||
// crease (containing the absolute path) since the agent
|
||||
// cannot access files outside the project.
|
||||
let is_in_project = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some();
|
||||
if !is_in_project {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content =
|
||||
buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.prompt_capabilities.borrow().image
|
||||
&& let Some(task) =
|
||||
paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
|
||||
@@ -571,17 +700,18 @@ impl MessageEditor {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let path_style = project.read(cx).path_style(cx);
|
||||
let buffer = self.editor.read(cx).buffer().clone();
|
||||
let Some(buffer) = buffer.read(cx).as_singleton() else {
|
||||
return;
|
||||
};
|
||||
let mut tasks = Vec::new();
|
||||
for path in paths {
|
||||
let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
|
||||
let Some(entry) = project.read(cx).entry_for_path(&path, cx) else {
|
||||
continue;
|
||||
};
|
||||
let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
|
||||
let Some(worktree) = project.read(cx).worktree_for_id(path.worktree_id, cx) else {
|
||||
continue;
|
||||
};
|
||||
let abs_path = worktree.read(cx).absolutize(&path.path);
|
||||
@@ -689,9 +819,13 @@ impl MessageEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.clear(window, cx);
|
||||
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let path_style = workspace.read(cx).project().read(cx).path_style(cx);
|
||||
let mut text = String::new();
|
||||
let mut mentions = Vec::new();
|
||||
|
||||
@@ -934,7 +1068,7 @@ mod tests {
|
||||
cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
@@ -1045,7 +1179,7 @@ mod tests {
|
||||
cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace_handle.clone(),
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
@@ -1206,7 +1340,7 @@ mod tests {
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace_handle,
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
@@ -1428,7 +1562,7 @@ mod tests {
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace_handle,
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
@@ -1919,7 +2053,7 @@ mod tests {
|
||||
cx.new(|cx| {
|
||||
let editor = MessageEditor::new(
|
||||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
@@ -2024,7 +2158,7 @@ mod tests {
|
||||
cx.new(|cx| {
|
||||
let mut editor = MessageEditor::new(
|
||||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
@@ -2093,7 +2227,7 @@ mod tests {
|
||||
cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
@@ -2156,7 +2290,7 @@ mod tests {
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace_handle,
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
@@ -2314,7 +2448,7 @@ mod tests {
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace_handle,
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
|
||||
@@ -100,7 +100,7 @@ impl ThreadError {
|
||||
{
|
||||
Self::ModelRequestLimitReached(error.plan)
|
||||
} else if let Some(acp_error) = error.downcast_ref::<acp::Error>()
|
||||
&& acp_error.code == acp::ErrorCode::AUTH_REQUIRED.code
|
||||
&& acp_error.code == acp::ErrorCode::AuthRequired
|
||||
{
|
||||
Self::AuthenticationRequired(acp_error.message.clone().into())
|
||||
} else {
|
||||
@@ -170,7 +170,7 @@ impl ThreadFeedbackState {
|
||||
}
|
||||
}
|
||||
let session_id = thread.read(cx).session_id().clone();
|
||||
let agent = thread.read(cx).connection().telemetry_id();
|
||||
let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
|
||||
let task = telemetry.thread_data(&session_id, cx);
|
||||
let rating = match feedback {
|
||||
ThreadFeedback::Positive => "positive",
|
||||
@@ -180,7 +180,7 @@ impl ThreadFeedbackState {
|
||||
let thread = task.await?;
|
||||
telemetry::event!(
|
||||
"Agent Thread Rated",
|
||||
agent = agent,
|
||||
agent = agent_telemetry_id,
|
||||
session_id = session_id,
|
||||
rating = rating,
|
||||
thread = thread
|
||||
@@ -207,13 +207,13 @@ impl ThreadFeedbackState {
|
||||
self.comments_editor.take();
|
||||
|
||||
let session_id = thread.read(cx).session_id().clone();
|
||||
let agent = thread.read(cx).connection().telemetry_id();
|
||||
let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
|
||||
let task = telemetry.thread_data(&session_id, cx);
|
||||
cx.background_spawn(async move {
|
||||
let thread = task.await?;
|
||||
telemetry::event!(
|
||||
"Agent Thread Feedback Comments",
|
||||
agent = agent,
|
||||
agent = agent_telemetry_id,
|
||||
session_id = session_id,
|
||||
comments = comments,
|
||||
thread = thread
|
||||
@@ -333,6 +333,7 @@ impl AcpThreadView {
|
||||
project: Entity<Project>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
track_load_event: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -344,7 +345,7 @@ impl AcpThreadView {
|
||||
let message_editor = cx.new(|cx| {
|
||||
let mut editor = MessageEditor::new(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
@@ -369,7 +370,7 @@ impl AcpThreadView {
|
||||
let entry_view_state = cx.new(|_| {
|
||||
EntryViewState::new(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
project.downgrade(),
|
||||
history_store.clone(),
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
@@ -391,8 +392,9 @@ impl AcpThreadView {
|
||||
),
|
||||
];
|
||||
|
||||
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
|
||||
== Some(crate::ExternalAgent::Codex);
|
||||
let show_codex_windows_warning = cfg!(windows)
|
||||
&& project.read(cx).is_local()
|
||||
&& agent.clone().downcast::<agent_servers::Codex>().is_some();
|
||||
|
||||
Self {
|
||||
agent: agent.clone(),
|
||||
@@ -404,6 +406,7 @@ impl AcpThreadView {
|
||||
resume_thread.clone(),
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
track_load_event,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
@@ -448,6 +451,7 @@ impl AcpThreadView {
|
||||
self.resume_thread_metadata.clone(),
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -461,6 +465,7 @@ impl AcpThreadView {
|
||||
resume_thread: Option<DbThreadMetadata>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
track_load_event: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ThreadState {
|
||||
@@ -519,6 +524,10 @@ impl AcpThreadView {
|
||||
}
|
||||
};
|
||||
|
||||
if track_load_event {
|
||||
telemetry::event!("Agent Thread Started", agent = connection.telemetry_id());
|
||||
}
|
||||
|
||||
let result = if let Some(native_agent) = connection
|
||||
.clone()
|
||||
.downcast::<agent::NativeAgentConnection>()
|
||||
@@ -1133,8 +1142,8 @@ impl AcpThreadView {
|
||||
let Some(thread) = self.thread() else {
|
||||
return;
|
||||
};
|
||||
let agent_telemetry_id = self.agent.telemetry_id();
|
||||
let session_id = thread.read(cx).session_id().clone();
|
||||
let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
|
||||
let thread = thread.downgrade();
|
||||
if self.should_be_following {
|
||||
self.workspace
|
||||
@@ -1512,6 +1521,7 @@ impl AcpThreadView {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
|
||||
// Check for the experimental "terminal-auth" _meta field
|
||||
let auth_method = connection.auth_methods().iter().find(|m| m.id == method);
|
||||
@@ -1579,19 +1589,18 @@ impl AcpThreadView {
|
||||
);
|
||||
cx.notify();
|
||||
self.auth_task = Some(cx.spawn_in(window, {
|
||||
let agent = self.agent.clone();
|
||||
async move |this, cx| {
|
||||
let result = authenticate.await;
|
||||
|
||||
match &result {
|
||||
Ok(_) => telemetry::event!(
|
||||
"Authenticate Agent Succeeded",
|
||||
agent = agent.telemetry_id()
|
||||
agent = agent_telemetry_id
|
||||
),
|
||||
Err(_) => {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Failed",
|
||||
agent = agent.telemetry_id(),
|
||||
agent = agent_telemetry_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1675,6 +1684,7 @@ impl AcpThreadView {
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1730,43 +1740,38 @@ impl AcpThreadView {
|
||||
connection.authenticate(method, cx)
|
||||
};
|
||||
cx.notify();
|
||||
self.auth_task =
|
||||
Some(cx.spawn_in(window, {
|
||||
let agent = self.agent.clone();
|
||||
async move |this, cx| {
|
||||
let result = authenticate.await;
|
||||
self.auth_task = Some(cx.spawn_in(window, {
|
||||
async move |this, cx| {
|
||||
let result = authenticate.await;
|
||||
|
||||
match &result {
|
||||
Ok(_) => telemetry::event!(
|
||||
"Authenticate Agent Succeeded",
|
||||
agent = agent.telemetry_id()
|
||||
),
|
||||
Err(_) => {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Failed",
|
||||
agent = agent.telemetry_id(),
|
||||
)
|
||||
}
|
||||
match &result {
|
||||
Ok(_) => telemetry::event!(
|
||||
"Authenticate Agent Succeeded",
|
||||
agent = agent_telemetry_id
|
||||
),
|
||||
Err(_) => {
|
||||
telemetry::event!("Authenticate Agent Failed", agent = agent_telemetry_id,)
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if let Err(err) = result {
|
||||
if let ThreadState::Unauthenticated {
|
||||
pending_auth_method,
|
||||
..
|
||||
} = &mut this.thread_state
|
||||
{
|
||||
pending_auth_method.take();
|
||||
}
|
||||
this.handle_thread_error(err, cx);
|
||||
} else {
|
||||
this.reset(window, cx);
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if let Err(err) = result {
|
||||
if let ThreadState::Unauthenticated {
|
||||
pending_auth_method,
|
||||
..
|
||||
} = &mut this.thread_state
|
||||
{
|
||||
pending_auth_method.take();
|
||||
}
|
||||
this.handle_thread_error(err, cx);
|
||||
} else {
|
||||
this.reset(window, cx);
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn spawn_external_agent_login(
|
||||
@@ -1896,10 +1901,11 @@ impl AcpThreadView {
|
||||
let Some(thread) = self.thread() else {
|
||||
return;
|
||||
};
|
||||
let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
|
||||
|
||||
telemetry::event!(
|
||||
"Agent Tool Call Authorized",
|
||||
agent = self.agent.telemetry_id(),
|
||||
agent = agent_telemetry_id,
|
||||
session = thread.read(cx).session_id(),
|
||||
option = option_kind
|
||||
);
|
||||
@@ -3509,7 +3515,9 @@ impl AcpThreadView {
|
||||
(method.id.0.clone(), method.name.clone())
|
||||
};
|
||||
|
||||
Button::new(SharedString::from(method_id.clone()), name)
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
|
||||
Button::new(method_id.clone(), name)
|
||||
.label_size(LabelSize::Small)
|
||||
.map(|this| {
|
||||
if ix == 0 {
|
||||
@@ -3528,7 +3536,7 @@ impl AcpThreadView {
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = this.agent.telemetry_id(),
|
||||
agent = agent_telemetry_id,
|
||||
method = method_id
|
||||
);
|
||||
|
||||
@@ -5376,47 +5384,39 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
|
||||
if self.show_codex_windows_warning {
|
||||
Some(
|
||||
Callout::new()
|
||||
.icon(IconName::Warning)
|
||||
.severity(Severity::Warning)
|
||||
.title("Codex on Windows")
|
||||
.description(
|
||||
"For best performance, run Codex in Windows Subsystem for Linux (WSL2)",
|
||||
)
|
||||
.actions_slot(
|
||||
Button::new("open-wsl-modal", "Open in WSL")
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener({
|
||||
move |_, _, _window, cx| {
|
||||
#[cfg(windows)]
|
||||
_window.dispatch_action(
|
||||
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.dismiss_action(
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Warning"))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _, cx| {
|
||||
this.show_codex_windows_warning = false;
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
),
|
||||
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Callout {
|
||||
Callout::new()
|
||||
.icon(IconName::Warning)
|
||||
.severity(Severity::Warning)
|
||||
.title("Codex on Windows")
|
||||
.description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)")
|
||||
.actions_slot(
|
||||
Button::new("open-wsl-modal", "Open in WSL")
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener({
|
||||
move |_, _, _window, cx| {
|
||||
#[cfg(windows)]
|
||||
_window.dispatch_action(
|
||||
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.dismiss_action(
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Warning"))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _, cx| {
|
||||
this.show_codex_windows_warning = false;
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
@@ -5936,12 +5936,8 @@ impl Render for AcpThreadView {
|
||||
_ => this,
|
||||
})
|
||||
.children(self.render_thread_retry_status_callout(window, cx))
|
||||
.children({
|
||||
if cfg!(windows) && self.project.read(cx).is_local() {
|
||||
self.render_codex_windows_warning(cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.when(self.show_codex_windows_warning, |this| {
|
||||
this.child(self.render_codex_windows_warning(cx))
|
||||
})
|
||||
.children(self.render_thread_error(window, cx))
|
||||
.when_some(
|
||||
@@ -6057,13 +6053,13 @@ fn default_markdown_style(
|
||||
},
|
||||
border_color: Some(colors.border_variant),
|
||||
background: Some(colors.editor_background.into()),
|
||||
text: Some(TextStyleRefinement {
|
||||
text: TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.buffer_font.features.clone()),
|
||||
font_size: Some(buffer_font_size.into()),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
inline_code: TextStyleRefinement {
|
||||
@@ -6243,7 +6239,7 @@ pub(crate) mod tests {
|
||||
StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
|
||||
tool_call_id,
|
||||
vec![acp::PermissionOption::new(
|
||||
"1".into(),
|
||||
"1",
|
||||
"Allow",
|
||||
acp::PermissionOptionKind::AllowOnce,
|
||||
)],
|
||||
@@ -6398,6 +6394,7 @@ pub(crate) mod tests {
|
||||
project,
|
||||
history_store,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -6475,10 +6472,6 @@ pub(crate) mod tests {
|
||||
where
|
||||
C: 'static + AgentConnection + Send + Clone,
|
||||
{
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"test"
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::Ai
|
||||
}
|
||||
@@ -6505,8 +6498,8 @@ pub(crate) mod tests {
|
||||
struct SaboteurAgentConnection;
|
||||
|
||||
impl AgentConnection for SaboteurAgentConnection {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"saboteur"
|
||||
fn telemetry_id(&self) -> SharedString {
|
||||
"saboteur".into()
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
@@ -6569,8 +6562,8 @@ pub(crate) mod tests {
|
||||
struct RefusalAgentConnection;
|
||||
|
||||
impl AgentConnection for RefusalAgentConnection {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"refusal"
|
||||
fn telemetry_id(&self) -> SharedString {
|
||||
"refusal".into()
|
||||
}
|
||||
|
||||
fn new_thread(
|
||||
@@ -6671,6 +6664,7 @@ pub(crate) mod tests {
|
||||
project.clone(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -34,9 +34,9 @@ use project::{
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure,
|
||||
Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize,
|
||||
PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
|
||||
ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider,
|
||||
DividerColor, ElevationIndex, Indicator, LabelSize, PopoverMenu, Switch, Tooltip,
|
||||
WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
@@ -838,7 +838,7 @@ impl AgentConfiguration {
|
||||
.min_w_0()
|
||||
.child(
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("tooltip-{}", item_id)))
|
||||
.id(format!("tooltip-{}", item_id))
|
||||
.h_full()
|
||||
.w_3()
|
||||
.mr_2()
|
||||
@@ -879,7 +879,6 @@ impl AgentConfiguration {
|
||||
.child(context_server_configuration_menu)
|
||||
.child(
|
||||
Switch::new("context-server-switch", is_running.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let context_server_manager = self.context_server_store.clone();
|
||||
let fs = self.fs.clone();
|
||||
@@ -976,9 +975,12 @@ impl AgentConfiguration {
|
||||
let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
|
||||
AgentIcon::Path(icon_path)
|
||||
} else {
|
||||
AgentIcon::Name(IconName::Ai)
|
||||
AgentIcon::Name(IconName::Sparkle)
|
||||
};
|
||||
(name, icon)
|
||||
let display_name = agent_server_store
|
||||
.agent_display_name(&name)
|
||||
.unwrap_or_else(|| name.0.clone());
|
||||
(name, icon, display_name)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -1085,6 +1087,7 @@ impl AgentConfiguration {
|
||||
.child(self.render_agent_server(
|
||||
AgentIcon::Name(IconName::AiClaude),
|
||||
"Claude Code",
|
||||
"Claude Code",
|
||||
false,
|
||||
cx,
|
||||
))
|
||||
@@ -1092,6 +1095,7 @@ impl AgentConfiguration {
|
||||
.child(self.render_agent_server(
|
||||
AgentIcon::Name(IconName::AiOpenAi),
|
||||
"Codex CLI",
|
||||
"Codex CLI",
|
||||
false,
|
||||
cx,
|
||||
))
|
||||
@@ -1099,16 +1103,23 @@ impl AgentConfiguration {
|
||||
.child(self.render_agent_server(
|
||||
AgentIcon::Name(IconName::AiGemini),
|
||||
"Gemini CLI",
|
||||
"Gemini CLI",
|
||||
false,
|
||||
cx,
|
||||
))
|
||||
.map(|mut parent| {
|
||||
for (name, icon) in user_defined_agents {
|
||||
for (name, icon, display_name) in user_defined_agents {
|
||||
parent = parent
|
||||
.child(
|
||||
Divider::horizontal().color(DividerColor::BorderFaded),
|
||||
)
|
||||
.child(self.render_agent_server(icon, name, true, cx));
|
||||
.child(self.render_agent_server(
|
||||
icon,
|
||||
name,
|
||||
display_name,
|
||||
true,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
parent
|
||||
}),
|
||||
@@ -1119,11 +1130,14 @@ impl AgentConfiguration {
|
||||
fn render_agent_server(
|
||||
&self,
|
||||
icon: AgentIcon,
|
||||
name: impl Into<SharedString>,
|
||||
id: impl Into<SharedString>,
|
||||
display_name: impl Into<SharedString>,
|
||||
external: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let name = name.into();
|
||||
let id = id.into();
|
||||
let display_name = display_name.into();
|
||||
|
||||
let icon = match icon {
|
||||
AgentIcon::Name(icon_name) => Icon::new(icon_name)
|
||||
.size(IconSize::Small)
|
||||
@@ -1133,12 +1147,15 @@ impl AgentConfiguration {
|
||||
.color(Color::Muted),
|
||||
};
|
||||
|
||||
let tooltip_id = SharedString::new(format!("agent-source-{}", name));
|
||||
let tooltip_message = format!("The {} agent was installed from an extension.", name);
|
||||
let tooltip_id = SharedString::new(format!("agent-source-{}", id));
|
||||
let tooltip_message = format!(
|
||||
"The {} agent was installed from an extension.",
|
||||
display_name
|
||||
);
|
||||
|
||||
let agent_server_name = ExternalAgentServerName(name.clone());
|
||||
let agent_server_name = ExternalAgentServerName(id.clone());
|
||||
|
||||
let uninstall_btn_id = SharedString::from(format!("uninstall-{}", name));
|
||||
let uninstall_btn_id = SharedString::from(format!("uninstall-{}", id));
|
||||
let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -1162,7 +1179,7 @@ impl AgentConfiguration {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(icon)
|
||||
.child(Label::new(name))
|
||||
.child(Label::new(display_name))
|
||||
.when(external, |this| {
|
||||
this.child(
|
||||
div()
|
||||
|
||||
@@ -87,7 +87,7 @@ impl ConfigureContextServerToolsModal {
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("tool-header-{}", index)))
|
||||
.id(format!("tool-header-{}", index))
|
||||
.py_1()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
|
||||
@@ -422,7 +422,7 @@ impl ManageProfilesModal {
|
||||
let is_focused = profile.navigation.focus_handle.contains_focused(window, cx);
|
||||
|
||||
div()
|
||||
.id(SharedString::from(format!("profile-{}", profile.id)))
|
||||
.id(format!("profile-{}", profile.id))
|
||||
.track_focus(&profile.navigation.focus_handle)
|
||||
.on_action({
|
||||
let profile_id = profile.id.clone();
|
||||
@@ -431,7 +431,7 @@ impl ManageProfilesModal {
|
||||
})
|
||||
})
|
||||
.child(
|
||||
ListItem::new(SharedString::from(format!("profile-{}", profile.id)))
|
||||
ListItem::new(format!("profile-{}", profile.id))
|
||||
.toggle_state(is_focused)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
|
||||
@@ -130,7 +130,12 @@ impl AgentDiffPane {
|
||||
.action_log()
|
||||
.read(cx)
|
||||
.changed_buffers(cx);
|
||||
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||
let mut paths_to_delete = self
|
||||
.multibuffer
|
||||
.read(cx)
|
||||
.paths()
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for (buffer, diff_handle) in changed_buffers {
|
||||
if buffer.read(cx).file().is_none() {
|
||||
|
||||
@@ -63,6 +63,10 @@ impl AgentModelSelector {
|
||||
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
pub fn active_model(&self, cx: &App) -> Option<language_model::ConfiguredModel> {
|
||||
self.selector.read(cx).delegate.active_model(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentModelSelector {
|
||||
@@ -98,7 +102,7 @@ impl Render for AgentModelSelector {
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
|
||||
@@ -259,7 +259,7 @@ impl AgentType {
|
||||
Self::Gemini => Some(IconName::AiGemini),
|
||||
Self::ClaudeCode => Some(IconName::AiClaude),
|
||||
Self::Codex => Some(IconName::AiOpenAi),
|
||||
Self::Custom { .. } => Some(IconName::Terminal),
|
||||
Self::Custom { .. } => Some(IconName::Sparkle),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,6 +305,7 @@ impl ActiveView {
|
||||
project,
|
||||
history_store,
|
||||
prompt_store,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -885,10 +886,6 @@ impl AgentPanel {
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
|
||||
if !loading {
|
||||
telemetry::event!("Agent Thread Started", agent = server.telemetry_id());
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let selected_agent = ext_agent.into();
|
||||
if this.selected_agent != selected_agent {
|
||||
@@ -905,6 +902,7 @@ impl AgentPanel {
|
||||
project,
|
||||
this.history_store.clone(),
|
||||
this.prompt_store.clone(),
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1853,14 +1851,17 @@ impl AgentPanel {
|
||||
let agent_server_store = self.project.read(cx).agent_server_store().clone();
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
// Get custom icon path for selected agent before building menu (to avoid borrow issues)
|
||||
let selected_agent_custom_icon =
|
||||
let (selected_agent_custom_icon, selected_agent_label) =
|
||||
if let AgentType::Custom { name, .. } = &self.selected_agent {
|
||||
agent_server_store
|
||||
.read(cx)
|
||||
.agent_icon(&ExternalAgentServerName(name.clone()))
|
||||
let store = agent_server_store.read(cx);
|
||||
let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
|
||||
|
||||
let label = store
|
||||
.agent_display_name(&ExternalAgentServerName(name.clone()))
|
||||
.unwrap_or_else(|| self.selected_agent.label());
|
||||
(icon, label)
|
||||
} else {
|
||||
None
|
||||
(None, self.selected_agent.label())
|
||||
};
|
||||
|
||||
let active_thread = match &self.active_view {
|
||||
@@ -2083,13 +2084,16 @@ impl AgentPanel {
|
||||
|
||||
for agent_name in agent_names {
|
||||
let icon_path = agent_server_store.agent_icon(&agent_name);
|
||||
let display_name = agent_server_store
|
||||
.agent_display_name(&agent_name)
|
||||
.unwrap_or_else(|| agent_name.0.clone());
|
||||
|
||||
let mut entry = ContextMenuEntry::new(agent_name.clone());
|
||||
let mut entry = ContextMenuEntry::new(display_name);
|
||||
|
||||
if let Some(icon_path) = icon_path {
|
||||
entry = entry.custom_icon_svg(icon_path);
|
||||
} else {
|
||||
entry = entry.icon(IconName::Terminal);
|
||||
entry = entry.icon(IconName::Sparkle);
|
||||
}
|
||||
entry = entry
|
||||
.when(
|
||||
@@ -2153,8 +2157,6 @@ impl AgentPanel {
|
||||
}
|
||||
});
|
||||
|
||||
let selected_agent_label = self.selected_agent.label();
|
||||
|
||||
let is_thread_loading = self
|
||||
.active_thread_view()
|
||||
.map(|thread| thread.read(cx).is_loading())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mod acp;
|
||||
pub mod acp;
|
||||
mod agent_configuration;
|
||||
mod agent_diff;
|
||||
mod agent_model_selector;
|
||||
@@ -7,8 +7,6 @@ mod buffer_codegen;
|
||||
mod completion_provider;
|
||||
mod context;
|
||||
mod context_server_configuration;
|
||||
#[cfg(test)]
|
||||
mod evals;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod language_model_selector;
|
||||
@@ -28,7 +26,7 @@ use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, Entity, SharedString, actions};
|
||||
use language::{
|
||||
@@ -160,16 +158,6 @@ pub enum ExternalAgent {
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
pub fn parse_built_in(server: &dyn agent_servers::AgentServer) -> Option<Self> {
|
||||
match server.telemetry_id() {
|
||||
"gemini-cli" => Some(Self::Gemini),
|
||||
"claude-code" => Some(Self::ClaudeCode),
|
||||
"codex" => Some(Self::Codex),
|
||||
"zed" => Some(Self::NativeAgent),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server(
|
||||
&self,
|
||||
fs: Arc<dyn fs::Fs>,
|
||||
@@ -226,7 +214,7 @@ pub fn init(
|
||||
is_eval: bool,
|
||||
cx: &mut App,
|
||||
) {
|
||||
assistant_text_thread::init(client.clone(), cx);
|
||||
assistant_text_thread::init(client, cx);
|
||||
rules_library::init(cx);
|
||||
if !is_eval {
|
||||
// Initializing the language model from the user settings messes with the eval, so we only initialize them when
|
||||
@@ -239,13 +227,8 @@ pub fn init(
|
||||
TextThreadEditor::init(cx);
|
||||
|
||||
register_slash_commands(cx);
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx);
|
||||
inline_assistant::init(fs.clone(), prompt_builder.clone(), cx);
|
||||
terminal_inline_assistant::init(fs.clone(), prompt_builder, cx);
|
||||
cx.observe_new(move |workspace, window, cx| {
|
||||
ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
|
||||
})
|
||||
@@ -261,11 +244,17 @@ pub fn init(
|
||||
update_command_palette_filter(app_cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.on_flags_ready(|_, cx| {
|
||||
update_command_palette_filter(cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_command_palette_filter(cx: &mut App) {
|
||||
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
||||
let agent_enabled = AgentSettings::get_global(cx).enabled;
|
||||
let agent_v2_enabled = cx.has_flag::<AgentV2FeatureFlag>();
|
||||
let edit_prediction_provider = AllLanguageSettings::get_global(cx)
|
||||
.edit_predictions
|
||||
.provider;
|
||||
@@ -286,6 +275,7 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
|
||||
if disable_ai {
|
||||
filter.hide_namespace("agent");
|
||||
filter.hide_namespace("agents");
|
||||
filter.hide_namespace("assistant");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
@@ -297,8 +287,10 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
} else {
|
||||
if agent_enabled {
|
||||
filter.show_namespace("agent");
|
||||
filter.show_namespace("agents");
|
||||
} else {
|
||||
filter.hide_namespace("agent");
|
||||
filter.hide_namespace("agents");
|
||||
}
|
||||
|
||||
filter.show_namespace("assistant");
|
||||
@@ -334,6 +326,9 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
|
||||
filter.show_namespace("zed_predict_onboarding");
|
||||
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
if !agent_v2_enabled {
|
||||
filter.hide_action_types(&[TypeId::of::<zed_actions::agent::ToggleAgentPane>()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -432,7 +427,7 @@ mod tests {
|
||||
use gpui::{BorrowAppContext, TestAppContext, px};
|
||||
use project::DisableAiSettings;
|
||||
use settings::{
|
||||
DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore,
|
||||
DefaultAgentView, DockPosition, DockSide, NotifyWhenAgentWaiting, Settings, SettingsStore,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
@@ -451,10 +446,12 @@ mod tests {
|
||||
enabled: true,
|
||||
button: true,
|
||||
dock: DockPosition::Right,
|
||||
agents_panel_dock: DockSide::Left,
|
||||
default_width: px(300.),
|
||||
default_height: px(600.),
|
||||
default_model: None,
|
||||
inline_assistant_model: None,
|
||||
inline_assistant_use_streaming_tools: false,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: vec![],
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use uuid::Uuid;
|
||||
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
use feature_flags::{FeatureFlagAppExt as _, InlineAssistantUseToolFeatureFlag};
|
||||
use futures::{
|
||||
SinkExt, Stream, StreamExt, TryStreamExt as _,
|
||||
channel::mpsc,
|
||||
future::{LocalBoxFuture, Shared},
|
||||
join,
|
||||
stream::BoxStream,
|
||||
};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
|
||||
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
|
||||
use language::{Buffer, IndentKind, LanguageName, Point, TransactionId, line_diff};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelTextStream, Role, report_assistant_event,
|
||||
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelRequestTool, LanguageModelTextStream, LanguageModelToolChoice,
|
||||
LanguageModelToolUse, Role, TokenUsage,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use prompt_store::PromptBuilder;
|
||||
use rope::Rope;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use smol::future::FutureExt;
|
||||
use std::{
|
||||
cmp,
|
||||
@@ -33,7 +41,26 @@ use std::{
|
||||
time::Instant,
|
||||
};
|
||||
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
|
||||
/// Use this tool when you cannot or should not make a rewrite. This includes:
|
||||
/// - The user's request is unclear, ambiguous, or nonsensical
|
||||
/// - The requested change cannot be made by only editing the <rewrite_this> section
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FailureMessageInput {
|
||||
/// A brief message to the user explaining why you're unable to fulfill the request or to ask a question about the request.
|
||||
#[serde(default)]
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.
|
||||
/// Only use this tool when you are confident you understand the user's request and can fulfill it
|
||||
/// by editing the marked section.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RewriteSectionInput {
|
||||
/// The text to replace the section with.
|
||||
#[serde(default)]
|
||||
pub replacement_text: String,
|
||||
}
|
||||
|
||||
pub struct BufferCodegen {
|
||||
alternatives: Vec<Entity<CodegenAlternative>>,
|
||||
@@ -43,9 +70,9 @@ pub struct BufferCodegen {
|
||||
buffer: Entity<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
pub is_insertion: bool,
|
||||
session_id: Uuid,
|
||||
}
|
||||
|
||||
impl BufferCodegen {
|
||||
@@ -53,7 +80,7 @@ impl BufferCodegen {
|
||||
buffer: Entity<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
session_id: Uuid,
|
||||
builder: Arc<PromptBuilder>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -62,8 +89,8 @@ impl BufferCodegen {
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
false,
|
||||
Some(telemetry.clone()),
|
||||
builder.clone(),
|
||||
session_id,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -76,8 +103,8 @@ impl BufferCodegen {
|
||||
buffer,
|
||||
range,
|
||||
initial_transaction_id,
|
||||
telemetry,
|
||||
builder,
|
||||
session_id,
|
||||
};
|
||||
this.activate(0, cx);
|
||||
this
|
||||
@@ -92,10 +119,18 @@ impl BufferCodegen {
|
||||
.push(cx.subscribe(&codegen, |_, _, event, cx| cx.emit(*event)));
|
||||
}
|
||||
|
||||
pub fn active_completion(&self, cx: &App) -> Option<String> {
|
||||
self.active_alternative().read(cx).current_completion()
|
||||
}
|
||||
|
||||
pub fn active_alternative(&self) -> &Entity<CodegenAlternative> {
|
||||
&self.alternatives[self.active_alternative]
|
||||
}
|
||||
|
||||
pub fn language_name(&self, cx: &App) -> Option<LanguageName> {
|
||||
self.active_alternative().read(cx).language_name(cx)
|
||||
}
|
||||
|
||||
pub fn status<'a>(&self, cx: &'a App) -> &'a CodegenStatus {
|
||||
&self.active_alternative().read(cx).status
|
||||
}
|
||||
@@ -154,8 +189,8 @@ impl BufferCodegen {
|
||||
self.buffer.clone(),
|
||||
self.range.clone(),
|
||||
false,
|
||||
Some(self.telemetry.clone()),
|
||||
self.builder.clone(),
|
||||
self.session_id,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
@@ -214,6 +249,14 @@ impl BufferCodegen {
|
||||
pub fn last_equal_ranges<'a>(&self, cx: &'a App) -> &'a [Range<Anchor>] {
|
||||
self.active_alternative().read(cx).last_equal_ranges()
|
||||
}
|
||||
|
||||
pub fn selected_text<'a>(&self, cx: &'a App) -> Option<&'a str> {
|
||||
self.active_alternative().read(cx).selected_text()
|
||||
}
|
||||
|
||||
pub fn session_id(&self) -> Uuid {
|
||||
self.session_id
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<CodegenEvent> for BufferCodegen {}
|
||||
@@ -229,7 +272,6 @@ pub struct CodegenAlternative {
|
||||
status: CodegenStatus,
|
||||
generation: Task<()>,
|
||||
diff: Diff,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
_subscription: gpui::Subscription,
|
||||
builder: Arc<PromptBuilder>,
|
||||
active: bool,
|
||||
@@ -237,7 +279,11 @@ pub struct CodegenAlternative {
|
||||
line_operations: Vec<LineOperation>,
|
||||
elapsed_time: Option<f64>,
|
||||
completion: Option<String>,
|
||||
selected_text: Option<String>,
|
||||
pub message_id: Option<String>,
|
||||
session_id: Uuid,
|
||||
pub description: Option<String>,
|
||||
pub failure: Option<String>,
|
||||
}
|
||||
|
||||
impl EventEmitter<CodegenEvent> for CodegenAlternative {}
|
||||
@@ -247,8 +293,8 @@ impl CodegenAlternative {
|
||||
buffer: Entity<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
active: bool,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
session_id: Uuid,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
@@ -287,18 +333,28 @@ impl CodegenAlternative {
|
||||
status: CodegenStatus::Idle,
|
||||
generation: Task::ready(()),
|
||||
diff: Diff::default(),
|
||||
telemetry,
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
builder,
|
||||
active,
|
||||
active: active,
|
||||
edits: Vec::new(),
|
||||
line_operations: Vec::new(),
|
||||
range,
|
||||
elapsed_time: None,
|
||||
completion: None,
|
||||
selected_text: None,
|
||||
session_id,
|
||||
description: None,
|
||||
failure: None,
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn language_name(&self, cx: &App) -> Option<LanguageName> {
|
||||
self.old_buffer
|
||||
.read(cx)
|
||||
.language()
|
||||
.map(|language| language.name())
|
||||
}
|
||||
|
||||
pub fn set_active(&mut self, active: bool, cx: &mut Context<Self>) {
|
||||
if active != self.active {
|
||||
self.active = active;
|
||||
@@ -340,6 +396,12 @@ impl CodegenAlternative {
|
||||
&self.last_equal_ranges
|
||||
}
|
||||
|
||||
pub fn use_streaming_tools(model: &dyn LanguageModel, cx: &App) -> bool {
|
||||
model.supports_streaming_tools()
|
||||
&& cx.has_flag::<InlineAssistantUseToolFeatureFlag>()
|
||||
&& AgentSettings::get_global(cx).inline_assistant_use_streaming_tools
|
||||
}
|
||||
|
||||
pub fn start(
|
||||
&mut self,
|
||||
user_prompt: String,
|
||||
@@ -355,23 +417,133 @@ impl CodegenAlternative {
|
||||
|
||||
self.edit_position = Some(self.range.start.bias_right(&self.snapshot));
|
||||
|
||||
let api_key = model.api_key(cx);
|
||||
let telemetry_id = model.telemetry_id();
|
||||
let provider_id = model.provider_id();
|
||||
let stream: LocalBoxFuture<Result<LanguageModelTextStream>> =
|
||||
if user_prompt.trim().to_lowercase() == "delete" {
|
||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.build_request(&model, user_prompt, context_task, cx)?;
|
||||
cx.spawn(async move |_, cx| {
|
||||
Ok(model.stream_completion_text(request.await, cx).await?)
|
||||
})
|
||||
.boxed_local()
|
||||
};
|
||||
self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
|
||||
if Self::use_streaming_tools(model.as_ref(), cx) {
|
||||
let request = self.build_request(&model, user_prompt, context_task, cx)?;
|
||||
let completion_events = cx.spawn({
|
||||
let model = model.clone();
|
||||
async move |_, cx| model.stream_completion(request.await, cx).await
|
||||
});
|
||||
self.generation = self.handle_completion(model, completion_events, cx);
|
||||
} else {
|
||||
let stream: LocalBoxFuture<Result<LanguageModelTextStream>> =
|
||||
if user_prompt.trim().to_lowercase() == "delete" {
|
||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.build_request(&model, user_prompt, context_task, cx)?;
|
||||
cx.spawn({
|
||||
let model = model.clone();
|
||||
async move |_, cx| {
|
||||
Ok(model.stream_completion_text(request.await, cx).await?)
|
||||
}
|
||||
})
|
||||
.boxed_local()
|
||||
};
|
||||
self.generation = self.handle_stream(model, stream, cx);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_request_tools(
|
||||
&self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
user_prompt: String,
|
||||
context_task: Shared<Task<Option<LoadedContext>>>,
|
||||
cx: &mut App,
|
||||
) -> Result<Task<LanguageModelRequest>> {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let language = buffer.language_at(self.range.start);
|
||||
let language_name = if let Some(language) = language.as_ref() {
|
||||
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
|
||||
None
|
||||
} else {
|
||||
Some(language.name())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let language_name = language_name.as_ref();
|
||||
let start = buffer.point_to_buffer_offset(self.range.start);
|
||||
let end = buffer.point_to_buffer_offset(self.range.end);
|
||||
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
|
||||
let (start_buffer, start_buffer_offset) = start;
|
||||
let (end_buffer, end_buffer_offset) = end;
|
||||
if start_buffer.remote_id() == end_buffer.remote_id() {
|
||||
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
|
||||
} else {
|
||||
anyhow::bail!("invalid transformation range");
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("invalid transformation range");
|
||||
};
|
||||
|
||||
let system_prompt = self
|
||||
.builder
|
||||
.generate_inline_transformation_prompt_tools(
|
||||
language_name,
|
||||
buffer,
|
||||
range.start.0..range.end.0,
|
||||
)
|
||||
.context("generating content prompt")?;
|
||||
|
||||
let temperature = AgentSettings::temperature_for_model(model, cx);
|
||||
|
||||
let tool_input_format = model.tool_input_format();
|
||||
let tool_choice = model
|
||||
.supports_tool_choice(LanguageModelToolChoice::Any)
|
||||
.then_some(LanguageModelToolChoice::Any);
|
||||
|
||||
Ok(cx.spawn(async move |_cx| {
|
||||
let mut messages = vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![system_prompt.into()],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
}];
|
||||
|
||||
let mut user_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
};
|
||||
|
||||
if let Some(context) = context_task.await {
|
||||
context.add_to_request_message(&mut user_message);
|
||||
}
|
||||
|
||||
user_message.content.push(user_prompt.into());
|
||||
messages.push(user_message);
|
||||
|
||||
let tools = vec![
|
||||
LanguageModelRequestTool {
|
||||
name: "rewrite_section".to_string(),
|
||||
description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
|
||||
},
|
||||
LanguageModelRequestTool {
|
||||
name: "failure_message".to_string(),
|
||||
description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
|
||||
},
|
||||
];
|
||||
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: Some(CompletionIntent::InlineAssist),
|
||||
mode: None,
|
||||
tools,
|
||||
tool_choice,
|
||||
stop: Vec::new(),
|
||||
temperature,
|
||||
messages,
|
||||
thinking_allowed: false,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
@@ -379,6 +551,10 @@ impl CodegenAlternative {
|
||||
context_task: Shared<Task<Option<LoadedContext>>>,
|
||||
cx: &mut App,
|
||||
) -> Result<Task<LanguageModelRequest>> {
|
||||
if Self::use_streaming_tools(model.as_ref(), cx) {
|
||||
return self.build_request_tools(model, user_prompt, context_task, cx);
|
||||
}
|
||||
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let language = buffer.language_at(self.range.start);
|
||||
let language_name = if let Some(language) = language.as_ref() {
|
||||
@@ -449,12 +625,14 @@ impl CodegenAlternative {
|
||||
|
||||
pub fn handle_stream(
|
||||
&mut self,
|
||||
model_telemetry_id: String,
|
||||
model_provider_id: String,
|
||||
model_api_key: Option<String>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
) -> Task<()> {
|
||||
let anthropic_reporter = language_model::AnthropicEventReporter::new(&model, cx);
|
||||
let session_id = self.session_id;
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let model_provider_id = model.provider_id().to_string();
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Make a new snapshot and re-resolve anchor in case the document was modified.
|
||||
@@ -469,6 +647,8 @@ impl CodegenAlternative {
|
||||
.text_for_range(self.range.start..self.range.end)
|
||||
.collect::<Rope>();
|
||||
|
||||
self.selected_text = Some(selected_text.to_string());
|
||||
|
||||
let selection_start = self.range.start.to_point(&snapshot);
|
||||
|
||||
// Start with the indentation of the first line in the selection
|
||||
@@ -490,8 +670,6 @@ impl CodegenAlternative {
|
||||
}
|
||||
}
|
||||
|
||||
let http_client = cx.http_client();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let language_name = {
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
@@ -508,8 +686,10 @@ impl CodegenAlternative {
|
||||
let completion = Arc::new(Mutex::new(String::new()));
|
||||
let completion_clone = completion.clone();
|
||||
|
||||
self.generation = cx.spawn(async move |codegen, cx| {
|
||||
cx.notify();
|
||||
cx.spawn(async move |codegen, cx| {
|
||||
let stream = stream.await;
|
||||
|
||||
let token_usage = stream
|
||||
.as_ref()
|
||||
.ok()
|
||||
@@ -522,10 +702,11 @@ impl CodegenAlternative {
|
||||
let model_telemetry_id = model_telemetry_id.clone();
|
||||
let model_provider_id = model_provider_id.clone();
|
||||
let (mut diff_tx, mut diff_rx) = mpsc::channel(1);
|
||||
let executor = cx.background_executor().clone();
|
||||
let message_id = message_id.clone();
|
||||
let line_based_stream_diff: Task<anyhow::Result<()>> =
|
||||
cx.background_spawn(async move {
|
||||
let line_based_stream_diff: Task<anyhow::Result<()>> = cx.background_spawn({
|
||||
let anthropic_reporter = anthropic_reporter.clone();
|
||||
let language_name = language_name.clone();
|
||||
async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
@@ -533,6 +714,7 @@ impl CodegenAlternative {
|
||||
stream?.stream.map_err(|error| error.into()),
|
||||
);
|
||||
futures::pin_mut!(chunks);
|
||||
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let mut line_diff = LineDiff::default();
|
||||
|
||||
@@ -621,27 +803,30 @@ impl CodegenAlternative {
|
||||
let result = diff.await;
|
||||
|
||||
let error_message = result.as_ref().err().map(|error| error.to_string());
|
||||
report_assistant_event(
|
||||
AssistantEventData {
|
||||
conversation_id: None,
|
||||
message_id,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model_telemetry_id,
|
||||
model_provider: model_provider_id,
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
},
|
||||
telemetry,
|
||||
http_client,
|
||||
model_api_key,
|
||||
&executor,
|
||||
telemetry::event!(
|
||||
"Assistant Responded",
|
||||
kind = "inline",
|
||||
phase = "response",
|
||||
session_id = session_id.to_string(),
|
||||
model = model_telemetry_id,
|
||||
model_provider = model_provider_id,
|
||||
language_name = language_name.as_ref().map(|n| n.to_string()),
|
||||
message_id = message_id.as_deref(),
|
||||
response_latency = response_latency,
|
||||
error_message = error_message.as_deref(),
|
||||
);
|
||||
|
||||
anthropic_reporter.report(language_model::AnthropicEventData {
|
||||
completion_type: language_model::AnthropicCompletionType::Editor,
|
||||
event: language_model::AnthropicEventType::Response,
|
||||
language_name: language_name.map(|n| n.to_string()),
|
||||
message_id,
|
||||
});
|
||||
|
||||
result?;
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
while let Some((char_ops, line_ops)) = diff_rx.next().await {
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
@@ -724,8 +909,25 @@ impl CodegenAlternative {
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn current_completion(&self) -> Option<String> {
|
||||
self.completion.clone()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn current_description(&self) -> Option<String> {
|
||||
self.description.clone()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn current_failure(&self) -> Option<String> {
|
||||
self.failure.clone()
|
||||
}
|
||||
|
||||
pub fn selected_text(&self) -> Option<&str> {
|
||||
self.selected_text.as_deref()
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -899,6 +1101,219 @@ impl CodegenAlternative {
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_completion(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
completion_stream: Task<
|
||||
Result<
|
||||
BoxStream<
|
||||
'static,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>,
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<()> {
|
||||
self.diff = Diff::default();
|
||||
self.status = CodegenStatus::Pending;
|
||||
|
||||
cx.notify();
|
||||
// Leaving this in generation so that STOP equivalent events are respected even
|
||||
// while we're still pre-processing the completion event
|
||||
cx.spawn(async move |codegen, cx| {
|
||||
let finish_with_status = |status: CodegenStatus, cx: &mut AsyncApp| {
|
||||
let _ = codegen.update(cx, |this, cx| {
|
||||
this.status = status;
|
||||
cx.emit(CodegenEvent::Finished);
|
||||
cx.notify();
|
||||
});
|
||||
};
|
||||
|
||||
let mut completion_events = match completion_stream.await {
|
||||
Ok(events) => events,
|
||||
Err(err) => {
|
||||
finish_with_status(CodegenStatus::Error(err.into()), cx);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
enum ToolUseOutput {
|
||||
Rewrite {
|
||||
text: String,
|
||||
description: Option<String>,
|
||||
},
|
||||
Failure(String),
|
||||
}
|
||||
|
||||
enum ModelUpdate {
|
||||
Description(String),
|
||||
Failure(String),
|
||||
}
|
||||
|
||||
let chars_read_so_far = Arc::new(Mutex::new(0usize));
|
||||
let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option<ToolUseOutput> {
|
||||
let mut chars_read_so_far = chars_read_so_far.lock();
|
||||
match tool_use.name.as_ref() {
|
||||
"rewrite_section" => {
|
||||
let Ok(input) =
|
||||
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let text = input.replacement_text[*chars_read_so_far..].to_string();
|
||||
*chars_read_so_far = input.replacement_text.len();
|
||||
Some(ToolUseOutput::Rewrite {
|
||||
text,
|
||||
description: None,
|
||||
})
|
||||
}
|
||||
"failure_message" => {
|
||||
let Ok(mut input) =
|
||||
serde_json::from_value::<FailureMessageInput>(tool_use.input)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
Some(ToolUseOutput::Failure(std::mem::take(&mut input.message)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
let (message_tx, mut message_rx) = futures::channel::mpsc::unbounded::<ModelUpdate>();
|
||||
|
||||
cx.spawn({
|
||||
let codegen = codegen.clone();
|
||||
async move |cx| {
|
||||
while let Some(update) = message_rx.next().await {
|
||||
let _ = codegen.update(cx, |this, _cx| match update {
|
||||
ModelUpdate::Description(d) => this.description = Some(d),
|
||||
ModelUpdate::Failure(f) => this.failure = Some(f),
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut message_id = None;
|
||||
let mut first_text = None;
|
||||
let last_token_usage = Arc::new(Mutex::new(TokenUsage::default()));
|
||||
let total_text = Arc::new(Mutex::new(String::new()));
|
||||
|
||||
loop {
|
||||
if let Some(first_event) = completion_events.next().await {
|
||||
match first_event {
|
||||
Ok(LanguageModelCompletionEvent::StartMessage { message_id: id }) => {
|
||||
message_id = Some(id);
|
||||
}
|
||||
Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => {
|
||||
if let Some(output) = process_tool_use(tool_use) {
|
||||
let (text, update) = match output {
|
||||
ToolUseOutput::Rewrite { text, description } => {
|
||||
(Some(text), description.map(ModelUpdate::Description))
|
||||
}
|
||||
ToolUseOutput::Failure(message) => {
|
||||
(None, Some(ModelUpdate::Failure(message)))
|
||||
}
|
||||
};
|
||||
if let Some(update) = update {
|
||||
let _ = message_tx.unbounded_send(update);
|
||||
}
|
||||
first_text = text;
|
||||
if first_text.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => {
|
||||
*last_token_usage.lock() = token_usage;
|
||||
}
|
||||
Ok(LanguageModelCompletionEvent::Text(text)) => {
|
||||
let mut lock = total_text.lock();
|
||||
lock.push_str(&text);
|
||||
}
|
||||
Ok(e) => {
|
||||
log::warn!("Unexpected event: {:?}", e);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
finish_with_status(CodegenStatus::Error(e.into()), cx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(first_text) = first_text else {
|
||||
finish_with_status(CodegenStatus::Done, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let move_last_token_usage = last_token_usage.clone();
|
||||
|
||||
let text_stream = Box::pin(futures::stream::once(async { Ok(first_text) }).chain(
|
||||
completion_events.filter_map(move |e| {
|
||||
let process_tool_use = process_tool_use.clone();
|
||||
let last_token_usage = move_last_token_usage.clone();
|
||||
let total_text = total_text.clone();
|
||||
let mut message_tx = message_tx.clone();
|
||||
async move {
|
||||
match e {
|
||||
Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => {
|
||||
let Some(output) = process_tool_use(tool_use) else {
|
||||
return None;
|
||||
};
|
||||
let (text, update) = match output {
|
||||
ToolUseOutput::Rewrite { text, description } => {
|
||||
(Some(text), description.map(ModelUpdate::Description))
|
||||
}
|
||||
ToolUseOutput::Failure(message) => {
|
||||
(None, Some(ModelUpdate::Failure(message)))
|
||||
}
|
||||
};
|
||||
if let Some(update) = update {
|
||||
let _ = message_tx.send(update).await;
|
||||
}
|
||||
text.map(Ok)
|
||||
}
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => {
|
||||
*last_token_usage.lock() = token_usage;
|
||||
None
|
||||
}
|
||||
Ok(LanguageModelCompletionEvent::Text(text)) => {
|
||||
let mut lock = total_text.lock();
|
||||
lock.push_str(&text);
|
||||
None
|
||||
}
|
||||
Ok(LanguageModelCompletionEvent::Stop(_reason)) => None,
|
||||
e => {
|
||||
log::error!("UNEXPECTED EVENT {:?}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
));
|
||||
|
||||
let language_model_text_stream = LanguageModelTextStream {
|
||||
message_id: message_id,
|
||||
stream: text_stream,
|
||||
last_token_usage,
|
||||
};
|
||||
|
||||
let Some(task) = codegen
|
||||
.update(cx, move |codegen, cx| {
|
||||
codegen.handle_stream(model, async { Ok(language_model_text_stream) }, cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
task.await;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -1060,8 +1475,10 @@ mod tests {
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, Language, LanguageConfig, LanguageMatcher, Point, tree_sitter_rust};
|
||||
use language::{Buffer, Point};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use languages::rust_lang;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use std::{future, sync::Arc};
|
||||
@@ -1078,7 +1495,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(rust_lang(), cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
@@ -1090,8 +1507,8 @@ mod tests {
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
prompt_builder,
|
||||
Uuid::new_v4(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1140,7 +1557,7 @@ mod tests {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(rust_lang(), cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
@@ -1152,8 +1569,8 @@ mod tests {
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
prompt_builder,
|
||||
Uuid::new_v4(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1204,7 +1621,7 @@ mod tests {
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(rust_lang(), cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
@@ -1216,8 +1633,8 @@ mod tests {
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
prompt_builder,
|
||||
Uuid::new_v4(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1280,8 +1697,8 @@ mod tests {
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
None,
|
||||
prompt_builder,
|
||||
Uuid::new_v4(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1320,7 +1737,7 @@ mod tests {
|
||||
let x = 0;
|
||||
}
|
||||
"};
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(rust_lang(), cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
@@ -1332,8 +1749,8 @@ mod tests {
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
false,
|
||||
None,
|
||||
prompt_builder,
|
||||
Uuid::new_v4(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1422,11 +1839,10 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
) -> mpsc::UnboundedSender<String> {
|
||||
let (chunks_tx, chunks_rx) = mpsc::unbounded();
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
String::new(),
|
||||
String::new(),
|
||||
None,
|
||||
codegen.generation = codegen.handle_stream(
|
||||
model,
|
||||
future::ready(Ok(LanguageModelTextStream {
|
||||
message_id: None,
|
||||
stream: chunks_rx.map(Ok).boxed(),
|
||||
@@ -1437,27 +1853,4 @@ mod tests {
|
||||
});
|
||||
chunks_tx
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::inline_assistant::test::run_inline_assistant_test;
|
||||
|
||||
use eval_utils::{EvalOutput, NoProcessor};
|
||||
use gpui::TestAppContext;
|
||||
use language_model::{LanguageModelRegistry, SelectedModel};
|
||||
use rand::{SeedableRng as _, rngs::StdRng};
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||
fn eval_single_cursor_edit() {
|
||||
eval_utils::eval(20, 1.0, NoProcessor, move || {
|
||||
run_eval(
|
||||
&EvalInput {
|
||||
prompt: "Rename this variable to buffer_text".to_string(),
|
||||
buffer: indoc::indoc! {"
|
||||
struct EvalExampleStruct {
|
||||
text: Strˇing,
|
||||
prompt: String,
|
||||
}
|
||||
"}
|
||||
.to_string(),
|
||||
},
|
||||
&|_, output| {
|
||||
let expected = indoc::indoc! {"
|
||||
struct EvalExampleStruct {
|
||||
buffer_text: String,
|
||||
prompt: String,
|
||||
}
|
||||
"};
|
||||
if output == expected {
|
||||
EvalOutput {
|
||||
outcome: eval_utils::OutcomeKind::Passed,
|
||||
data: "Passed!".to_string(),
|
||||
metadata: (),
|
||||
}
|
||||
} else {
|
||||
EvalOutput {
|
||||
outcome: eval_utils::OutcomeKind::Failed,
|
||||
data: format!("Failed to rename variable, output: {}", output),
|
||||
metadata: (),
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
struct EvalInput {
|
||||
buffer: String,
|
||||
prompt: String,
|
||||
}
|
||||
|
||||
fn run_eval(
|
||||
input: &EvalInput,
|
||||
judge: &dyn Fn(&EvalInput, &str) -> eval_utils::EvalOutput<()>,
|
||||
) -> eval_utils::EvalOutput<()> {
|
||||
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
|
||||
let mut cx = TestAppContext::build(dispatcher, None);
|
||||
cx.skip_drawing();
|
||||
|
||||
let buffer_text = run_inline_assistant_test(
|
||||
input.buffer.clone(),
|
||||
input.prompt.clone(),
|
||||
|cx| {
|
||||
// Reconfigure to use a real model instead of the fake one
|
||||
let model_name = std::env::var("ZED_AGENT_MODEL")
|
||||
.unwrap_or("anthropic/claude-sonnet-4-latest".into());
|
||||
|
||||
let selected_model = SelectedModel::from_str(&model_name)
|
||||
.expect("Invalid model format. Use 'provider/model-id'");
|
||||
|
||||
log::info!("Selected model: {selected_model:?}");
|
||||
|
||||
cx.update(|_, cx| {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_inline_assistant_model(Some(&selected_model), cx);
|
||||
});
|
||||
});
|
||||
},
|
||||
|_cx| {
|
||||
log::info!("Waiting for actual response from the LLM...");
|
||||
},
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
judge(input, &buffer_text)
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
use language_model::AnthropicEventData;
|
||||
use language_model::report_anthropic_event;
|
||||
use std::cmp;
|
||||
use std::mem;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::load_context;
|
||||
use crate::mention_set::MentionSet;
|
||||
@@ -15,7 +18,6 @@ use crate::{
|
||||
use agent::HistoryStore;
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::EditorSnapshot;
|
||||
use editor::MultiBufferOffset;
|
||||
@@ -38,15 +40,13 @@ use gpui::{
|
||||
WeakEntity, Window, point,
|
||||
};
|
||||
use language::{Buffer, Point, Selection, TransactionId};
|
||||
use language_model::{
|
||||
ConfigurationError, ConfiguredModel, LanguageModelRegistry, report_assistant_event,
|
||||
};
|
||||
use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, DisableAiSettings, LspAction, Project, ProjectTransaction};
|
||||
use prompt_store::{PromptBuilder, PromptStore};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use text::{OffsetRangeExt, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
@@ -54,13 +54,8 @@ use util::{RangeExt, ResultExt, maybe};
|
||||
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
|
||||
pub fn init(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>, cx: &mut App) {
|
||||
cx.set_global(InlineAssistant::new(fs, prompt_builder));
|
||||
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
if DisableAiSettings::get_global(cx).disable_ai {
|
||||
@@ -100,7 +95,6 @@ pub struct InlineAssistant {
|
||||
confirmed_assists: HashMap<InlineAssistId, Entity<CodegenAlternative>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
_inline_assistant_completions: Option<mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>>,
|
||||
}
|
||||
@@ -108,11 +102,7 @@ pub struct InlineAssistant {
|
||||
impl Global for InlineAssistant {}
|
||||
|
||||
impl InlineAssistant {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
) -> Self {
|
||||
pub fn new(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>) -> Self {
|
||||
Self {
|
||||
next_assist_id: InlineAssistId::default(),
|
||||
next_assist_group_id: InlineAssistGroupId::default(),
|
||||
@@ -122,20 +112,11 @@ impl InlineAssistant {
|
||||
confirmed_assists: HashMap::default(),
|
||||
prompt_history: VecDeque::default(),
|
||||
prompt_builder,
|
||||
telemetry,
|
||||
fs,
|
||||
_inline_assistant_completions: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_completion_receiver(
|
||||
&mut self,
|
||||
sender: mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>,
|
||||
) {
|
||||
self._inline_assistant_completions = Some(sender);
|
||||
}
|
||||
|
||||
pub fn register_workspace(
|
||||
&mut self,
|
||||
workspace: &Entity<Workspace>,
|
||||
@@ -387,17 +368,9 @@ impl InlineAssistant {
|
||||
let mut selections = Vec::<Selection<Point>>::new();
|
||||
let mut newest_selection = None;
|
||||
for mut selection in initial_selections {
|
||||
if selection.end > selection.start {
|
||||
selection.start.column = 0;
|
||||
// If the selection ends at the start of the line, we don't want to include it.
|
||||
if selection.end.column == 0 {
|
||||
selection.end.row -= 1;
|
||||
}
|
||||
selection.end.column = snapshot
|
||||
.buffer_snapshot()
|
||||
.line_len(MultiBufferRow(selection.end.row));
|
||||
} else if let Some(fold) =
|
||||
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
|
||||
if selection.end == selection.start
|
||||
&& let Some(fold) =
|
||||
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
|
||||
{
|
||||
selection.start = fold.range().start;
|
||||
selection.end = fold.range().end;
|
||||
@@ -424,6 +397,15 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selection.start.column = 0;
|
||||
// If the selection ends at the start of the line, we don't want to include it.
|
||||
if selection.end.column == 0 && selection.start.row != selection.end.row {
|
||||
selection.end.row -= 1;
|
||||
}
|
||||
selection.end.column = snapshot
|
||||
.buffer_snapshot()
|
||||
.line_len(MultiBufferRow(selection.end.row));
|
||||
}
|
||||
|
||||
if let Some(prev_selection) = selections.last_mut()
|
||||
@@ -456,17 +438,25 @@ impl InlineAssistant {
|
||||
codegen_ranges.push(anchor_range);
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
|
||||
self.telemetry.report_assistant_event(AssistantEventData {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Invoked,
|
||||
message_id: None,
|
||||
model: model.model.telemetry_id(),
|
||||
model_provider: model.provider.id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name: buffer.language().map(|language| language.name().to_proto()),
|
||||
});
|
||||
telemetry::event!(
|
||||
"Assistant Invoked",
|
||||
kind = "inline",
|
||||
phase = "invoked",
|
||||
model = model.model.telemetry_id(),
|
||||
model_provider = model.provider.id().to_string(),
|
||||
language_name = buffer.language().map(|language| language.name().to_proto())
|
||||
);
|
||||
|
||||
report_anthropic_event(
|
||||
&model.model,
|
||||
AnthropicEventData {
|
||||
completion_type: language_model::AnthropicCompletionType::Editor,
|
||||
event: language_model::AnthropicEventType::Invoked,
|
||||
language_name: buffer.language().map(|language| language.name().to_proto()),
|
||||
message_id: None,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +480,7 @@ impl InlineAssistant {
|
||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
|
||||
let assist_group_id = self.next_assist_group_id.post_inc();
|
||||
let session_id = Uuid::new_v4();
|
||||
let prompt_buffer = cx.new(|cx| {
|
||||
MultiBuffer::singleton(
|
||||
cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)),
|
||||
@@ -507,7 +498,7 @@ impl InlineAssistant {
|
||||
editor.read(cx).buffer().clone(),
|
||||
range.clone(),
|
||||
initial_transaction_id,
|
||||
self.telemetry.clone(),
|
||||
session_id,
|
||||
self.prompt_builder.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -521,6 +512,7 @@ impl InlineAssistant {
|
||||
self.prompt_history.clone(),
|
||||
prompt_buffer.clone(),
|
||||
codegen.clone(),
|
||||
session_id,
|
||||
self.fs.clone(),
|
||||
thread_store.clone(),
|
||||
prompt_store.clone(),
|
||||
@@ -544,14 +536,15 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
let [prompt_block_id, end_block_id] =
|
||||
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
|
||||
let [prompt_block_id, tool_description_block_id, end_block_id] =
|
||||
self.insert_assist_blocks(&editor, &range, &prompt_editor, cx);
|
||||
|
||||
assists.push((
|
||||
assist_id,
|
||||
range.clone(),
|
||||
prompt_editor,
|
||||
prompt_block_id,
|
||||
tool_description_block_id,
|
||||
end_block_id,
|
||||
));
|
||||
}
|
||||
@@ -570,7 +563,15 @@ impl InlineAssistant {
|
||||
};
|
||||
|
||||
let mut assist_group = InlineAssistGroup::new();
|
||||
for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists {
|
||||
for (
|
||||
assist_id,
|
||||
range,
|
||||
prompt_editor,
|
||||
prompt_block_id,
|
||||
tool_description_block_id,
|
||||
end_block_id,
|
||||
) in assists
|
||||
{
|
||||
let codegen = prompt_editor.read(cx).codegen().clone();
|
||||
|
||||
self.assists.insert(
|
||||
@@ -581,6 +582,7 @@ impl InlineAssistant {
|
||||
editor,
|
||||
&prompt_editor,
|
||||
prompt_block_id,
|
||||
tool_description_block_id,
|
||||
end_block_id,
|
||||
range,
|
||||
codegen,
|
||||
@@ -689,7 +691,7 @@ impl InlineAssistant {
|
||||
range: &Range<Anchor>,
|
||||
prompt_editor: &Entity<PromptEditor<BufferCodegen>>,
|
||||
cx: &mut App,
|
||||
) -> [CustomBlockId; 2] {
|
||||
) -> [CustomBlockId; 3] {
|
||||
let prompt_editor_height = prompt_editor.update(cx, |prompt_editor, cx| {
|
||||
prompt_editor
|
||||
.editor
|
||||
@@ -703,6 +705,14 @@ impl InlineAssistant {
|
||||
render: build_assist_editor_renderer(prompt_editor),
|
||||
priority: 0,
|
||||
},
|
||||
// Placeholder for tool description - will be updated dynamically
|
||||
BlockProperties {
|
||||
style: BlockStyle::Flex,
|
||||
placement: BlockPlacement::Below(range.end),
|
||||
height: Some(0),
|
||||
render: Arc::new(|_cx| div().into_any_element()),
|
||||
priority: 0,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
placement: BlockPlacement::Below(range.end),
|
||||
@@ -721,7 +731,7 @@ impl InlineAssistant {
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let block_ids = editor.insert_blocks(assist_blocks, None, cx);
|
||||
[block_ids[0], block_ids[1]]
|
||||
[block_ids[0], block_ids[1], block_ids[2]]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1050,8 +1060,6 @@ impl InlineAssistant {
|
||||
}
|
||||
|
||||
let active_alternative = assist.codegen.read(cx).active_alternative().clone();
|
||||
let message_id = active_alternative.read(cx).message_id.clone();
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
|
||||
let language_name = assist.editor.upgrade().and_then(|editor| {
|
||||
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||
@@ -1060,28 +1068,49 @@ impl InlineAssistant {
|
||||
ranges
|
||||
.first()
|
||||
.and_then(|(buffer, _, _)| buffer.language())
|
||||
.map(|language| language.name())
|
||||
.map(|language| language.name().0.to_string())
|
||||
});
|
||||
report_assistant_event(
|
||||
AssistantEventData {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
|
||||
let codegen = assist.codegen.read(cx);
|
||||
let session_id = codegen.session_id();
|
||||
let message_id = active_alternative.read(cx).message_id.clone();
|
||||
let model_telemetry_id = model.model.telemetry_id();
|
||||
let model_provider_id = model.model.provider_id().to_string();
|
||||
|
||||
let (phase, event_type, anthropic_event_type) = if undo {
|
||||
(
|
||||
"rejected",
|
||||
"Assistant Response Rejected",
|
||||
language_model::AnthropicEventType::Reject,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"accepted",
|
||||
"Assistant Response Accepted",
|
||||
language_model::AnthropicEventType::Accept,
|
||||
)
|
||||
};
|
||||
|
||||
telemetry::event!(
|
||||
event_type,
|
||||
phase,
|
||||
session_id = session_id.to_string(),
|
||||
kind = "inline",
|
||||
model = model_telemetry_id,
|
||||
model_provider = model_provider_id,
|
||||
language_name = language_name,
|
||||
message_id = message_id.as_deref(),
|
||||
);
|
||||
|
||||
report_anthropic_event(
|
||||
&model.model,
|
||||
language_model::AnthropicEventData {
|
||||
completion_type: language_model::AnthropicCompletionType::Editor,
|
||||
event: anthropic_event_type,
|
||||
language_name,
|
||||
message_id,
|
||||
phase: if undo {
|
||||
AssistantPhase::Rejected
|
||||
} else {
|
||||
AssistantPhase::Accepted
|
||||
},
|
||||
model: model.model.telemetry_id(),
|
||||
model_provider: model.model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
},
|
||||
Some(self.telemetry.clone()),
|
||||
cx.http_client(),
|
||||
model.model.api_key(cx),
|
||||
cx.background_executor(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1113,6 +1142,9 @@ impl InlineAssistant {
|
||||
let mut to_remove = decorations.removed_line_block_ids;
|
||||
to_remove.insert(decorations.prompt_block_id);
|
||||
to_remove.insert(decorations.end_block_id);
|
||||
if let Some(tool_description_block_id) = decorations.model_explanation {
|
||||
to_remove.insert(tool_description_block_id);
|
||||
}
|
||||
editor.remove_blocks(to_remove, None, cx);
|
||||
});
|
||||
|
||||
@@ -1553,6 +1585,27 @@ impl InlineAssistant {
|
||||
.map(InlineAssistTarget::Terminal)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_completion_receiver(
|
||||
&mut self,
|
||||
sender: mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>,
|
||||
) {
|
||||
self._inline_assistant_completions = Some(sender);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn get_codegen(
|
||||
&mut self,
|
||||
assist_id: InlineAssistId,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<CodegenAlternative>> {
|
||||
self.assists.get(&assist_id).map(|inline_assist| {
|
||||
inline_assist
|
||||
.codegen
|
||||
.update(cx, |codegen, _cx| codegen.active_alternative().clone())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct EditorInlineAssists {
|
||||
@@ -1686,6 +1739,7 @@ impl InlineAssist {
|
||||
editor: &Entity<Editor>,
|
||||
prompt_editor: &Entity<PromptEditor<BufferCodegen>>,
|
||||
prompt_block_id: CustomBlockId,
|
||||
tool_description_block_id: CustomBlockId,
|
||||
end_block_id: CustomBlockId,
|
||||
range: Range<Anchor>,
|
||||
codegen: Entity<BufferCodegen>,
|
||||
@@ -1700,7 +1754,8 @@ impl InlineAssist {
|
||||
decorations: Some(InlineAssistDecorations {
|
||||
prompt_block_id,
|
||||
prompt_editor: prompt_editor.clone(),
|
||||
removed_line_block_ids: HashSet::default(),
|
||||
removed_line_block_ids: Default::default(),
|
||||
model_explanation: Some(tool_description_block_id),
|
||||
end_block_id,
|
||||
}),
|
||||
range,
|
||||
@@ -1804,6 +1859,7 @@ struct InlineAssistDecorations {
|
||||
prompt_block_id: CustomBlockId,
|
||||
prompt_editor: Entity<PromptEditor<BufferCodegen>>,
|
||||
removed_line_block_ids: HashSet<CustomBlockId>,
|
||||
model_explanation: Option<CustomBlockId>,
|
||||
end_block_id: CustomBlockId,
|
||||
}
|
||||
|
||||
@@ -1971,8 +2027,10 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[cfg(any(test, feature = "unit-eval"))]
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub mod test {
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::HistoryStore;
|
||||
@@ -1983,7 +2041,6 @@ pub mod test {
|
||||
use futures::channel::mpsc;
|
||||
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
|
||||
use language::Buffer;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use smol::stream::StreamExt as _;
|
||||
@@ -1992,13 +2049,32 @@ pub mod test {
|
||||
|
||||
use crate::InlineAssistant;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InlineAssistantOutput {
|
||||
Success {
|
||||
completion: Option<String>,
|
||||
description: Option<String>,
|
||||
full_buffer_text: String,
|
||||
},
|
||||
Failure {
|
||||
failure: String,
|
||||
},
|
||||
// These fields are used for logging
|
||||
#[allow(unused)]
|
||||
Malformed {
|
||||
completion: Option<String>,
|
||||
description: Option<String>,
|
||||
failure: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn run_inline_assistant_test<SetupF, TestF>(
|
||||
base_buffer: String,
|
||||
prompt: String,
|
||||
setup: SetupF,
|
||||
test: TestF,
|
||||
cx: &mut TestAppContext,
|
||||
) -> String
|
||||
) -> InlineAssistantOutput
|
||||
where
|
||||
SetupF: FnOnce(&mut gpui::VisualTestContext),
|
||||
TestF: FnOnce(&mut gpui::VisualTestContext),
|
||||
@@ -2011,8 +2087,7 @@ pub mod test {
|
||||
cx.set_http_client(http);
|
||||
Client::production(cx)
|
||||
});
|
||||
let mut inline_assistant =
|
||||
InlineAssistant::new(fs.clone(), prompt_builder, client.telemetry().clone());
|
||||
let mut inline_assistant = InlineAssistant::new(fs.clone(), prompt_builder);
|
||||
|
||||
let (tx, mut completion_rx) = mpsc::unbounded();
|
||||
inline_assistant.set_completion_receiver(tx);
|
||||
@@ -2091,39 +2166,217 @@ pub mod test {
|
||||
|
||||
test(cx);
|
||||
|
||||
cx.executor()
|
||||
.block_test(async { completion_rx.next().await });
|
||||
let assist_id = cx
|
||||
.executor()
|
||||
.block_test(async { completion_rx.next().await })
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
buffer.read_with(cx, |buffer, _| buffer.text())
|
||||
}
|
||||
let (completion, description, failure) = cx.update(|_, cx| {
|
||||
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
||||
let codegen = inline_assistant.get_codegen(assist_id, cx).unwrap();
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn test_inline_assistant(
|
||||
base_buffer: &'static str,
|
||||
llm_output: &'static str,
|
||||
cx: &mut TestAppContext,
|
||||
) -> String {
|
||||
run_inline_assistant_test(
|
||||
base_buffer.to_string(),
|
||||
"Prompt doesn't matter because we're using a fake model".to_string(),
|
||||
|cx| {
|
||||
cx.update(|_, cx| LanguageModelRegistry::test(cx));
|
||||
},
|
||||
|cx| {
|
||||
let fake_model = cx.update(|_, cx| {
|
||||
LanguageModelRegistry::global(cx)
|
||||
.update(cx, |registry, _| registry.fake_model())
|
||||
});
|
||||
let fake = fake_model.as_fake();
|
||||
let completion = codegen.read(cx).current_completion();
|
||||
let description = codegen.read(cx).current_description();
|
||||
let failure = codegen.read(cx).current_failure();
|
||||
|
||||
// let fake = fake_model;
|
||||
fake.send_last_completion_stream_text_chunk(llm_output.to_string());
|
||||
fake.end_last_completion_stream();
|
||||
(completion, description, failure)
|
||||
})
|
||||
});
|
||||
|
||||
// Run again to process the model's response
|
||||
cx.run_until_parked();
|
||||
},
|
||||
cx,
|
||||
)
|
||||
if failure.is_some() && (completion.is_some() || description.is_some()) {
|
||||
InlineAssistantOutput::Malformed {
|
||||
completion,
|
||||
description,
|
||||
failure,
|
||||
}
|
||||
} else if let Some(failure) = failure {
|
||||
InlineAssistantOutput::Failure { failure }
|
||||
} else {
|
||||
InlineAssistantOutput::Success {
|
||||
completion,
|
||||
description,
|
||||
full_buffer_text: buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "unit-eval"))]
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub mod evals {
|
||||
use std::str::FromStr;
|
||||
|
||||
use eval_utils::{EvalOutput, NoProcessor};
|
||||
use gpui::TestAppContext;
|
||||
use language_model::{LanguageModelRegistry, SelectedModel};
|
||||
|
||||
|
||||
use crate::inline_assistant::test::{InlineAssistantOutput, run_inline_assistant_test};
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||
fn eval_single_cursor_edit() {
|
||||
run_eval(
|
||||
20,
|
||||
1.0,
|
||||
"Rename this variable to buffer_text".to_string(),
|
||||
indoc::indoc! {"
|
||||
struct EvalExampleStruct {
|
||||
text: Strˇing,
|
||||
prompt: String,
|
||||
}
|
||||
"}
|
||||
.to_string(),
|
||||
exact_buffer_match(indoc::indoc! {"
|
||||
struct EvalExampleStruct {
|
||||
buffer_text: String,
|
||||
prompt: String,
|
||||
}
|
||||
"}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||
fn eval_cant_do() {
|
||||
run_eval(
|
||||
20,
|
||||
0.95,
|
||||
"Rename the struct to EvalExampleStructNope",
|
||||
indoc::indoc! {"
|
||||
struct EvalExampleStruct {
|
||||
text: Strˇing,
|
||||
prompt: String,
|
||||
}
|
||||
"},
|
||||
uncertain_output,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||
fn eval_unclear() {
|
||||
run_eval(
|
||||
20,
|
||||
0.95,
|
||||
"Make exactly the change I want you to make",
|
||||
indoc::indoc! {"
|
||||
struct EvalExampleStruct {
|
||||
text: Strˇing,
|
||||
prompt: String,
|
||||
}
|
||||
"},
|
||||
uncertain_output,
|
||||
);
|
||||
}
|
||||
|
||||
fn run_eval(
|
||||
iterations: usize,
|
||||
expected_pass_ratio: f32,
|
||||
prompt: impl Into<String>,
|
||||
buffer: impl Into<String>,
|
||||
judge: impl Fn(InlineAssistantOutput) -> eval_utils::EvalOutput<()> + Send + Sync + 'static,
|
||||
) {
|
||||
let buffer = buffer.into();
|
||||
let prompt = prompt.into();
|
||||
|
||||
eval_utils::eval(iterations, expected_pass_ratio, NoProcessor, move || {
|
||||
let dispatcher = gpui::TestDispatcher::new(rand::random());
|
||||
let mut cx = TestAppContext::build(dispatcher, None);
|
||||
cx.skip_drawing();
|
||||
|
||||
let output = run_inline_assistant_test(
|
||||
buffer.clone(),
|
||||
prompt.clone(),
|
||||
|cx| {
|
||||
// Reconfigure to use a real model instead of the fake one
|
||||
let model_name = std::env::var("ZED_AGENT_MODEL")
|
||||
.unwrap_or("anthropic/claude-sonnet-4-latest".into());
|
||||
|
||||
let selected_model = SelectedModel::from_str(&model_name)
|
||||
.expect("Invalid model format. Use 'provider/model-id'");
|
||||
|
||||
log::info!("Selected model: {selected_model:?}");
|
||||
|
||||
cx.update(|_, cx| {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_inline_assistant_model(Some(&selected_model), cx);
|
||||
});
|
||||
});
|
||||
},
|
||||
|_cx| {
|
||||
log::info!("Waiting for actual response from the LLM...");
|
||||
},
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.quit();
|
||||
|
||||
judge(output)
|
||||
});
|
||||
}
|
||||
|
||||
fn uncertain_output(output: InlineAssistantOutput) -> EvalOutput<()> {
|
||||
match &output {
|
||||
o @ InlineAssistantOutput::Success {
|
||||
completion,
|
||||
description,
|
||||
..
|
||||
} => {
|
||||
if description.is_some() && completion.is_none() {
|
||||
EvalOutput::passed(format!(
|
||||
"Assistant produced no completion, but a description:\n{}",
|
||||
description.as_ref().unwrap()
|
||||
))
|
||||
} else {
|
||||
EvalOutput::failed(format!("Assistant produced a completion:\n{:?}", o))
|
||||
}
|
||||
}
|
||||
InlineAssistantOutput::Failure {
|
||||
failure: error_message,
|
||||
} => EvalOutput::passed(format!(
|
||||
"Assistant produced a failure message: {}",
|
||||
error_message
|
||||
)),
|
||||
o @ InlineAssistantOutput::Malformed { .. } => {
|
||||
EvalOutput::failed(format!("Assistant produced a malformed response:\n{:?}", o))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exact_buffer_match(
|
||||
correct_output: impl Into<String>,
|
||||
) -> impl Fn(InlineAssistantOutput) -> EvalOutput<()> {
|
||||
let correct_output = correct_output.into();
|
||||
move |output| match output {
|
||||
InlineAssistantOutput::Success {
|
||||
description,
|
||||
full_buffer_text,
|
||||
..
|
||||
} => {
|
||||
if full_buffer_text == correct_output && description.is_none() {
|
||||
EvalOutput::passed("Assistant output matches")
|
||||
} else if full_buffer_text == correct_output {
|
||||
EvalOutput::failed(format!(
|
||||
"Assistant output produced an unescessary description description:\n{:?}",
|
||||
description
|
||||
))
|
||||
} else {
|
||||
EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output:\n{:?}\ndescription:\n{:?}",
|
||||
full_buffer_text, description
|
||||
))
|
||||
}
|
||||
}
|
||||
o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ use editor::{
|
||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||
actions::{MoveDown, MoveUp},
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt, InlineAssistantUseToolFeatureFlag};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Subscription, TextStyle, WeakEntity, Window,
|
||||
AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Subscription, TextStyle, TextStyleRefinement, WeakEntity, Window, actions,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
@@ -25,11 +27,13 @@ use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use workspace::Workspace;
|
||||
use uuid::Uuid;
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::{Toast, Workspace};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
use crate::agent_model_selector::AgentModelSelector;
|
||||
use crate::buffer_codegen::BufferCodegen;
|
||||
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative};
|
||||
use crate::completion_provider::{
|
||||
PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
|
||||
};
|
||||
@@ -38,6 +42,19 @@ use crate::mention_set::{MentionSet, crease_for_mention};
|
||||
use crate::terminal_codegen::TerminalCodegen;
|
||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
||||
|
||||
actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
|
||||
|
||||
enum CompletionState {
|
||||
Pending,
|
||||
Generated { completion_text: Option<String> },
|
||||
Rated,
|
||||
}
|
||||
|
||||
struct SessionState {
|
||||
session_id: Uuid,
|
||||
completion: CompletionState,
|
||||
}
|
||||
|
||||
pub struct PromptEditor<T> {
|
||||
pub editor: Entity<Editor>,
|
||||
mode: PromptEditorMode,
|
||||
@@ -53,6 +70,7 @@ pub struct PromptEditor<T> {
|
||||
_codegen_subscription: Subscription,
|
||||
editor_subscriptions: Vec<Subscription>,
|
||||
show_rate_limit_notice: bool,
|
||||
session_state: SessionState,
|
||||
_phantom: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
@@ -65,7 +83,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
|
||||
const RIGHT_PADDING: Pixels = px(9.);
|
||||
|
||||
let (left_gutter_width, right_padding) = match &self.mode {
|
||||
let (left_gutter_width, right_padding, explanation) = match &self.mode {
|
||||
PromptEditorMode::Buffer {
|
||||
id: _,
|
||||
codegen,
|
||||
@@ -83,17 +101,23 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
|
||||
let right_padding = editor_margins.right + RIGHT_PADDING;
|
||||
|
||||
(left_gutter_width, right_padding)
|
||||
let active_alternative = codegen.active_alternative().read(cx);
|
||||
let explanation = active_alternative
|
||||
.description
|
||||
.clone()
|
||||
.or_else(|| active_alternative.failure.clone());
|
||||
|
||||
(left_gutter_width, right_padding, explanation)
|
||||
}
|
||||
PromptEditorMode::Terminal { .. } => {
|
||||
// Give the equivalent of the same left-padding that we're using on the right
|
||||
(Pixels::from(40.0), Pixels::from(24.))
|
||||
(Pixels::from(40.0), Pixels::from(24.), None)
|
||||
}
|
||||
};
|
||||
|
||||
let bottom_padding = match &self.mode {
|
||||
PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
|
||||
PromptEditorMode::Terminal { .. } => rems_from_px(8.0),
|
||||
PromptEditorMode::Terminal { .. } => rems_from_px(4.0),
|
||||
};
|
||||
|
||||
buttons.extend(self.render_buttons(window, cx));
|
||||
@@ -111,22 +135,33 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
this.trigger_completion_menu(window, cx);
|
||||
}));
|
||||
|
||||
let markdown = window.use_state(cx, |_, cx| Markdown::new("".into(), None, None, cx));
|
||||
|
||||
if let Some(explanation) = &explanation {
|
||||
markdown.update(cx, |markdown, cx| {
|
||||
markdown.reset(SharedString::from(explanation), cx);
|
||||
});
|
||||
}
|
||||
|
||||
let explanation_label = self
|
||||
.render_markdown(markdown, markdown_style(window, cx))
|
||||
.into_any_element();
|
||||
|
||||
v_flex()
|
||||
.key_context("PromptEditor")
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.block_mouse_except_scroll()
|
||||
.gap_0p5()
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.size_full()
|
||||
.pt_0p5()
|
||||
.pb(bottom_padding)
|
||||
.pr(right_padding)
|
||||
.gap_0p5()
|
||||
.justify_center()
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
@@ -135,18 +170,20 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::move_down))
|
||||
.on_action(cx.listener(Self::thumbs_up))
|
||||
.on_action(cx.listener(Self::thumbs_down))
|
||||
.capture_action(cx.listener(Self::cycle_prev))
|
||||
.capture_action(cx.listener(Self::cycle_next))
|
||||
.child(
|
||||
WithRemSize::new(ui_font_size)
|
||||
.h_full()
|
||||
.w(left_gutter_width)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.h_full()
|
||||
.w(left_gutter_width)
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.gap_1()
|
||||
.child(self.render_close_button(cx))
|
||||
.map(|el| {
|
||||
let CodegenStatus::Error(error) = self.codegen_status(cx) else {
|
||||
@@ -177,26 +214,83 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(add_context_button)
|
||||
.child(self.model_selector.clone())
|
||||
.children(buttons),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
WithRemSize::new(ui_font_size)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.child(h_flex().flex_shrink_0().w(left_gutter_width))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_1()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.child(add_context_button)
|
||||
.child(self.model_selector.clone()),
|
||||
),
|
||||
)
|
||||
.when_some(explanation, |this, _| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.child(div().w(left_gutter_width + px(6.)))
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.min_w_0()
|
||||
.pt(rems_from_px(3.))
|
||||
.pl_0p5()
|
||||
.flex_1()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(explanation_label),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
let mut text_style = window.text_style();
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
color: Some(colors.text),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
MarkdownStyle {
|
||||
base_text_style: text_style.clone(),
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: colors.element_selection_background,
|
||||
heading_level_styles: Some(HeadingLevelStyles {
|
||||
h1: Some(TextStyleRefinement {
|
||||
font_size: Some(rems(1.15).into()),
|
||||
..Default::default()
|
||||
}),
|
||||
h2: Some(TextStyleRefinement {
|
||||
font_size: Some(rems(1.1).into()),
|
||||
..Default::default()
|
||||
}),
|
||||
h3: Some(TextStyleRefinement {
|
||||
font_size: Some(rems(1.05).into()),
|
||||
..Default::default()
|
||||
}),
|
||||
h4: Some(TextStyleRefinement {
|
||||
font_size: Some(rems(1.).into()),
|
||||
..Default::default()
|
||||
}),
|
||||
h5: Some(TextStyleRefinement {
|
||||
font_size: Some(rems(0.95).into()),
|
||||
..Default::default()
|
||||
}),
|
||||
h6: Some(TextStyleRefinement {
|
||||
font_size: Some(rems(0.875).into()),
|
||||
..Default::default()
|
||||
}),
|
||||
}),
|
||||
inline_code: TextStyleRefinement {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.buffer_font.features.clone()),
|
||||
background_color: Some(colors.editor_foreground.opacity(0.08)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +448,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}
|
||||
|
||||
self.edited_since_done = true;
|
||||
self.session_state.completion = CompletionState::Pending;
|
||||
cx.notify();
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
@@ -425,22 +520,207 @@ impl<T: 'static> PromptEditor<T> {
|
||||
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.codegen_status(cx) {
|
||||
CodegenStatus::Idle => {
|
||||
self.fire_started_telemetry(cx);
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}
|
||||
CodegenStatus::Pending => {}
|
||||
CodegenStatus::Done => {
|
||||
if self.edited_since_done {
|
||||
self.fire_started_telemetry(cx);
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
} else {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
|
||||
}
|
||||
}
|
||||
CodegenStatus::Error(_) => {
|
||||
self.fire_started_telemetry(cx);
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fire_started_telemetry(&self, cx: &Context<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let model_telemetry_id = model.model.telemetry_id();
|
||||
let model_provider_id = model.provider.id().to_string();
|
||||
|
||||
let (kind, language_name) = match &self.mode {
|
||||
PromptEditorMode::Buffer { codegen, .. } => {
|
||||
let codegen = codegen.read(cx);
|
||||
(
|
||||
"inline",
|
||||
codegen.language_name(cx).map(|name| name.to_string()),
|
||||
)
|
||||
}
|
||||
PromptEditorMode::Terminal { .. } => ("inline_terminal", None),
|
||||
};
|
||||
|
||||
telemetry::event!(
|
||||
"Assistant Started",
|
||||
session_id = self.session_state.session_id.to_string(),
|
||||
kind = kind,
|
||||
phase = "started",
|
||||
model = model_telemetry_id,
|
||||
model_provider = model_provider_id,
|
||||
language_name = language_name,
|
||||
);
|
||||
}
|
||||
|
||||
fn thumbs_up(&mut self, _: &ThumbsUpResult, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
match &self.session_state.completion {
|
||||
CompletionState::Pending => {
|
||||
self.toast("Can't rate, still generating...", None, cx);
|
||||
return;
|
||||
}
|
||||
CompletionState::Rated => {
|
||||
self.toast(
|
||||
"Already rated this completion",
|
||||
Some(self.session_state.session_id),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
CompletionState::Generated { completion_text } => {
|
||||
let model_info = self.model_selector.read(cx).active_model(cx);
|
||||
let (model_id, use_streaming_tools) = {
|
||||
let Some(configured_model) = model_info else {
|
||||
self.toast("No configured model", None, cx);
|
||||
return;
|
||||
};
|
||||
(
|
||||
configured_model.model.telemetry_id(),
|
||||
CodegenAlternative::use_streaming_tools(
|
||||
configured_model.model.as_ref(),
|
||||
cx,
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
let selected_text = match &self.mode {
|
||||
PromptEditorMode::Buffer { codegen, .. } => {
|
||||
codegen.read(cx).selected_text(cx).map(|s| s.to_string())
|
||||
}
|
||||
PromptEditorMode::Terminal { .. } => None,
|
||||
};
|
||||
|
||||
let prompt = self.editor.read(cx).text(cx);
|
||||
|
||||
let kind = match &self.mode {
|
||||
PromptEditorMode::Buffer { .. } => "inline",
|
||||
PromptEditorMode::Terminal { .. } => "inline_terminal",
|
||||
};
|
||||
|
||||
telemetry::event!(
|
||||
"Inline Assistant Rated",
|
||||
rating = "positive",
|
||||
session_id = self.session_state.session_id.to_string(),
|
||||
kind = kind,
|
||||
model = model_id,
|
||||
prompt = prompt,
|
||||
completion = completion_text,
|
||||
selected_text = selected_text,
|
||||
use_streaming_tools
|
||||
);
|
||||
|
||||
self.session_state.completion = CompletionState::Rated;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn thumbs_down(&mut self, _: &ThumbsDownResult, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
match &self.session_state.completion {
|
||||
CompletionState::Pending => {
|
||||
self.toast("Can't rate, still generating...", None, cx);
|
||||
return;
|
||||
}
|
||||
CompletionState::Rated => {
|
||||
self.toast(
|
||||
"Already rated this completion",
|
||||
Some(self.session_state.session_id),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
CompletionState::Generated { completion_text } => {
|
||||
let model_info = self.model_selector.read(cx).active_model(cx);
|
||||
let (model_telemetry_id, use_streaming_tools) = {
|
||||
let Some(configured_model) = model_info else {
|
||||
self.toast("No configured model", None, cx);
|
||||
return;
|
||||
};
|
||||
(
|
||||
configured_model.model.telemetry_id(),
|
||||
CodegenAlternative::use_streaming_tools(
|
||||
configured_model.model.as_ref(),
|
||||
cx,
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
let selected_text = match &self.mode {
|
||||
PromptEditorMode::Buffer { codegen, .. } => {
|
||||
codegen.read(cx).selected_text(cx).map(|s| s.to_string())
|
||||
}
|
||||
PromptEditorMode::Terminal { .. } => None,
|
||||
};
|
||||
|
||||
let prompt = self.editor.read(cx).text(cx);
|
||||
|
||||
let kind = match &self.mode {
|
||||
PromptEditorMode::Buffer { .. } => "inline",
|
||||
PromptEditorMode::Terminal { .. } => "inline_terminal",
|
||||
};
|
||||
|
||||
telemetry::event!(
|
||||
"Inline Assistant Rated",
|
||||
rating = "negative",
|
||||
session_id = self.session_state.session_id.to_string(),
|
||||
kind = kind,
|
||||
model = model_telemetry_id,
|
||||
prompt = prompt,
|
||||
completion = completion_text,
|
||||
selected_text = selected_text,
|
||||
use_streaming_tools
|
||||
);
|
||||
|
||||
self.session_state.completion = CompletionState::Rated;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toast(&mut self, msg: &str, uuid: Option<Uuid>, cx: &mut Context<'_, PromptEditor<T>>) {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
enum InlinePromptRating {}
|
||||
workspace.show_toast(
|
||||
{
|
||||
let mut toast = Toast::new(
|
||||
NotificationId::unique::<InlinePromptRating>(),
|
||||
msg.to_string(),
|
||||
)
|
||||
.autohide();
|
||||
|
||||
if let Some(uuid) = uuid {
|
||||
toast = toast.on_click("Click to copy rating ID", move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(uuid.to_string()));
|
||||
});
|
||||
};
|
||||
|
||||
toast
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(ix) = self.prompt_history_ix {
|
||||
if ix > 0 {
|
||||
@@ -546,6 +826,9 @@ impl<T: 'static> PromptEditor<T> {
|
||||
.into_any_element(),
|
||||
]
|
||||
} else {
|
||||
let show_rating_buttons = cx.has_flag::<InlineAssistantUseToolFeatureFlag>();
|
||||
let rated = matches!(self.session_state.completion, CompletionState::Rated);
|
||||
|
||||
let accept = IconButton::new("accept", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
@@ -557,25 +840,59 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}))
|
||||
.into_any_element();
|
||||
|
||||
match &self.mode {
|
||||
PromptEditorMode::Terminal { .. } => vec![
|
||||
accept,
|
||||
IconButton::new("confirm", IconName::PlayFilled)
|
||||
.icon_color(Color::Info)
|
||||
let mut buttons = Vec::new();
|
||||
|
||||
if show_rating_buttons {
|
||||
buttons.push(
|
||||
IconButton::new("thumbs-down", IconName::ThumbsDown)
|
||||
.icon_color(if rated { Color::Muted } else { Color::Default })
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|_window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Execute Generated Command",
|
||||
&menu::SecondaryConfirm,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
|
||||
.disabled(rated)
|
||||
.tooltip(Tooltip::text("Bad result"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_down(&ThumbsDownResult, window, cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
],
|
||||
PromptEditorMode::Buffer { .. } => vec![accept],
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
IconButton::new("thumbs-up", IconName::ThumbsUp)
|
||||
.icon_color(if rated { Color::Muted } else { Color::Default })
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(rated)
|
||||
.tooltip(Tooltip::text("Good result"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_up(&ThumbsUpResult, window, cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
|
||||
buttons.push(accept);
|
||||
|
||||
match &self.mode {
|
||||
PromptEditorMode::Terminal { .. } => {
|
||||
buttons.push(
|
||||
IconButton::new("confirm", IconName::PlayFilled)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|_window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Execute Generated Command",
|
||||
&menu::SecondaryConfirm,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested {
|
||||
execute: true,
|
||||
});
|
||||
}))
|
||||
.into_any_element(),
|
||||
);
|
||||
buttons
|
||||
}
|
||||
PromptEditorMode::Buffer { .. } => buttons,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -759,6 +1076,10 @@ impl<T: 'static> PromptEditor<T> {
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
|
||||
MarkdownElement::new(markdown, style)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum PromptEditorMode {
|
||||
@@ -830,6 +1151,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_buffer: Entity<MultiBuffer>,
|
||||
codegen: Entity<BufferCodegen>,
|
||||
session_id: Uuid,
|
||||
fs: Arc<dyn Fs>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
@@ -900,6 +1222,10 @@ impl PromptEditor<BufferCodegen> {
|
||||
editor_subscriptions: Vec::new(),
|
||||
show_rate_limit_notice: false,
|
||||
mode,
|
||||
session_state: SessionState {
|
||||
session_id,
|
||||
completion: CompletionState::Pending,
|
||||
},
|
||||
_phantom: Default::default(),
|
||||
};
|
||||
|
||||
@@ -910,7 +1236,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
|
||||
fn handle_codegen_changed(
|
||||
&mut self,
|
||||
_: Entity<BufferCodegen>,
|
||||
codegen: Entity<BufferCodegen>,
|
||||
cx: &mut Context<PromptEditor<BufferCodegen>>,
|
||||
) {
|
||||
match self.codegen_status(cx) {
|
||||
@@ -919,10 +1245,15 @@ impl PromptEditor<BufferCodegen> {
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
}
|
||||
CodegenStatus::Pending => {
|
||||
self.session_state.completion = CompletionState::Pending;
|
||||
self.editor
|
||||
.update(cx, |editor, _| editor.set_read_only(true));
|
||||
}
|
||||
CodegenStatus::Done => {
|
||||
let completion = codegen.read(cx).active_completion(cx);
|
||||
self.session_state.completion = CompletionState::Generated {
|
||||
completion_text: completion,
|
||||
};
|
||||
self.edited_since_done = false;
|
||||
self.editor
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
@@ -978,6 +1309,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_buffer: Entity<MultiBuffer>,
|
||||
codegen: Entity<TerminalCodegen>,
|
||||
session_id: Uuid,
|
||||
fs: Arc<dyn Fs>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
@@ -1043,6 +1375,10 @@ impl PromptEditor<TerminalCodegen> {
|
||||
editor_subscriptions: Vec::new(),
|
||||
mode,
|
||||
show_rate_limit_notice: false,
|
||||
session_state: SessionState {
|
||||
session_id,
|
||||
completion: CompletionState::Pending,
|
||||
},
|
||||
_phantom: Default::default(),
|
||||
};
|
||||
this.count_lines(cx);
|
||||
@@ -1075,17 +1411,21 @@ impl PromptEditor<TerminalCodegen> {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
|
||||
fn handle_codegen_changed(&mut self, codegen: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
|
||||
match &self.codegen().read(cx).status {
|
||||
CodegenStatus::Idle => {
|
||||
self.editor
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
}
|
||||
CodegenStatus::Pending => {
|
||||
self.session_state.completion = CompletionState::Pending;
|
||||
self.editor
|
||||
.update(cx, |editor, _| editor.set_read_only(true));
|
||||
}
|
||||
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
self.session_state.completion = CompletionState::Generated {
|
||||
completion_text: codegen.read(cx).completion(),
|
||||
};
|
||||
self.edited_since_done = false;
|
||||
self.editor
|
||||
.update(cx, |editor, _| editor.set_read_only(false));
|
||||
|
||||
@@ -542,7 +542,7 @@ impl PickerDelegate for ProfilePickerDelegate {
|
||||
let is_active = active_id == candidate.id;
|
||||
|
||||
Some(
|
||||
ListItem::new(SharedString::from(candidate.id.0.clone()))
|
||||
ListItem::new(candidate.id.0.clone())
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use client::telemetry::Telemetry;
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, report_assistant_event,
|
||||
};
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequest};
|
||||
use std::time::Instant;
|
||||
use terminal::Terminal;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct TerminalCodegen {
|
||||
pub status: CodegenStatus,
|
||||
pub telemetry: Option<Arc<Telemetry>>,
|
||||
terminal: Entity<Terminal>,
|
||||
generation: Task<()>,
|
||||
pub message_id: Option<String>,
|
||||
transaction: Option<TerminalTransaction>,
|
||||
session_id: Uuid,
|
||||
}
|
||||
|
||||
impl EventEmitter<CodegenEvent> for TerminalCodegen {}
|
||||
|
||||
impl TerminalCodegen {
|
||||
pub fn new(terminal: Entity<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
|
||||
pub fn new(terminal: Entity<Terminal>, session_id: Uuid) -> Self {
|
||||
Self {
|
||||
terminal,
|
||||
telemetry,
|
||||
status: CodegenStatus::Idle,
|
||||
generation: Task::ready(()),
|
||||
message_id: None,
|
||||
transaction: None,
|
||||
session_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_id(&self) -> Uuid {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt_task: Task<LanguageModelRequest>, cx: &mut Context<Self>) {
|
||||
let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
@@ -39,15 +40,15 @@ impl TerminalCodegen {
|
||||
return;
|
||||
};
|
||||
|
||||
let model_api_key = model.api_key(cx);
|
||||
let http_client = cx.http_client();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let anthropic_reporter = language_model::AnthropicEventReporter::new(&model, cx);
|
||||
let session_id = self.session_id;
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let model_provider_id = model.provider_id().to_string();
|
||||
|
||||
self.status = CodegenStatus::Pending;
|
||||
self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
|
||||
self.generation = cx.spawn(async move |this, cx| {
|
||||
let prompt = prompt_task.await;
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let model_provider_id = model.provider_id();
|
||||
let response = model.stream_completion_text(prompt, cx).await;
|
||||
let generate = async {
|
||||
let message_id = response
|
||||
@@ -59,7 +60,7 @@ impl TerminalCodegen {
|
||||
|
||||
let task = cx.background_spawn({
|
||||
let message_id = message_id.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
let anthropic_reporter = anthropic_reporter.clone();
|
||||
async move {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
@@ -79,24 +80,27 @@ impl TerminalCodegen {
|
||||
let result = task.await;
|
||||
|
||||
let error_message = result.as_ref().err().map(|error| error.to_string());
|
||||
report_assistant_event(
|
||||
AssistantEventData {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::InlineTerminal,
|
||||
message_id,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model_telemetry_id,
|
||||
model_provider: model_provider_id.to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name: None,
|
||||
},
|
||||
telemetry,
|
||||
http_client,
|
||||
model_api_key,
|
||||
&executor,
|
||||
|
||||
telemetry::event!(
|
||||
"Assistant Responded",
|
||||
session_id = session_id.to_string(),
|
||||
kind = "inline_terminal",
|
||||
phase = "response",
|
||||
model = model_telemetry_id,
|
||||
model_provider = model_provider_id,
|
||||
language_name = Option::<&str>::None,
|
||||
message_id = message_id,
|
||||
response_latency = response_latency,
|
||||
error_message = error_message,
|
||||
);
|
||||
|
||||
anthropic_reporter.report(language_model::AnthropicEventData {
|
||||
completion_type: language_model::AnthropicCompletionType::Terminal,
|
||||
event: language_model::AnthropicEventType::Response,
|
||||
language_name: None,
|
||||
message_id,
|
||||
});
|
||||
|
||||
result?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
@@ -135,6 +139,12 @@ impl TerminalCodegen {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn completion(&self) -> Option<String> {
|
||||
self.transaction
|
||||
.as_ref()
|
||||
.map(|transaction| transaction.completion.clone())
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, cx: &mut Context<Self>) {
|
||||
self.status = CodegenStatus::Done;
|
||||
self.generation = Task::ready(());
|
||||
@@ -167,27 +177,32 @@ pub const CLEAR_INPUT: &str = "\x03";
|
||||
const CARRIAGE_RETURN: &str = "\x0d";
|
||||
|
||||
struct TerminalTransaction {
|
||||
completion: String,
|
||||
terminal: Entity<Terminal>,
|
||||
}
|
||||
|
||||
impl TerminalTransaction {
|
||||
pub fn start(terminal: Entity<Terminal>) -> Self {
|
||||
Self { terminal }
|
||||
Self {
|
||||
completion: String::new(),
|
||||
terminal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, hunk: String, cx: &mut App) {
|
||||
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
|
||||
let input = Self::sanitize_input(hunk);
|
||||
self.completion.push_str(&input);
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(input.into_bytes()));
|
||||
}
|
||||
|
||||
pub fn undo(&self, cx: &mut App) {
|
||||
pub fn undo(self, cx: &mut App) {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.as_bytes()));
|
||||
}
|
||||
|
||||
pub fn complete(&self, cx: &mut App) {
|
||||
pub fn complete(self, cx: &mut App) {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _| terminal.input(CARRIAGE_RETURN.as_bytes()));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::{
|
||||
use agent::HistoryStore;
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
@@ -17,24 +17,19 @@ use gpui::{App, Entity, Focusable, Global, Subscription, Task, UpdateGlobal, Wea
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
Role, report_assistant_event,
|
||||
Role, report_anthropic_event,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::{PromptBuilder, PromptStore};
|
||||
use std::sync::Arc;
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use terminal_view::TerminalView;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
|
||||
pub fn init(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>, cx: &mut App) {
|
||||
cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder));
|
||||
}
|
||||
|
||||
const DEFAULT_CONTEXT_LINES: usize = 50;
|
||||
@@ -44,7 +39,6 @@ pub struct TerminalInlineAssistant {
|
||||
next_assist_id: TerminalInlineAssistId,
|
||||
assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
|
||||
prompt_history: VecDeque<String>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
}
|
||||
@@ -52,16 +46,11 @@ pub struct TerminalInlineAssistant {
|
||||
impl Global for TerminalInlineAssistant {}
|
||||
|
||||
impl TerminalInlineAssistant {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
) -> Self {
|
||||
pub fn new(fs: Arc<dyn Fs>, prompt_builder: Arc<PromptBuilder>) -> Self {
|
||||
Self {
|
||||
next_assist_id: TerminalInlineAssistId::default(),
|
||||
assists: HashMap::default(),
|
||||
prompt_history: VecDeque::default(),
|
||||
telemetry: Some(telemetry),
|
||||
fs,
|
||||
prompt_builder,
|
||||
}
|
||||
@@ -80,13 +69,14 @@ impl TerminalInlineAssistant {
|
||||
) {
|
||||
let terminal = terminal_view.read(cx).terminal().clone();
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
let session_id = Uuid::new_v4();
|
||||
let prompt_buffer = cx.new(|cx| {
|
||||
MultiBuffer::singleton(
|
||||
cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
||||
let codegen = cx.new(|_| TerminalCodegen::new(terminal, session_id));
|
||||
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
PromptEditor::new_terminal(
|
||||
@@ -94,6 +84,7 @@ impl TerminalInlineAssistant {
|
||||
self.prompt_history.clone(),
|
||||
prompt_buffer.clone(),
|
||||
codegen,
|
||||
session_id,
|
||||
self.fs.clone(),
|
||||
thread_store.clone(),
|
||||
prompt_store.clone(),
|
||||
@@ -309,27 +300,45 @@ impl TerminalInlineAssistant {
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
{
|
||||
let codegen = assist.codegen.read(cx);
|
||||
let executor = cx.background_executor().clone();
|
||||
report_assistant_event(
|
||||
AssistantEventData {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::InlineTerminal,
|
||||
message_id: codegen.message_id.clone(),
|
||||
phase: if undo {
|
||||
AssistantPhase::Rejected
|
||||
} else {
|
||||
AssistantPhase::Accepted
|
||||
},
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
let session_id = codegen.session_id();
|
||||
let message_id = codegen.message_id.clone();
|
||||
let model_telemetry_id = model.telemetry_id();
|
||||
let model_provider_id = model.provider_id().to_string();
|
||||
|
||||
let (phase, event_type, anthropic_event_type) = if undo {
|
||||
(
|
||||
"rejected",
|
||||
"Assistant Response Rejected",
|
||||
language_model::AnthropicEventType::Reject,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"accepted",
|
||||
"Assistant Response Accepted",
|
||||
language_model::AnthropicEventType::Accept,
|
||||
)
|
||||
};
|
||||
|
||||
// Fire Zed telemetry
|
||||
telemetry::event!(
|
||||
event_type,
|
||||
kind = "inline_terminal",
|
||||
phase = phase,
|
||||
model = model_telemetry_id,
|
||||
model_provider = model_provider_id,
|
||||
message_id = message_id,
|
||||
session_id = session_id,
|
||||
);
|
||||
|
||||
report_anthropic_event(
|
||||
&model,
|
||||
language_model::AnthropicEventData {
|
||||
completion_type: language_model::AnthropicCompletionType::Terminal,
|
||||
event: anthropic_event_type,
|
||||
language_name: None,
|
||||
message_id,
|
||||
},
|
||||
codegen.telemetry.clone(),
|
||||
cx.http_client(),
|
||||
model.api_key(cx),
|
||||
&executor,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1682,6 +1682,98 @@ impl TextThreadEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
.and_then(|entry| match entry {
|
||||
ClipboardEntry::String(text) => {
|
||||
text.metadata_json::<Vec<editor::ClipboardSelection>>()
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if has_file_context {
|
||||
if let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) =
|
||||
clipboard_item.entries().first()
|
||||
{
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
|
||||
editor.insert("\n", window, cx);
|
||||
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.stop_propagation();
|
||||
|
||||
let mut images = if let Some(item) = cx.read_from_clipboard() {
|
||||
@@ -3232,7 +3324,6 @@ mod tests {
|
||||
let mut text_thread = TextThread::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
|
||||
@@ -106,9 +106,6 @@ impl Render for AgentNotification {
|
||||
.font(ui_font)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_xl()
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(AgentNotificationEvent::Accepted);
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
|
||||
40
crates/agent_ui_v2/Cargo.toml
Normal file
40
crates/agent_ui_v2/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "agent_ui_v2"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/agent_ui_v2.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
agent_servers.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_ui.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_text_thread.workspace = true
|
||||
chrono.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
text.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
1
crates/agent_ui_v2/LICENSE-GPL
Symbolic link
1
crates/agent_ui_v2/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
LICENSE-GPL
|
||||
287
crates/agent_ui_v2/src/agent_thread_pane.rs
Normal file
287
crates/agent_ui_v2/src/agent_thread_pane.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
use agent::{HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use agent_ui::acp::AcpThreadView;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Entity, EventEmitter, Focusable, Pixels, SharedString, Subscription, WeakEntity, prelude::*,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::DockSide;
|
||||
use settings::Settings as _;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use ui::{Tab, Tooltip, prelude::*};
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{ClosePane, MinimizePane, UtilityPane, UtilityPanePosition},
|
||||
utility_pane::UtilityPaneSlot,
|
||||
};
|
||||
|
||||
pub const DEFAULT_UTILITY_PANE_WIDTH: Pixels = gpui::px(400.0);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum SerializedHistoryEntryId {
|
||||
AcpThread(String),
|
||||
TextThread(String),
|
||||
}
|
||||
|
||||
impl From<HistoryEntryId> for SerializedHistoryEntryId {
|
||||
fn from(id: HistoryEntryId) -> Self {
|
||||
match id {
|
||||
HistoryEntryId::AcpThread(session_id) => {
|
||||
SerializedHistoryEntryId::AcpThread(session_id.0.to_string())
|
||||
}
|
||||
HistoryEntryId::TextThread(path) => {
|
||||
SerializedHistoryEntryId::TextThread(path.to_string_lossy().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SerializedAgentThreadPane {
|
||||
pub expanded: bool,
|
||||
pub width: Option<Pixels>,
|
||||
pub thread_id: Option<SerializedHistoryEntryId>,
|
||||
}
|
||||
|
||||
pub enum AgentsUtilityPaneEvent {
|
||||
StateChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<AgentsUtilityPaneEvent> for AgentThreadPane {}
|
||||
impl EventEmitter<MinimizePane> for AgentThreadPane {}
|
||||
impl EventEmitter<ClosePane> for AgentThreadPane {}
|
||||
|
||||
struct ActiveThreadView {
|
||||
view: Entity<AcpThreadView>,
|
||||
thread_id: HistoryEntryId,
|
||||
_notify: Subscription,
|
||||
}
|
||||
|
||||
pub struct AgentThreadPane {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
expanded: bool,
|
||||
width: Option<Pixels>,
|
||||
thread_view: Option<ActiveThreadView>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
}
|
||||
|
||||
impl AgentThreadPane {
|
||||
pub fn new(workspace: WeakEntity<Workspace>, cx: &mut ui::Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
Self {
|
||||
focus_handle,
|
||||
expanded: false,
|
||||
width: None,
|
||||
thread_view: None,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn thread_id(&self) -> Option<HistoryEntryId> {
|
||||
self.thread_view.as_ref().map(|tv| tv.thread_id.clone())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> SerializedAgentThreadPane {
|
||||
SerializedAgentThreadPane {
|
||||
expanded: self.expanded,
|
||||
width: self.width,
|
||||
thread_id: self.thread_id().map(SerializedHistoryEntryId::from),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_thread(
|
||||
&mut self,
|
||||
entry: HistoryEntry,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let thread_id = entry.id();
|
||||
|
||||
let resume_thread = match &entry {
|
||||
HistoryEntry::AcpThread(thread) => Some(thread.clone()),
|
||||
HistoryEntry::TextThread(_) => None,
|
||||
};
|
||||
|
||||
let agent: Rc<dyn AgentServer> = Rc::new(NativeAgentServer::new(fs, history_store.clone()));
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
AcpThreadView::new(
|
||||
agent,
|
||||
resume_thread,
|
||||
None,
|
||||
workspace,
|
||||
project,
|
||||
history_store,
|
||||
prompt_store,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let notify = cx.observe(&thread_view, |_, _, cx| {
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
self.thread_view = Some(ActiveThreadView {
|
||||
view: thread_view,
|
||||
thread_id,
|
||||
_notify: notify,
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn title(&self, cx: &App) -> SharedString {
|
||||
if let Some(active_thread_view) = &self.thread_view {
|
||||
let thread_view = active_thread_view.view.read(cx);
|
||||
if let Some(thread) = thread_view.thread() {
|
||||
let title = thread.read(cx).title();
|
||||
if !title.is_empty() {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
thread_view.title(cx)
|
||||
} else {
|
||||
"Thread".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let position = self.position(window, cx);
|
||||
let slot = match position {
|
||||
UtilityPanePosition::Left => UtilityPaneSlot::Left,
|
||||
UtilityPanePosition::Right => UtilityPaneSlot::Right,
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let toggle_icon = self.toggle_icon(cx);
|
||||
let title = self.title(cx);
|
||||
|
||||
let pane_toggle_button = |workspace: WeakEntity<Workspace>| {
|
||||
IconButton::new("toggle_utility_pane", toggle_icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Toggle Agent Pane"))
|
||||
.on_click(move |_, window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_utility_pane(slot, window, cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("utility-pane-header")
|
||||
.w_full()
|
||||
.h(Tab::container_height(cx))
|
||||
.px_1p5()
|
||||
.gap(DynamicSpacing::Base06.rems(cx))
|
||||
.when(slot == UtilityPaneSlot::Right, |this| {
|
||||
this.flex_row_reverse()
|
||||
})
|
||||
.flex_none()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(pane_toggle_button(workspace))
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.min_w_0()
|
||||
.gap_1()
|
||||
.map(|this| {
|
||||
if slot == UtilityPaneSlot::Right {
|
||||
this.flex_row_reverse().justify_start()
|
||||
} else {
|
||||
this.justify_between()
|
||||
}
|
||||
})
|
||||
.child(Label::new(title).truncate())
|
||||
.child(
|
||||
IconButton::new("close_btn", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Close Agent Pane"))
|
||||
.on_click(cx.listener(|this, _: &gpui::ClickEvent, _window, cx| {
|
||||
cx.emit(ClosePane);
|
||||
this.thread_view = None;
|
||||
cx.notify()
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentThreadPane {
|
||||
fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
|
||||
if let Some(thread_view) = &self.thread_view {
|
||||
thread_view.view.focus_handle(cx)
|
||||
} else {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UtilityPane for AgentThreadPane {
|
||||
fn position(&self, _window: &Window, cx: &App) -> UtilityPanePosition {
|
||||
match AgentSettings::get_global(cx).agents_panel_dock {
|
||||
DockSide::Left => UtilityPanePosition::Left,
|
||||
DockSide::Right => UtilityPanePosition::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_icon(&self, _cx: &App) -> IconName {
|
||||
IconName::Thread
|
||||
}
|
||||
|
||||
fn expanded(&self, _cx: &App) -> bool {
|
||||
self.expanded
|
||||
}
|
||||
|
||||
fn set_expanded(&mut self, expanded: bool, cx: &mut Context<Self>) {
|
||||
self.expanded = expanded;
|
||||
cx.emit(AgentsUtilityPaneEvent::StateChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn width(&self, _cx: &App) -> Pixels {
|
||||
self.width.unwrap_or(DEFAULT_UTILITY_PANE_WIDTH)
|
||||
}
|
||||
|
||||
fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>) {
|
||||
self.width = width;
|
||||
cx.emit(AgentsUtilityPaneEvent::StateChanged);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentThreadPane {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let content = if let Some(thread_view) = &self.thread_view {
|
||||
div().size_full().child(thread_view.view.clone())
|
||||
} else {
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(Label::new("Select a thread to view details").size(LabelSize::Default))
|
||||
};
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(self.render_header(window, cx))
|
||||
.child(content)
|
||||
}
|
||||
}
|
||||
4
crates/agent_ui_v2/src/agent_ui_v2.rs
Normal file
4
crates/agent_ui_v2/src/agent_ui_v2.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
mod agent_thread_pane;
|
||||
mod thread_history;
|
||||
|
||||
pub mod agents_panel;
|
||||
438
crates/agent_ui_v2/src/agents_panel.rs
Normal file
438
crates/agent_ui_v2/src/agents_panel.rs
Normal file
@@ -0,0 +1,438 @@
|
||||
use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_text_thread::TextThreadStore;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, AsyncWindowContext, Entity, EventEmitter, Focusable, Pixels, Subscription, Task,
|
||||
WeakEntity, actions, prelude::*,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::{PromptBuilder, PromptStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use std::sync::Arc;
|
||||
use ui::{App, Context, IconName, IntoElement, ParentElement, Render, Styled, Window};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
Panel, Workspace,
|
||||
dock::{ClosePane, DockPosition, PanelEvent, UtilityPane},
|
||||
utility_pane::{UtilityPaneSlot, utility_slot_for_dock_position},
|
||||
};
|
||||
|
||||
use crate::agent_thread_pane::{
|
||||
AgentThreadPane, AgentsUtilityPaneEvent, SerializedAgentThreadPane, SerializedHistoryEntryId,
|
||||
};
|
||||
use crate::thread_history::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
|
||||
const AGENTS_PANEL_KEY: &str = "agents_panel";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct SerializedAgentsPanel {
|
||||
width: Option<Pixels>,
|
||||
pane: Option<SerializedAgentThreadPane>,
|
||||
}
|
||||
|
||||
actions!(
|
||||
agents,
|
||||
[
|
||||
/// Toggle the visibility of the agents panel.
|
||||
ToggleAgentsPanel
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, _: &ToggleAgentsPanel, window, cx| {
|
||||
workspace.toggle_panel_focus::<AgentsPanel>(window, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct AgentsPanel {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
agent_thread_pane: Option<Entity<AgentThreadPane>>,
|
||||
history: Entity<AcpThreadHistory>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
width: Option<Pixels>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl AgentsPanel {
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<Entity<Self>, anyhow::Error>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let serialized_panel = cx
|
||||
.background_spawn(async move {
|
||||
KEY_VALUE_STORE
|
||||
.read_kvp(AGENTS_PANEL_KEY)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|panel| {
|
||||
serde_json::from_str::<SerializedAgentsPanel>(&panel).ok()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
let (fs, project, prompt_builder) = workspace.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let project = workspace.project().clone();
|
||||
let prompt_builder = PromptBuilder::load(fs.clone(), false, cx);
|
||||
(fs, project, prompt_builder)
|
||||
})?;
|
||||
|
||||
let text_thread_store = workspace
|
||||
.update(cx, |_, cx| {
|
||||
TextThreadStore::new(
|
||||
project.clone(),
|
||||
prompt_builder.clone(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let prompt_store = workspace
|
||||
.update(cx, |_, cx| PromptStore::global(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
workspace.update_in(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
let mut panel = Self::new(
|
||||
workspace.clone(),
|
||||
fs,
|
||||
project,
|
||||
prompt_store,
|
||||
text_thread_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if let Some(serialized_panel) = serialized_panel {
|
||||
panel.width = serialized_panel.width;
|
||||
if let Some(serialized_pane) = serialized_panel.pane {
|
||||
panel.restore_utility_pane(serialized_pane, window, cx);
|
||||
}
|
||||
}
|
||||
panel
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
fs: Arc<dyn Fs>,
|
||||
project: Entity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
text_thread_store: Entity<TextThreadStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
|
||||
let history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx));
|
||||
|
||||
let this = cx.weak_entity();
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(&history, window, Self::handle_history_event),
|
||||
cx.on_flags_ready(move |_, cx| {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
workspace,
|
||||
project,
|
||||
agent_thread_pane: None,
|
||||
history,
|
||||
history_store,
|
||||
prompt_store,
|
||||
fs,
|
||||
width: None,
|
||||
pending_serialization: Task::ready(None),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_utility_pane(
|
||||
&mut self,
|
||||
serialized_pane: SerializedAgentThreadPane,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(thread_id) = &serialized_pane.thread_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entry = self
|
||||
.history_store
|
||||
.read(cx)
|
||||
.entries()
|
||||
.find(|e| match (&e.id(), thread_id) {
|
||||
(
|
||||
HistoryEntryId::AcpThread(session_id),
|
||||
SerializedHistoryEntryId::AcpThread(id),
|
||||
) => session_id.to_string() == *id,
|
||||
(HistoryEntryId::TextThread(path), SerializedHistoryEntryId::TextThread(id)) => {
|
||||
path.to_string_lossy() == *id
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if let Some(entry) = entry {
|
||||
self.open_thread(
|
||||
entry,
|
||||
serialized_pane.expanded,
|
||||
serialized_pane.width,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_utility_pane_event(
|
||||
&mut self,
|
||||
_utility_pane: Entity<AgentThreadPane>,
|
||||
event: &AgentsUtilityPaneEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
AgentsUtilityPaneEvent::StateChanged => {
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_close_pane_event(
|
||||
&mut self,
|
||||
_utility_pane: Entity<AgentThreadPane>,
|
||||
_event: &ClosePane,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.agent_thread_pane = None;
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_history_event(
|
||||
&mut self,
|
||||
_history: &Entity<AcpThreadHistory>,
|
||||
event: &ThreadHistoryEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadHistoryEvent::Open(entry) => {
|
||||
self.open_thread(entry.clone(), true, None, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_thread(
|
||||
&mut self,
|
||||
entry: HistoryEntry,
|
||||
expanded: bool,
|
||||
width: Option<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let entry_id = entry.id();
|
||||
|
||||
if let Some(existing_pane) = &self.agent_thread_pane {
|
||||
if existing_pane.read(cx).thread_id() == Some(entry_id) {
|
||||
existing_pane.update(cx, |pane, cx| {
|
||||
pane.set_expanded(true, cx);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let fs = self.fs.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let history_store = self.history_store.clone();
|
||||
let prompt_store = self.prompt_store.clone();
|
||||
|
||||
let agent_thread_pane = cx.new(|cx| {
|
||||
let mut pane = AgentThreadPane::new(workspace.clone(), cx);
|
||||
pane.open_thread(
|
||||
entry,
|
||||
fs,
|
||||
workspace.clone(),
|
||||
project,
|
||||
history_store,
|
||||
prompt_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if let Some(width) = width {
|
||||
pane.set_width(Some(width), cx);
|
||||
}
|
||||
pane.set_expanded(expanded, cx);
|
||||
pane
|
||||
});
|
||||
|
||||
let state_subscription = cx.subscribe(&agent_thread_pane, Self::handle_utility_pane_event);
|
||||
let close_subscription = cx.subscribe(&agent_thread_pane, Self::handle_close_pane_event);
|
||||
|
||||
self._subscriptions.push(state_subscription);
|
||||
self._subscriptions.push(close_subscription);
|
||||
|
||||
let slot = self.utility_slot(window, cx);
|
||||
let panel_id = cx.entity_id();
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.register_utility_pane(slot, panel_id, agent_thread_pane.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
self.agent_thread_pane = Some(agent_thread_pane);
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn utility_slot(&self, window: &Window, cx: &App) -> UtilityPaneSlot {
|
||||
let position = self.position(window, cx);
|
||||
utility_slot_for_dock_position(position)
|
||||
}
|
||||
|
||||
fn re_register_utility_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(pane) = &self.agent_thread_pane {
|
||||
let slot = self.utility_slot(window, cx);
|
||||
let panel_id = cx.entity_id();
|
||||
let pane = pane.clone();
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.register_utility_pane(slot, panel_id, pane, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(&mut self, cx: &mut Context<Self>) {
|
||||
let width = self.width;
|
||||
let pane = self
|
||||
.agent_thread_pane
|
||||
.as_ref()
|
||||
.map(|pane| pane.read(cx).serialize());
|
||||
|
||||
self.pending_serialization = cx.background_spawn(async move {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
AGENTS_PANEL_KEY.into(),
|
||||
serde_json::to_string(&SerializedAgentsPanel { width, pane }).unwrap(),
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for AgentsPanel {}
|
||||
|
||||
impl Focusable for AgentsPanel {
|
||||
fn focus_handle(&self, _cx: &ui::App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for AgentsPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"AgentsPanel"
|
||||
}
|
||||
|
||||
fn panel_key() -> &'static str {
|
||||
AGENTS_PANEL_KEY
|
||||
}
|
||||
|
||||
fn position(&self, _window: &Window, cx: &App) -> DockPosition {
|
||||
match AgentSettings::get_global(cx).agents_panel_dock {
|
||||
settings::DockSide::Left => DockPosition::Left,
|
||||
settings::DockSide::Right => DockPosition::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
position != DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn set_position(
|
||||
&mut self,
|
||||
position: DockPosition,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
update_settings_file(self.fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().agents_panel_dock = Some(match position {
|
||||
DockPosition::Left => settings::DockSide::Left,
|
||||
DockPosition::Bottom => settings::DockSide::Right,
|
||||
DockPosition::Right => settings::DockSide::Left,
|
||||
});
|
||||
});
|
||||
self.re_register_utility_pane(window, cx);
|
||||
}
|
||||
|
||||
fn size(&self, window: &Window, cx: &App) -> Pixels {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
match self.position(window, cx) {
|
||||
DockPosition::Left | DockPosition::Right => {
|
||||
self.width.unwrap_or(settings.default_width)
|
||||
}
|
||||
DockPosition::Bottom => self.width.unwrap_or(settings.default_height),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.position(window, cx) {
|
||||
DockPosition::Left | DockPosition::Right => self.width = size,
|
||||
DockPosition::Bottom => {}
|
||||
}
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
|
||||
(self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAgentTwo)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
|
||||
Some("Agents Panel")
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleAgentsPanel)
|
||||
}
|
||||
|
||||
fn activation_priority(&self) -> u32 {
|
||||
4
|
||||
}
|
||||
|
||||
fn enabled(&self, cx: &App) -> bool {
|
||||
AgentSettings::get_global(cx).enabled(cx) && cx.has_flag::<AgentV2FeatureFlag>()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentsPanel {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
gpui::div().size_full().child(self.history.clone())
|
||||
}
|
||||
}
|
||||
735
crates/agent_ui_v2/src/thread_history.rs
Normal file
735
crates/agent_ui_v2/src/thread_history.rs
Normal file
@@ -0,0 +1,735 @@
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
|
||||
UniformListScrollHandle, Window, actions, uniform_list,
|
||||
};
|
||||
use std::{fmt::Display, ops::Range};
|
||||
use text::Bias;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
actions!(
|
||||
agents,
|
||||
[
|
||||
/// Removes all thread history.
|
||||
RemoveHistory,
|
||||
/// Removes the currently selected thread.
|
||||
RemoveSelectedThread,
|
||||
]
|
||||
);
|
||||
|
||||
pub struct AcpThreadHistory {
|
||||
pub(crate) history_store: Entity<HistoryStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_index: usize,
|
||||
hovered_index: Option<usize>,
|
||||
search_editor: Entity<Editor>,
|
||||
search_query: SharedString,
|
||||
visible_items: Vec<ListItemType>,
|
||||
local_timezone: UtcOffset,
|
||||
confirming_delete_history: bool,
|
||||
_update_task: Task<()>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
|
||||
enum ListItemType {
|
||||
BucketSeparator(TimeBucket),
|
||||
Entry {
|
||||
entry: HistoryEntry,
|
||||
format: EntryTimeFormat,
|
||||
},
|
||||
SearchResult {
|
||||
entry: HistoryEntry,
|
||||
positions: Vec<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ListItemType {
|
||||
fn history_entry(&self) -> Option<&HistoryEntry> {
|
||||
match self {
|
||||
ListItemType::Entry { entry, .. } => Some(entry),
|
||||
ListItemType::SearchResult { entry, .. } => Some(entry),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum ThreadHistoryEvent {
|
||||
Open(HistoryEntry),
|
||||
}
|
||||
|
||||
impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
|
||||
|
||||
impl AcpThreadHistory {
|
||||
pub fn new(
|
||||
history_store: Entity<agent::HistoryStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let search_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Search threads...", window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let search_editor_subscription =
|
||||
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
|
||||
if let EditorEvent::BufferEdited = event {
|
||||
let query = search_editor.read(cx).text(cx);
|
||||
if this.search_query != query {
|
||||
this.search_query = query.into();
|
||||
this.update_visible_items(false, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
|
||||
this.update_visible_items(true, cx);
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::default();
|
||||
|
||||
let mut this = Self {
|
||||
history_store,
|
||||
scroll_handle,
|
||||
selected_index: 0,
|
||||
hovered_index: None,
|
||||
visible_items: Default::default(),
|
||||
search_editor,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
.unwrap(),
|
||||
search_query: SharedString::default(),
|
||||
confirming_delete_history: false,
|
||||
_subscriptions: vec![search_editor_subscription, history_store_subscription],
|
||||
_update_task: Task::ready(()),
|
||||
};
|
||||
this.update_visible_items(false, cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
|
||||
let entries = self
|
||||
.history_store
|
||||
.update(cx, |store, _| store.entries().collect());
|
||||
let new_list_items = if self.search_query.is_empty() {
|
||||
self.add_list_separators(entries, cx)
|
||||
} else {
|
||||
self.filter_search_results(entries, cx)
|
||||
};
|
||||
let selected_history_entry = if preserve_selected_item {
|
||||
self.selected_history_entry().cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self._update_task = cx.spawn(async move |this, cx| {
|
||||
let new_visible_items = new_list_items.await;
|
||||
this.update(cx, |this, cx| {
|
||||
let new_selected_index = if let Some(history_entry) = selected_history_entry {
|
||||
let history_entry_id = history_entry.id();
|
||||
new_visible_items
|
||||
.iter()
|
||||
.position(|visible_entry| {
|
||||
visible_entry
|
||||
.history_entry()
|
||||
.is_some_and(|entry| entry.id() == history_entry_id)
|
||||
})
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
this.visible_items = new_visible_items;
|
||||
this.set_selected_index(new_selected_index, Bias::Right, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
|
||||
fn add_list_separators(&self, entries: Vec<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
|
||||
cx.background_spawn(async move {
|
||||
let mut items = Vec::with_capacity(entries.len() + 1);
|
||||
let mut bucket = None;
|
||||
let today = Local::now().naive_local().date();
|
||||
|
||||
for entry in entries.into_iter() {
|
||||
let entry_date = entry
|
||||
.updated_at()
|
||||
.with_timezone(&Local)
|
||||
.naive_local()
|
||||
.date();
|
||||
let entry_bucket = TimeBucket::from_dates(today, entry_date);
|
||||
|
||||
if Some(entry_bucket) != bucket {
|
||||
bucket = Some(entry_bucket);
|
||||
items.push(ListItemType::BucketSeparator(entry_bucket));
|
||||
}
|
||||
|
||||
items.push(ListItemType::Entry {
|
||||
entry,
|
||||
format: entry_bucket.into(),
|
||||
});
|
||||
}
|
||||
items
|
||||
})
|
||||
}
|
||||
|
||||
fn filter_search_results(
|
||||
&self,
|
||||
entries: Vec<HistoryEntry>,
|
||||
cx: &App,
|
||||
) -> Task<Vec<ListItemType>> {
|
||||
let query = self.search_query.clone();
|
||||
cx.background_spawn({
|
||||
let executor = cx.background_executor().clone();
|
||||
async move {
|
||||
let mut candidates = Vec::with_capacity(entries.len());
|
||||
|
||||
for (idx, entry) in entries.iter().enumerate() {
|
||||
candidates.push(StringMatchCandidate::new(idx, entry.title()));
|
||||
}
|
||||
|
||||
const MAX_MATCHES: usize = 100;
|
||||
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
MAX_MATCHES,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|search_match| ListItemType::SearchResult {
|
||||
entry: entries[search_match.candidate_id].clone(),
|
||||
positions: search_match.positions,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn search_produced_no_matches(&self) -> bool {
|
||||
self.visible_items.is_empty() && !self.search_query.is_empty()
|
||||
}
|
||||
|
||||
fn selected_history_entry(&self) -> Option<&HistoryEntry> {
|
||||
self.get_history_entry(self.selected_index)
|
||||
}
|
||||
|
||||
fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> {
|
||||
self.visible_items.get(visible_items_ix)?.history_entry()
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
|
||||
if self.visible_items.is_empty() {
|
||||
self.selected_index = 0;
|
||||
return;
|
||||
}
|
||||
while matches!(
|
||||
self.visible_items.get(index),
|
||||
None | Some(ListItemType::BucketSeparator(..))
|
||||
) {
|
||||
index = match bias {
|
||||
Bias::Left => {
|
||||
if index == 0 {
|
||||
self.visible_items.len() - 1
|
||||
} else {
|
||||
index - 1
|
||||
}
|
||||
}
|
||||
Bias::Right => {
|
||||
if index >= self.visible_items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
index + 1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
self.selected_index = index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(index, ScrollStrategy::Top);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_index == 0 {
|
||||
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next(
|
||||
&mut self,
|
||||
_: &menu::SelectNext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_index == self.visible_items.len() - 1 {
|
||||
self.set_selected_index(0, Bias::Right, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.set_selected_index(0, Bias::Right, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.confirm_entry(self.selected_index, cx);
|
||||
}
|
||||
|
||||
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.get_history_entry(ix) else {
|
||||
return;
|
||||
};
|
||||
cx.emit(ThreadHistoryEvent::Open(entry.clone()));
|
||||
}
|
||||
|
||||
fn remove_selected_thread(
|
||||
&mut self,
|
||||
_: &RemoveSelectedThread,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.remove_thread(self.selected_index, cx)
|
||||
}
|
||||
|
||||
fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.get_history_entry(visible_item_ix) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let task = match entry {
|
||||
HistoryEntry::AcpThread(thread) => self
|
||||
.history_store
|
||||
.update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
|
||||
HistoryEntry::TextThread(text_thread) => self.history_store.update(cx, |this, cx| {
|
||||
this.delete_text_thread(text_thread.path.clone(), cx)
|
||||
}),
|
||||
};
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn remove_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.history_store.update(cx, |store, cx| {
|
||||
store.delete_threads(cx).detach_and_log_err(cx)
|
||||
});
|
||||
self.confirming_delete_history = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.confirming_delete_history = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.confirming_delete_history = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_list_items(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
self.visible_items
|
||||
.get(range.clone())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||
match item {
|
||||
ListItemType::Entry { entry, format } => self
|
||||
.render_history_entry(entry, *format, ix, Vec::default(), cx)
|
||||
.into_any(),
|
||||
ListItemType::SearchResult { entry, positions } => self.render_history_entry(
|
||||
entry,
|
||||
EntryTimeFormat::DateAndTime,
|
||||
ix,
|
||||
positions.clone(),
|
||||
cx,
|
||||
),
|
||||
ListItemType::BucketSeparator(bucket) => div()
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.pt_2()
|
||||
.pb_1()
|
||||
.child(
|
||||
Label::new(bucket.to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_history_entry(
|
||||
&self,
|
||||
entry: &HistoryEntry,
|
||||
format: EntryTimeFormat,
|
||||
ix: usize,
|
||||
highlight_positions: Vec<usize>,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pb_1()
|
||||
.child(
|
||||
ListItem::new(ix)
|
||||
.rounded()
|
||||
.toggle_state(selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
HighlightedLabel::new(entry.title(), highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
} else if this.hovered_index == Some(ix) {
|
||||
this.hovered_index = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
.end_slot::<IconButton>(if hovered {
|
||||
Some(
|
||||
IconButton::new("delete", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, cx)
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.remove_thread(ix, cx);
|
||||
cx.stop_propagation()
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AcpThreadHistory {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.search_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AcpThreadHistory {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_no_history = self.history_store.read(cx).is_empty(cx);
|
||||
|
||||
v_flex()
|
||||
.key_context("ThreadHistory")
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::remove_selected_thread))
|
||||
.on_action(cx.listener(|this, _: &RemoveHistory, window, cx| {
|
||||
this.remove_history(window, cx);
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.h(Tab::container_height(cx))
|
||||
.w_full()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Icon::new(IconName::MagnifyingGlass)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(self.search_editor.clone()),
|
||||
)
|
||||
.child({
|
||||
let view = v_flex()
|
||||
.id("list-container")
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.flex_grow();
|
||||
|
||||
if has_no_history {
|
||||
view.justify_center().items_center().child(
|
||||
Label::new("You don't have any past threads yet.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else if self.search_produced_no_matches() {
|
||||
view.justify_center()
|
||||
.items_center()
|
||||
.child(Label::new("No threads match your search.").size(LabelSize::Small))
|
||||
} else {
|
||||
view.child(
|
||||
uniform_list(
|
||||
"thread-history",
|
||||
self.visible_items.len(),
|
||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||
this.render_list_items(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.p_1()
|
||||
.pr_4()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.flex_grow(),
|
||||
)
|
||||
.vertical_scrollbar_for(&self.scroll_handle, window, cx)
|
||||
}
|
||||
})
|
||||
.when(!has_no_history, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.when(!self.confirming_delete_history, |this| {
|
||||
this.child(
|
||||
Button::new("delete_history", "Delete All History")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.prompt_delete_history(window, cx);
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(self.confirming_delete_history, |this| {
|
||||
this.w_full()
|
||||
.gap_2()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Delete all threads?")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new("You won't be able to recover them later.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("cancel_delete", "Cancel")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.cancel_delete_history(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("confirm_delete", "Delete")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
.color(Color::Error)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(RemoveHistory),
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum EntryTimeFormat {
|
||||
DateAndTime,
|
||||
TimeOnly,
|
||||
}
|
||||
|
||||
impl EntryTimeFormat {
|
||||
fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String {
|
||||
let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
|
||||
|
||||
match self {
|
||||
EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
OffsetDateTime::now_utc(),
|
||||
timezone,
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
),
|
||||
EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimeBucket> for EntryTimeFormat {
|
||||
fn from(bucket: TimeBucket) -> Self {
|
||||
match bucket {
|
||||
TimeBucket::Today => EntryTimeFormat::TimeOnly,
|
||||
TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
|
||||
TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
|
||||
TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
|
||||
TimeBucket::All => EntryTimeFormat::DateAndTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
enum TimeBucket {
|
||||
Today,
|
||||
Yesterday,
|
||||
ThisWeek,
|
||||
PastWeek,
|
||||
All,
|
||||
}
|
||||
|
||||
impl TimeBucket {
|
||||
fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
|
||||
if date == reference {
|
||||
return TimeBucket::Today;
|
||||
}
|
||||
|
||||
if date == reference - TimeDelta::days(1) {
|
||||
return TimeBucket::Yesterday;
|
||||
}
|
||||
|
||||
let week = date.iso_week();
|
||||
|
||||
if reference.iso_week() == week {
|
||||
return TimeBucket::ThisWeek;
|
||||
}
|
||||
|
||||
let last_week = (reference - TimeDelta::days(7)).iso_week();
|
||||
|
||||
if week == last_week {
|
||||
return TimeBucket::PastWeek;
|
||||
}
|
||||
|
||||
TimeBucket::All
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TimeBucket {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TimeBucket::Today => write!(f, "Today"),
|
||||
TimeBucket::Yesterday => write!(f, "Yesterday"),
|
||||
TimeBucket::ThisWeek => write!(f, "This Week"),
|
||||
TimeBucket::PastWeek => write!(f, "Past Week"),
|
||||
TimeBucket::All => write!(f, "All"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
#[test]
|
||||
fn test_time_bucket_from_dates() {
|
||||
let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
|
||||
|
||||
let date = today;
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
|
||||
|
||||
// All: not in this week or last week
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
|
||||
|
||||
// Test year boundary cases
|
||||
let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap();
|
||||
assert_eq!(
|
||||
TimeBucket::from_dates(new_year, date),
|
||||
TimeBucket::Yesterday
|
||||
);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ pub use settings::{AnthropicAvailableModel as AvailableModel, ModelMode};
|
||||
use strum::{EnumIter, EnumString};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod batches;
|
||||
|
||||
pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
|
||||
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
@@ -427,10 +429,24 @@ impl Model {
|
||||
let mut headers = vec![];
|
||||
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4_5
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4_5
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeOpus4_5Thinking
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeSonnet4_5Thinking => {
|
||||
// Fine-grained tool streaming for newer models
|
||||
headers.push("fine-grained-tool-streaming-2025-05-14".to_string());
|
||||
}
|
||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
|
||||
// Try beta token-efficient tool use (supported in Claude 3.7 Sonnet only)
|
||||
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use/token-efficient-tool-use
|
||||
headers.push("token-efficient-tools-2025-02-19".to_string());
|
||||
headers.push("fine-grained-tool-streaming-2025-05-14".to_string());
|
||||
}
|
||||
Self::Custom {
|
||||
extra_beta_headers, ..
|
||||
@@ -465,6 +481,7 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate completion with streaming.
|
||||
pub async fn stream_completion(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
@@ -477,6 +494,101 @@ pub async fn stream_completion(
|
||||
.map(|output| output.0)
|
||||
}
|
||||
|
||||
/// Generate completion without streaming.
|
||||
pub async fn non_streaming_completion(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
beta_headers: Option<String>,
|
||||
) -> Result<Response, AnthropicError> {
|
||||
let (mut response, rate_limits) =
|
||||
send_request(client, api_url, api_key, &request, beta_headers).await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)
|
||||
} else {
|
||||
Err(handle_error_response(response, rate_limits).await)
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_request(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: impl Serialize,
|
||||
beta_headers: Option<String>,
|
||||
) -> Result<(http::Response<AsyncBody>, RateLimitInfo), AnthropicError> {
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("X-Api-Key", api_key.trim())
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
if let Some(beta_headers) = beta_headers {
|
||||
request_builder = request_builder.header("Anthropic-Beta", beta_headers);
|
||||
}
|
||||
|
||||
let serialized_request =
|
||||
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
||||
let request = request_builder
|
||||
.body(AsyncBody::from(serialized_request))
|
||||
.map_err(AnthropicError::BuildRequestBody)?;
|
||||
|
||||
let response = client
|
||||
.send(request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
|
||||
Ok((response, rate_limits))
|
||||
}
|
||||
|
||||
async fn handle_error_response(
|
||||
mut response: http::Response<AsyncBody>,
|
||||
rate_limits: RateLimitInfo,
|
||||
) -> AnthropicError {
|
||||
if response.status().as_u16() == 529 {
|
||||
return AnthropicError::ServerOverloaded {
|
||||
retry_after: rate_limits.retry_after,
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(retry_after) = rate_limits.retry_after {
|
||||
return AnthropicError::RateLimit { retry_after };
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
let read_result = response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse);
|
||||
|
||||
if let Err(err) = read_result {
|
||||
return err;
|
||||
}
|
||||
|
||||
match serde_json::from_str::<Event>(&body) {
|
||||
Ok(Event::Error { error }) => AnthropicError::ApiError(error),
|
||||
Ok(_) | Err(_) => AnthropicError::HttpResponseError {
|
||||
status_code: response.status(),
|
||||
message: body,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// An individual rate limit.
|
||||
#[derive(Debug)]
|
||||
pub struct RateLimit {
|
||||
@@ -580,30 +692,10 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
base: request,
|
||||
stream: true,
|
||||
};
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
|
||||
let mut request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("X-Api-Key", api_key.trim())
|
||||
.header("Content-Type", "application/json");
|
||||
let (response, rate_limits) =
|
||||
send_request(client, api_url, api_key, &request, beta_headers).await?;
|
||||
|
||||
if let Some(beta_headers) = beta_headers {
|
||||
request_builder = request_builder.header("Anthropic-Beta", beta_headers);
|
||||
}
|
||||
|
||||
let serialized_request =
|
||||
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
||||
let request = request_builder
|
||||
.body(AsyncBody::from(serialized_request))
|
||||
.map_err(AnthropicError::BuildRequestBody)?;
|
||||
|
||||
let mut response = client
|
||||
.send(request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
if response.status().is_success() {
|
||||
let reader = BufReader::new(response.into_body());
|
||||
let stream = reader
|
||||
@@ -622,27 +714,8 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
})
|
||||
.boxed();
|
||||
Ok((stream, Some(rate_limits)))
|
||||
} else if response.status().as_u16() == 529 {
|
||||
Err(AnthropicError::ServerOverloaded {
|
||||
retry_after: rate_limits.retry_after,
|
||||
})
|
||||
} else if let Some(retry_after) = rate_limits.retry_after {
|
||||
Err(AnthropicError::RateLimit { retry_after })
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
match serde_json::from_str::<Event>(&body) {
|
||||
Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
|
||||
Ok(_) | Err(_) => Err(AnthropicError::HttpResponseError {
|
||||
status_code: response.status(),
|
||||
message: body,
|
||||
}),
|
||||
}
|
||||
Err(handle_error_response(response, rate_limits).await)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
190
crates/anthropic/src/batches.rs
Normal file
190
crates/anthropic/src/batches.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
use anyhow::Result;
|
||||
use futures::AsyncReadExt;
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{AnthropicError, ApiError, RateLimitInfo, Request, Response};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BatchRequest {
|
||||
pub custom_id: String,
|
||||
pub params: Request,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateBatchRequest {
|
||||
pub requests: Vec<BatchRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MessageBatchRequestCounts {
|
||||
pub processing: u64,
|
||||
pub succeeded: u64,
|
||||
pub errored: u64,
|
||||
pub canceled: u64,
|
||||
pub expired: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct MessageBatch {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub batch_type: String,
|
||||
pub processing_status: String,
|
||||
pub request_counts: MessageBatchRequestCounts,
|
||||
pub ended_at: Option<String>,
|
||||
pub created_at: String,
|
||||
pub expires_at: String,
|
||||
pub archived_at: Option<String>,
|
||||
pub cancel_initiated_at: Option<String>,
|
||||
pub results_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum BatchResult {
|
||||
#[serde(rename = "succeeded")]
|
||||
Succeeded { message: Response },
|
||||
#[serde(rename = "errored")]
|
||||
Errored { error: ApiError },
|
||||
#[serde(rename = "canceled")]
|
||||
Canceled,
|
||||
#[serde(rename = "expired")]
|
||||
Expired,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BatchIndividualResponse {
|
||||
pub custom_id: String,
|
||||
pub result: BatchResult,
|
||||
}
|
||||
|
||||
pub async fn create_batch(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: CreateBatchRequest,
|
||||
) -> Result<MessageBatch, AnthropicError> {
|
||||
let uri = format!("{api_url}/v1/messages/batches");
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("X-Api-Key", api_key.trim())
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
let serialized_request =
|
||||
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
||||
let http_request = request_builder
|
||||
.body(AsyncBody::from(serialized_request))
|
||||
.map_err(AnthropicError::BuildRequestBody)?;
|
||||
|
||||
let mut response = client
|
||||
.send(http_request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
|
||||
if response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)
|
||||
} else {
|
||||
Err(crate::handle_error_response(response, rate_limits).await)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn retrieve_batch(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
message_batch_id: &str,
|
||||
) -> Result<MessageBatch, AnthropicError> {
|
||||
let uri = format!("{api_url}/v1/messages/batches/{message_batch_id}");
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::GET)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("X-Api-Key", api_key.trim());
|
||||
|
||||
let http_request = request_builder
|
||||
.body(AsyncBody::default())
|
||||
.map_err(AnthropicError::BuildRequestBody)?;
|
||||
|
||||
let mut response = client
|
||||
.send(http_request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
|
||||
if response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)
|
||||
} else {
|
||||
Err(crate::handle_error_response(response, rate_limits).await)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn retrieve_batch_results(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
message_batch_id: &str,
|
||||
) -> Result<Vec<BatchIndividualResponse>, AnthropicError> {
|
||||
let uri = format!("{api_url}/v1/messages/batches/{message_batch_id}/results");
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::GET)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("X-Api-Key", api_key.trim());
|
||||
|
||||
let http_request = request_builder
|
||||
.body(AsyncBody::default())
|
||||
.map_err(AnthropicError::BuildRequestBody)?;
|
||||
|
||||
let mut response = client
|
||||
.send(http_request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
|
||||
if response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for line in body.lines() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let result: BatchIndividualResponse =
|
||||
serde_json::from_str(line).map_err(AnthropicError::DeserializeResponse)?;
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
} else {
|
||||
Err(crate::handle_error_response(response, rate_limits).await)
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
globset.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
|
||||
@@ -226,10 +226,10 @@ fn collect_files(
|
||||
let Ok(matchers) = glob_inputs
|
||||
.iter()
|
||||
.map(|glob_input| {
|
||||
custom_path_matcher::PathMatcher::new(&[glob_input.to_owned()])
|
||||
util::paths::PathMatcher::new(&[glob_input.to_owned()], project.read(cx).path_style(cx))
|
||||
.with_context(|| format!("invalid path {glob_input}"))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<custom_path_matcher::PathMatcher>>>()
|
||||
.collect::<anyhow::Result<Vec<util::paths::PathMatcher>>>()
|
||||
else {
|
||||
return futures::stream::once(async {
|
||||
anyhow::bail!("invalid path");
|
||||
@@ -250,6 +250,7 @@ fn collect_files(
|
||||
let worktree_id = snapshot.id();
|
||||
let path_style = snapshot.path_style();
|
||||
let mut directory_stack: Vec<Arc<RelPath>> = Vec::new();
|
||||
let mut folded_directory_path: Option<Arc<RelPath>> = None;
|
||||
let mut folded_directory_names: Arc<RelPath> = RelPath::empty().into();
|
||||
let mut is_top_level_directory = true;
|
||||
|
||||
@@ -277,6 +278,16 @@ fn collect_files(
|
||||
)))?;
|
||||
}
|
||||
|
||||
if let Some(folded_path) = &folded_directory_path {
|
||||
if !entry.path.starts_with(folded_path) {
|
||||
folded_directory_names = RelPath::empty().into();
|
||||
folded_directory_path = None;
|
||||
if directory_stack.is_empty() {
|
||||
is_top_level_directory = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let filename = entry.path.file_name().unwrap_or_default().to_string();
|
||||
|
||||
if entry.is_dir() {
|
||||
@@ -292,13 +303,17 @@ fn collect_files(
|
||||
folded_directory_names =
|
||||
folded_directory_names.join(RelPath::unix(&filename).unwrap());
|
||||
}
|
||||
folded_directory_path = Some(entry.path.clone());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Skip empty directories
|
||||
folded_directory_names = RelPath::empty().into();
|
||||
folded_directory_path = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Render the directory (either folded or normal)
|
||||
if folded_directory_names.is_empty() {
|
||||
let label = if is_top_level_directory {
|
||||
is_top_level_directory = false;
|
||||
@@ -334,6 +349,8 @@ fn collect_files(
|
||||
},
|
||||
)))?;
|
||||
directory_stack.push(entry.path.clone());
|
||||
folded_directory_names = RelPath::empty().into();
|
||||
folded_directory_path = None;
|
||||
}
|
||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
||||
SlashCommandContent::Text {
|
||||
@@ -447,87 +464,6 @@ pub fn build_entry_output_section(
|
||||
}
|
||||
}
|
||||
|
||||
/// This contains a small fork of the util::paths::PathMatcher, that is stricter about the prefix
|
||||
/// check. Only subpaths pass the prefix check, rather than any prefix.
|
||||
mod custom_path_matcher {
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use std::fmt::Debug as _;
|
||||
use util::{paths::SanitizedPath, rel_path::RelPath};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PathMatcher {
|
||||
sources: Vec<String>,
|
||||
sources_with_trailing_slash: Vec<String>,
|
||||
glob: GlobSet,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PathMatcher {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.sources.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PathMatcher {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.sources.eq(&other.sources)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PathMatcher {}
|
||||
|
||||
impl PathMatcher {
|
||||
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
|
||||
let globs = globs
|
||||
.iter()
|
||||
.map(|glob| Glob::new(&SanitizedPath::new(glob).to_string()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
|
||||
let sources_with_trailing_slash = globs
|
||||
.iter()
|
||||
.map(|glob| glob.glob().to_string() + "/")
|
||||
.collect();
|
||||
let mut glob_builder = GlobSetBuilder::new();
|
||||
for single_glob in globs {
|
||||
glob_builder.add(single_glob);
|
||||
}
|
||||
let glob = glob_builder.build()?;
|
||||
Ok(PathMatcher {
|
||||
glob,
|
||||
sources,
|
||||
sources_with_trailing_slash,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_match(&self, other: &RelPath) -> bool {
|
||||
self.sources
|
||||
.iter()
|
||||
.zip(self.sources_with_trailing_slash.iter())
|
||||
.any(|(source, with_slash)| {
|
||||
let as_bytes = other.as_unix_str().as_bytes();
|
||||
let with_slash = if source.ends_with('/') {
|
||||
source.as_bytes()
|
||||
} else {
|
||||
with_slash.as_bytes()
|
||||
};
|
||||
|
||||
as_bytes.starts_with(with_slash) || as_bytes.ends_with(source.as_bytes())
|
||||
})
|
||||
|| self.glob.is_match(other.as_std_path())
|
||||
|| self.check_with_end_separator(other)
|
||||
}
|
||||
|
||||
fn check_with_end_separator(&self, path: &RelPath) -> bool {
|
||||
let path_str = path.as_unix_str();
|
||||
let separator = "/";
|
||||
if path_str.ends_with(separator) {
|
||||
false
|
||||
} else {
|
||||
self.glob.is_match(path_str.to_string() + separator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_buffer_to_output(
|
||||
buffer: &BufferSnapshot,
|
||||
path: Option<&str>,
|
||||
|
||||
@@ -46,7 +46,7 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
telemetry.workspace = true
|
||||
text.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -50,7 +50,6 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
|
||||
TextThread::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
@@ -189,7 +188,6 @@ fn test_message_splitting(cx: &mut App) {
|
||||
TextThread::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
@@ -294,7 +292,6 @@ fn test_messages_for_offsets(cx: &mut App) {
|
||||
TextThread::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
@@ -405,7 +402,6 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
TextThread::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
@@ -677,7 +673,6 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
TextThread::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
@@ -724,7 +719,6 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -780,7 +774,6 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1041,7 +1034,6 @@ fn test_mark_cache_anchors(cx: &mut App) {
|
||||
TextThread::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
@@ -1368,7 +1360,6 @@ fn setup_context_editor_with_fake_model(
|
||||
TextThread::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::{
|
||||
SlashCommandResult, SlashCommandWorkingSet,
|
||||
};
|
||||
use assistant_slash_commands::FileCommandMetadata;
|
||||
use client::{self, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry};
|
||||
use client::{self, ModelRequestUsage, RequestUsage, proto};
|
||||
use clock::ReplicaId;
|
||||
use cloud_llm_client::{CompletionIntent, UsageLimit};
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -14,15 +14,16 @@ use fs::{Fs, RenameOptions};
|
||||
use futures::{FutureExt, StreamExt, future::Shared};
|
||||
use gpui::{
|
||||
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
|
||||
Task,
|
||||
Task, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
AnthropicCompletionType, AnthropicEventData, AnthropicEventType, LanguageModel,
|
||||
LanguageModelCacheConfiguration, LanguageModelCompletionEvent, LanguageModelImage,
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelToolUseId, MessageContent, PaymentRequiredError, Role, StopReason,
|
||||
report_assistant_event,
|
||||
report_anthropic_event,
|
||||
};
|
||||
use open_ai::Model as OpenAiModel;
|
||||
use paths::text_threads_dir;
|
||||
@@ -40,7 +41,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
|
||||
use text::{BufferSnapshot, ToPoint};
|
||||
use ui::IconName;
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
@@ -686,9 +687,8 @@ pub struct TextThread {
|
||||
pending_cache_warming_task: Task<Option<()>>,
|
||||
path: Option<Arc<Path>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project: Option<Entity<Project>>,
|
||||
project: Option<WeakEntity<Project>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
completion_mode: agent_settings::CompletionMode,
|
||||
}
|
||||
@@ -708,8 +708,7 @@ impl EventEmitter<TextThreadEvent> for TextThread {}
|
||||
impl TextThread {
|
||||
pub fn local(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project: Option<Entity<Project>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
project: Option<WeakEntity<Project>>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -722,7 +721,6 @@ impl TextThread {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
project,
|
||||
telemetry,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -742,8 +740,7 @@ impl TextThread {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
project: Option<Entity<Project>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
project: Option<WeakEntity<Project>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let buffer = cx.new(|_cx| {
|
||||
@@ -784,7 +781,6 @@ impl TextThread {
|
||||
completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
|
||||
path: None,
|
||||
buffer,
|
||||
telemetry,
|
||||
project,
|
||||
language_registry,
|
||||
slash_commands,
|
||||
@@ -873,8 +869,7 @@ impl TextThread {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
project: Option<Entity<Project>>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
project: Option<WeakEntity<Project>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let id = saved_context.id.clone().unwrap_or_else(TextThreadId::new);
|
||||
@@ -886,7 +881,6 @@ impl TextThread {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
project,
|
||||
telemetry,
|
||||
cx,
|
||||
);
|
||||
this.path = Some(path);
|
||||
@@ -1167,10 +1161,6 @@ impl TextThread {
|
||||
self.language_registry.clone()
|
||||
}
|
||||
|
||||
pub fn project(&self) -> Option<Entity<Project>> {
|
||||
self.project.clone()
|
||||
}
|
||||
|
||||
pub fn prompt_builder(&self) -> Arc<PromptBuilder> {
|
||||
self.prompt_builder.clone()
|
||||
}
|
||||
@@ -2216,24 +2206,26 @@ impl TextThread {
|
||||
.read(cx)
|
||||
.language()
|
||||
.map(|language| language.name());
|
||||
report_assistant_event(
|
||||
AssistantEventData {
|
||||
conversation_id: Some(this.id.0.clone()),
|
||||
kind: AssistantKind::Panel,
|
||||
phase: AssistantPhase::Response,
|
||||
message_id: None,
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
},
|
||||
this.telemetry.clone(),
|
||||
cx.http_client(),
|
||||
model.api_key(cx),
|
||||
cx.background_executor(),
|
||||
|
||||
telemetry::event!(
|
||||
"Assistant Responded",
|
||||
conversation_id = this.id.0.clone(),
|
||||
kind = "panel",
|
||||
phase = "response",
|
||||
model = model.telemetry_id(),
|
||||
model_provider = model.provider_id().to_string(),
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name = language_name.as_ref().map(|name| name.to_proto()),
|
||||
);
|
||||
|
||||
report_anthropic_event(&model, AnthropicEventData {
|
||||
completion_type: AnthropicCompletionType::Panel,
|
||||
event: AnthropicEventType::Response,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
message_id: None,
|
||||
}, cx);
|
||||
|
||||
if let Ok(stop_reason) = result {
|
||||
match stop_reason {
|
||||
StopReason::ToolUse => {}
|
||||
@@ -2967,7 +2959,7 @@ impl TextThread {
|
||||
}
|
||||
|
||||
fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut App) {
|
||||
let Some(project) = &self.project else {
|
||||
let Some(project) = self.project.as_ref().and_then(|project| project.upgrade()) else {
|
||||
return;
|
||||
};
|
||||
project.read(cx).user_store().update(cx, |user_store, cx| {
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
|
||||
use client::{Client, TypedEnvelope, proto, telemetry::Telemetry};
|
||||
use client::{Client, TypedEnvelope, proto};
|
||||
use clock::ReplicaId;
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
@@ -48,10 +48,9 @@ pub struct TextThreadStore {
|
||||
fs: Arc<dyn Fs>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
_watch_updates: Task<Option<()>>,
|
||||
client: Arc<Client>,
|
||||
project: Entity<Project>,
|
||||
project: WeakEntity<Project>,
|
||||
project_is_shared: bool,
|
||||
client_subscription: Option<client::Subscription>,
|
||||
_project_subscriptions: Vec<gpui::Subscription>,
|
||||
@@ -88,7 +87,6 @@ impl TextThreadStore {
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let languages = project.read(cx).languages().clone();
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
cx.spawn(async move |cx| {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(text_threads_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
@@ -102,7 +100,6 @@ impl TextThreadStore {
|
||||
fs,
|
||||
languages,
|
||||
slash_commands,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
@@ -119,10 +116,10 @@ impl TextThreadStore {
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
project: project.downgrade(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_shared(project.clone(), cx);
|
||||
this.handle_project_shared(cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
@@ -143,10 +140,9 @@ impl TextThreadStore {
|
||||
fs: project.read(cx).fs().clone(),
|
||||
languages: project.read(cx).languages().clone(),
|
||||
slash_commands: Arc::default(),
|
||||
telemetry: project.read(cx).client().telemetry().clone(),
|
||||
_watch_updates: Task::ready(None),
|
||||
client: project.read(cx).client(),
|
||||
project,
|
||||
project: project.downgrade(),
|
||||
project_is_shared: false,
|
||||
client_subscription: None,
|
||||
_project_subscriptions: Default::default(),
|
||||
@@ -180,8 +176,10 @@ impl TextThreadStore {
|
||||
) -> Result<proto::OpenContextResponse> {
|
||||
let context_id = TextThreadId::from_proto(envelope.payload.context_id);
|
||||
let operations = this.update(&mut cx, |this, cx| {
|
||||
let project = this.project.upgrade().context("project not found")?;
|
||||
|
||||
anyhow::ensure!(
|
||||
!this.project.read(cx).is_via_collab(),
|
||||
!project.read(cx).is_via_collab(),
|
||||
"only the host contexts can be opened"
|
||||
);
|
||||
|
||||
@@ -211,8 +209,9 @@ impl TextThreadStore {
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::CreateContextResponse> {
|
||||
let (context_id, operations) = this.update(&mut cx, |this, cx| {
|
||||
let project = this.project.upgrade().context("project not found")?;
|
||||
anyhow::ensure!(
|
||||
!this.project.read(cx).is_via_collab(),
|
||||
!project.read(cx).is_via_collab(),
|
||||
"can only create contexts as the host"
|
||||
);
|
||||
|
||||
@@ -255,8 +254,9 @@ impl TextThreadStore {
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::SynchronizeContextsResponse> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let project = this.project.upgrade().context("project not found")?;
|
||||
anyhow::ensure!(
|
||||
!this.project.read(cx).is_via_collab(),
|
||||
!project.read(cx).is_via_collab(),
|
||||
"only the host can synchronize contexts"
|
||||
);
|
||||
|
||||
@@ -293,8 +293,12 @@ impl TextThreadStore {
|
||||
})?
|
||||
}
|
||||
|
||||
fn handle_project_shared(&mut self, _: Entity<Project>, cx: &mut Context<Self>) {
|
||||
let is_shared = self.project.read(cx).is_shared();
|
||||
fn handle_project_shared(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let is_shared = project.read(cx).is_shared();
|
||||
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
|
||||
if is_shared == was_shared {
|
||||
return;
|
||||
@@ -309,7 +313,7 @@ impl TextThreadStore {
|
||||
false
|
||||
}
|
||||
});
|
||||
let remote_id = self.project.read(cx).remote_id().unwrap();
|
||||
let remote_id = project.read(cx).remote_id().unwrap();
|
||||
self.client_subscription = self
|
||||
.client
|
||||
.subscribe_to_entity(remote_id)
|
||||
@@ -323,13 +327,13 @@ impl TextThreadStore {
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
project: Entity<Project>,
|
||||
_project: Entity<Project>,
|
||||
event: &project::Event,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::Event::RemoteIdChanged(_) => {
|
||||
self.handle_project_shared(project, cx);
|
||||
self.handle_project_shared(cx);
|
||||
}
|
||||
project::Event::Reshared => {
|
||||
self.advertise_contexts(cx);
|
||||
@@ -371,7 +375,6 @@ impl TextThreadStore {
|
||||
TextThread::local(
|
||||
self.languages.clone(),
|
||||
Some(self.project.clone()),
|
||||
Some(self.telemetry.clone()),
|
||||
self.prompt_builder.clone(),
|
||||
self.slash_commands.clone(),
|
||||
cx,
|
||||
@@ -382,7 +385,10 @@ impl TextThreadStore {
|
||||
}
|
||||
|
||||
pub fn create_remote(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<TextThread>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("project was dropped")));
|
||||
};
|
||||
let project = project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("project was not remote")));
|
||||
};
|
||||
@@ -391,7 +397,7 @@ impl TextThreadStore {
|
||||
let capability = project.capability();
|
||||
let language_registry = self.languages.clone();
|
||||
let project = self.project.clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
|
||||
let prompt_builder = self.prompt_builder.clone();
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let request = self.client.request(proto::CreateContext { project_id });
|
||||
@@ -408,7 +414,6 @@ impl TextThreadStore {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -446,7 +451,6 @@ impl TextThreadStore {
|
||||
let fs = self.fs.clone();
|
||||
let languages = self.languages.clone();
|
||||
let project = self.project.clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let load = cx.background_spawn({
|
||||
let path = path.clone();
|
||||
async move {
|
||||
@@ -467,7 +471,6 @@ impl TextThreadStore {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -541,7 +544,10 @@ impl TextThreadStore {
|
||||
text_thread_id: TextThreadId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<TextThread>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("project was dropped")));
|
||||
};
|
||||
let project = project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("project was not remote")));
|
||||
};
|
||||
@@ -554,7 +560,6 @@ impl TextThreadStore {
|
||||
let capability = project.capability();
|
||||
let language_registry = self.languages.clone();
|
||||
let project = self.project.clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let request = self.client.request(proto::OpenContext {
|
||||
project_id,
|
||||
context_id: text_thread_id.to_proto(),
|
||||
@@ -573,7 +578,6 @@ impl TextThreadStore {
|
||||
prompt_builder,
|
||||
slash_commands,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -618,7 +622,10 @@ impl TextThreadStore {
|
||||
event: &TextThreadEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(project_id) = project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -652,12 +659,14 @@ impl TextThreadStore {
|
||||
}
|
||||
|
||||
fn advertise_contexts(&self, cx: &App) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(project_id) = project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// For now, only the host can advertise their open contexts.
|
||||
if self.project.read(cx).is_via_collab() {
|
||||
if project.read(cx).is_via_collab() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -689,7 +698,10 @@ impl TextThreadStore {
|
||||
}
|
||||
|
||||
fn synchronize_contexts(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(project_id) = project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -828,7 +840,10 @@ impl TextThreadStore {
|
||||
}
|
||||
|
||||
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
|
||||
let context_server_store = self.project.read(cx).context_server_store();
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let context_server_store = project.read(cx).context_server_store();
|
||||
cx.subscribe(&context_server_store, Self::handle_context_server_event)
|
||||
.detach();
|
||||
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
use futures::channel::oneshot;
|
||||
use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, TaskLabel};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task};
|
||||
use language::{
|
||||
BufferRow, DiffOptions, File, Language, LanguageName, LanguageRegistry,
|
||||
language_settings::language_settings, word_diff_ranges,
|
||||
};
|
||||
use rope::Rope;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
future::Future,
|
||||
iter,
|
||||
ops::Range,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
use std::{cmp::Ordering, future::Future, iter, ops::Range, sync::Arc};
|
||||
use sum_tree::SumTree;
|
||||
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint as _};
|
||||
use util::ResultExt;
|
||||
|
||||
pub static CALCULATE_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
|
||||
pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5;
|
||||
|
||||
pub struct BufferDiff {
|
||||
@@ -247,12 +240,10 @@ impl BufferDiffSnapshot {
|
||||
base_text_exists = false;
|
||||
};
|
||||
|
||||
let hunks = cx
|
||||
.background_executor()
|
||||
.spawn_labeled(*CALCULATE_DIFF_TASK, {
|
||||
let buffer = buffer.clone();
|
||||
async move { compute_hunks(base_text_pair, buffer, diff_options) }
|
||||
});
|
||||
let hunks = cx.background_executor().spawn({
|
||||
let buffer = buffer.clone();
|
||||
async move { compute_hunks(base_text_pair, buffer, diff_options) }
|
||||
});
|
||||
|
||||
async move {
|
||||
let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
|
||||
@@ -285,18 +276,17 @@ impl BufferDiffSnapshot {
|
||||
debug_assert_eq!(&*text, &base_text_snapshot.text());
|
||||
(text, base_text_snapshot.as_rope().clone())
|
||||
});
|
||||
cx.background_executor()
|
||||
.spawn_labeled(*CALCULATE_DIFF_TASK, async move {
|
||||
Self {
|
||||
inner: BufferDiffInner {
|
||||
base_text: base_text_snapshot,
|
||||
pending_hunks: SumTree::new(&buffer),
|
||||
hunks: compute_hunks(base_text_pair, buffer, diff_options),
|
||||
base_text_exists,
|
||||
},
|
||||
secondary_diff: None,
|
||||
}
|
||||
})
|
||||
cx.background_executor().spawn(async move {
|
||||
Self {
|
||||
inner: BufferDiffInner {
|
||||
base_text: base_text_snapshot,
|
||||
pending_hunks: SumTree::new(&buffer),
|
||||
hunks: compute_hunks(base_text_pair, buffer, diff_options),
|
||||
base_text_exists,
|
||||
},
|
||||
secondary_diff: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -32,7 +32,7 @@ struct Detect;
|
||||
|
||||
trait InstalledApp {
|
||||
fn zed_version_string(&self) -> String;
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
|
||||
fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()>;
|
||||
fn run_foreground(
|
||||
&self,
|
||||
ipc_url: String,
|
||||
@@ -588,7 +588,7 @@ fn main() -> Result<()> {
|
||||
if args.foreground {
|
||||
app.run_foreground(url, user_data_dir.as_deref())?;
|
||||
} else {
|
||||
app.launch(url)?;
|
||||
app.launch(url, user_data_dir.as_deref())?;
|
||||
sender.join().unwrap()?;
|
||||
if let Some(handle) = stdin_pipe_handle {
|
||||
handle.join().unwrap()?;
|
||||
@@ -709,14 +709,18 @@ mod linux {
|
||||
)
|
||||
}
|
||||
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
let sock_path = paths::data_dir().join(format!(
|
||||
fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
|
||||
let data_dir = user_data_dir
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| paths::data_dir().clone());
|
||||
|
||||
let sock_path = data_dir.join(format!(
|
||||
"zed-{}.sock",
|
||||
*release_channel::RELEASE_CHANNEL_NAME
|
||||
));
|
||||
let sock = UnixDatagram::unbound()?;
|
||||
if sock.connect(&sock_path).is_err() {
|
||||
self.boot_background(ipc_url)?;
|
||||
self.boot_background(ipc_url, user_data_dir)?;
|
||||
} else {
|
||||
sock.send(ipc_url.as_bytes())?;
|
||||
}
|
||||
@@ -742,7 +746,11 @@ mod linux {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
fn boot_background(
|
||||
&self,
|
||||
ipc_url: String,
|
||||
user_data_dir: Option<&str>,
|
||||
) -> anyhow::Result<()> {
|
||||
let path = &self.0;
|
||||
|
||||
match fork::fork() {
|
||||
@@ -756,8 +764,13 @@ mod linux {
|
||||
if fork::close_fd().is_err() {
|
||||
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
let error =
|
||||
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
|
||||
let mut args: Vec<OsString> =
|
||||
vec![path.as_os_str().to_owned(), OsString::from(ipc_url)];
|
||||
if let Some(dir) = user_data_dir {
|
||||
args.push(OsString::from("--user-data-dir"));
|
||||
args.push(OsString::from(dir));
|
||||
}
|
||||
let error = exec::execvp(path.clone(), &args);
|
||||
// if exec succeeded, we never get here.
|
||||
eprintln!("failed to exec {:?}: {}", path, error);
|
||||
process::exit(1)
|
||||
@@ -943,11 +956,14 @@ mod windows {
|
||||
)
|
||||
}
|
||||
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
fn launch(&self, ipc_url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
|
||||
if check_single_instance() {
|
||||
std::process::Command::new(self.0.clone())
|
||||
.arg(ipc_url)
|
||||
.spawn()?;
|
||||
let mut cmd = std::process::Command::new(self.0.clone());
|
||||
cmd.arg(ipc_url);
|
||||
if let Some(dir) = user_data_dir {
|
||||
cmd.arg("--user-data-dir").arg(dir);
|
||||
}
|
||||
cmd.spawn()?;
|
||||
} else {
|
||||
unsafe {
|
||||
let pipe = CreateFileW(
|
||||
@@ -1096,7 +1112,7 @@ mod mac_os {
|
||||
format!("Zed {} – {}", self.version(), self.path().display(),)
|
||||
}
|
||||
|
||||
fn launch(&self, url: String) -> anyhow::Result<()> {
|
||||
fn launch(&self, url: String, user_data_dir: Option<&str>) -> anyhow::Result<()> {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => {
|
||||
let app_path = app_bundle;
|
||||
@@ -1146,8 +1162,11 @@ mod mac_os {
|
||||
format!("Cloning descriptor for file {subprocess_stdout_file:?}")
|
||||
})?;
|
||||
let mut command = std::process::Command::new(executable);
|
||||
let command = command
|
||||
.env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
|
||||
command.env(FORCE_CLI_MODE_ENV_VAR_NAME, "");
|
||||
if let Some(dir) = user_data_dir {
|
||||
command.arg("--user-data-dir").arg(dir);
|
||||
}
|
||||
command
|
||||
.stderr(subprocess_stdout_file)
|
||||
.stdout(subprocess_stdin_file)
|
||||
.arg(url);
|
||||
|
||||
@@ -53,7 +53,7 @@ text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http.workspace = true
|
||||
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
|
||||
tokio-socks.workspace = true
|
||||
tokio.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user