Compare commits
23 Commits
debugger-e
...
action-eva
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
884fa381a2 | ||
|
|
64ba403616 | ||
|
|
43b7377e2d | ||
|
|
1371c326eb | ||
|
|
ea60c89b4b | ||
|
|
0494c1186f | ||
|
|
c2e50f6aa2 | ||
|
|
e262532c1d | ||
|
|
694ba12fb6 | ||
|
|
a49463d810 | ||
|
|
467b1beeb9 | ||
|
|
0fe4aa76b1 | ||
|
|
c7b4e5e1ac | ||
|
|
8aea590646 | ||
|
|
9cf88d21aa | ||
|
|
50418ab113 | ||
|
|
4fe6ae52f8 | ||
|
|
e9bb15b906 | ||
|
|
f77e752908 | ||
|
|
28b1e08c0c | ||
|
|
69b6aef396 | ||
|
|
6e825281de | ||
|
|
5d5377d838 |
@@ -1 +0,0 @@
|
||||
.rules
|
||||
@@ -20,6 +20,7 @@ platforms = [
|
||||
[traversal-excludes]
|
||||
workspace-members = [
|
||||
"remote_server",
|
||||
"eval",
|
||||
]
|
||||
third-party = [
|
||||
{ name = "reqwest", version = "0.11.27" },
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
.rules
|
||||
2
.github/actions/run_tests/action.yml
vendored
2
.github/actions/run_tests/action.yml
vendored
@@ -10,7 +10,7 @@ runs:
|
||||
cargo install cargo-nextest --locked
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/actions/run_tests_windows/action.yml
vendored
2
.github/actions/run_tests_windows/action.yml
vendored
@@ -16,7 +16,7 @@ runs:
|
||||
run: cargo install cargo-nextest --locked
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -325,7 +325,7 @@ jobs:
|
||||
cache-provider: "buildjet"
|
||||
|
||||
- name: Install Clang & Mold
|
||||
run: ./script/remote-server && ./script/install-mold 2.34.0
|
||||
run: ./script/headless && ./script/install-mold 2.34.0
|
||||
|
||||
- name: Configure CI
|
||||
run: |
|
||||
@@ -519,7 +519,7 @@ jobs:
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
2
.github/workflows/issue_response.yml
vendored
2
.github/workflows/issue_response.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
2
.github/workflows/randomized_tests.yml
vendored
2
.github/workflows/randomized_tests.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
|
||||
70
.github/workflows/run_agent_eval_daily.yml
vendored
70
.github/workflows/run_agent_eval_daily.yml
vendored
@@ -11,8 +11,8 @@ env:
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
run_eval:
|
||||
name: Run Eval
|
||||
build_binary:
|
||||
name: Build Eval Binary
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -21,8 +21,70 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libx11-dev libxcb-shape0-dev libxcb-xfixes0-dev libxcb1-dev libxcb-render0-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-keysyms1-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxfixes-dev pkg-config libasound2-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev
|
||||
./script/headless && ./script/install-mold 2.34.0
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Run cargo eval
|
||||
run: cargo run -p eval
|
||||
- name: Build entire Zed application
|
||||
run: cargo build --release
|
||||
|
||||
- name: Build eval binary separately to ensure it's available
|
||||
run: cargo build --release -p eval
|
||||
|
||||
- name: Upload eval binary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: eval-binary
|
||||
path: target/release/eval
|
||||
|
||||
run_eval:
|
||||
name: Run Eval - ${{ matrix.exercise }}
|
||||
needs: build_binary
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
exercise:
|
||||
- find_and_replace_diff_card
|
||||
- metal_i64_support
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libx11-dev libxcb-shape0-dev libxcb-xfixes0-dev libxcb1-dev libxcb-render0-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-keysyms1-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxfixes-dev pkg-config libasound2-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev
|
||||
./script/headless && ./script/install-mold 2.34.0
|
||||
|
||||
- name: Create required directories
|
||||
run: |
|
||||
mkdir -p /home/runner/.config/zed
|
||||
touch /home/runner/.config/zed/settings.json
|
||||
mkdir -p ./crates/eval/repos
|
||||
mkdir -p ./crates/eval/worktrees
|
||||
mkdir -p ./crates/eval/runs
|
||||
|
||||
- name: Set up Anthropic API key
|
||||
run: echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Download eval binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: eval-binary
|
||||
path: ./target/release/
|
||||
|
||||
- name: Make eval binary executable
|
||||
run: chmod +x ./target/release/eval
|
||||
|
||||
- name: Run eval for ${{ matrix.exercise }}
|
||||
run: ./target/release/eval --cohort-id dailyrun${{ github.run_id }} ${{ matrix.exercise }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,6 +18,7 @@
|
||||
.venv
|
||||
.vscode
|
||||
.wrangler
|
||||
/.direnv
|
||||
/assets/*licenses.*
|
||||
/crates/collab/seed.json
|
||||
/crates/theme/schemas/theme.json
|
||||
|
||||
121
.rules
121
.rules
@@ -1,121 +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 create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
||||
|
||||
# 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 eachother 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 which have no data inside are created and registered with the `actions!(some_namespace, [SomeAction, AnotherAction])` macro call.
|
||||
|
||||
Actions that do have data must implement `Clone, Default, PartialEq, Deserialize, JsonSchema` and can be registered with an `impl_actions!(some_namespace, [SomeActionWithData])` macro call.
|
||||
|
||||
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 longer exists 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.
|
||||
@@ -1 +0,0 @@
|
||||
.rules
|
||||
@@ -45,6 +45,5 @@
|
||||
"hard_tabs": false,
|
||||
"formatter": "auto",
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"ensure_final_newline_on_save": true,
|
||||
"file_scan_exclusions": ["crates/eval/worktrees/", "crates/eval/repos/"]
|
||||
"ensure_final_newline_on_save": true
|
||||
}
|
||||
|
||||
111
Cargo.lock
generated
111
Cargo.lock
generated
@@ -1344,13 +1344,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-bedrockruntime"
|
||||
version = "1.82.0"
|
||||
version = "1.80.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cb95f77abd4321348dd2f52a25e1de199732f54d2a35860ad20f5df21c66b44"
|
||||
checksum = "39ee8ef191b908d013659ca2c0670215f0c920c781998e1dc55904d6bdb73b51"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
@@ -1362,7 +1361,6 @@ dependencies = [
|
||||
"bytes 1.10.1",
|
||||
"fastrand 2.3.0",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.32",
|
||||
"once_cell",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
@@ -2726,9 +2724,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.36"
|
||||
version = "4.5.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
|
||||
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -2736,9 +2734,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.36"
|
||||
version = "4.5.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
|
||||
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -3042,7 +3040,6 @@ dependencies = [
|
||||
"strum 0.27.1",
|
||||
"subtle",
|
||||
"supermaven_api",
|
||||
"task",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -4014,7 +4011,6 @@ dependencies = [
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"proto",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4190,7 +4186,6 @@ dependencies = [
|
||||
"command_palette_hooks",
|
||||
"dap",
|
||||
"db",
|
||||
"debugger_tools",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
@@ -4200,7 +4195,6 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"parking_lot",
|
||||
"picker",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
@@ -4901,6 +4895,7 @@ dependencies = [
|
||||
"client",
|
||||
"collections",
|
||||
"context_server",
|
||||
"dap",
|
||||
"dirs 5.0.1",
|
||||
"env_logger 0.11.8",
|
||||
"extension",
|
||||
@@ -4921,15 +4916,14 @@ dependencies = [
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"shellexpand 2.1.2",
|
||||
"smol",
|
||||
"telemetry",
|
||||
"toml 0.8.20",
|
||||
"unindent",
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6175,7 +6169,6 @@ dependencies = [
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows-numerics",
|
||||
"windows-registry 0.5.1",
|
||||
"workspace-hack",
|
||||
"x11-clipboard",
|
||||
"x11rb",
|
||||
@@ -7118,12 +7111,10 @@ dependencies = [
|
||||
name = "inline_completion"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7154,7 +7145,6 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
@@ -7656,6 +7646,7 @@ dependencies = [
|
||||
"http_client",
|
||||
"icons",
|
||||
"image",
|
||||
"log",
|
||||
"open_ai",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
@@ -7717,7 +7708,6 @@ dependencies = [
|
||||
"mistral",
|
||||
"ollama",
|
||||
"open_ai",
|
||||
"partial-json-fixer",
|
||||
"project",
|
||||
"proto",
|
||||
"schemars",
|
||||
@@ -7895,9 +7885,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
version = "0.2.171"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -7949,9 +7939,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.42"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
|
||||
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -8622,9 +8612,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.46"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
|
||||
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
@@ -9833,12 +9823,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "partial-json-fixer"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35ffd90b3f3b6477db7478016b9efb1b7e9d38eafd095f0542fe0ec2ea884a13"
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.4.2"
|
||||
@@ -10800,9 +10784,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -11754,7 +11738,6 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"env_logger 0.11.8",
|
||||
"extension",
|
||||
"extension_host",
|
||||
@@ -11943,7 +11926,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"windows-registry 0.2.0",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12641,9 +12624,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm"
|
||||
version = "1.1.10"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21e61af841881c137d4bc8e0d8411cee9168548b404f9e4788e8af7e8f94bd4e"
|
||||
checksum = "013d6c9e421b9c44c6eb6ebbee283b2a2c90eab2682088c4a2449706a42d117f"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
@@ -12670,9 +12653,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sea-orm-macros"
|
||||
version = "1.1.10"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6b86e3e77b548e6c6c1f612a1ca024d557dffdb81b838bf482ad3222140c77b"
|
||||
checksum = "31feeff3ad5e999c64b2b8fd30933b2871911567c4d62dcf70b3effd970c7891"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
@@ -13454,9 +13437,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.8.5"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e"
|
||||
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@@ -13467,11 +13450,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.8.5"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3"
|
||||
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bigdecimal",
|
||||
"bytes 1.10.1",
|
||||
"chrono",
|
||||
@@ -13492,6 +13474,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"rust_decimal",
|
||||
"rustls 0.23.26",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -13508,9 +13491,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.8.5"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce"
|
||||
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -13521,9 +13504,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.8.5"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7"
|
||||
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -13547,9 +13530,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.8.5"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7"
|
||||
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
@@ -13594,9 +13577,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.5"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6"
|
||||
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
@@ -13637,9 +13620,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.8.5"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc"
|
||||
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
@@ -13655,7 +13638,6 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_urlencoded",
|
||||
"sqlx-core",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -14226,11 +14208,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"dap-types",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"hex",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -15851,6 +15833,7 @@ dependencies = [
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"libc",
|
||||
"log",
|
||||
"lsp",
|
||||
"multi_buffer",
|
||||
@@ -17057,17 +17040,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result 0.3.2",
|
||||
"windows-strings 0.4.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
@@ -18349,7 +18321,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"schemars",
|
||||
"serde",
|
||||
"uuid",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -18396,9 +18367,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.6.1"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad17428120f5ca776dc5195e2411a282f5150a26d5536671f8943c622c31274f"
|
||||
checksum = "b91b8b05f1028157205026e525869eb860fa89bec87ea60b445efc91d05df31f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
@@ -480,7 +480,6 @@ num-format = "0.4.4"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking_lot = "0.12.1"
|
||||
partial-json-fixer = "0.5.3"
|
||||
pathdiff = "0.2"
|
||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
@@ -605,7 +604,7 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.6.1"
|
||||
zed_llm_client = "0.6.0"
|
||||
zstd = "0.11"
|
||||
metal = "0.29"
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ ENV CARGO_TERM_COLOR=always
|
||||
|
||||
COPY script/install-mold script/
|
||||
RUN ./script/install-mold "2.34.0"
|
||||
COPY script/remote-server script/
|
||||
RUN ./script/remote-server
|
||||
COPY script/headless script/
|
||||
RUN ./script/headless
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -49,6 +49,15 @@
|
||||
"down": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Prompt",
|
||||
"bindings": {
|
||||
"left": "menu::SelectPrevious",
|
||||
"right": "menu::SelectNext",
|
||||
"h": "menu::SelectPrevious",
|
||||
"l": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
@@ -130,6 +139,24 @@
|
||||
"ctrl-shift-alt-backspace": "editor::GoToNextChange"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-r": "git::Restore",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentDiff",
|
||||
"bindings": {
|
||||
"ctrl-y": "agent::Keep",
|
||||
"ctrl-n": "agent::Reject",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
@@ -178,31 +205,6 @@
|
||||
"ctrl-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-r": "git::Restore",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentDiff",
|
||||
"bindings": {
|
||||
"ctrl-y": "agent::Keep",
|
||||
"ctrl-n": "agent::Reject",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
@@ -218,93 +220,6 @@
|
||||
"ctrl-n": "assistant::NewChat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-shift-enter": "assistant::Edit",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"save": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel",
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-alt-p": "assistant::OpenPromptLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-e": "agent::ChatMode",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"bindings": {
|
||||
"up": "agent::FocusUp",
|
||||
"right": "agent::FocusRight",
|
||||
"left": "agent::FocusLeft",
|
||||
"down": "agent::FocusDown",
|
||||
"backspace": "agent::RemoveFocusedContext",
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"backspace": "agent::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptLibrary",
|
||||
"bindings": {
|
||||
@@ -687,6 +602,100 @@
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "repl::Run",
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-shift-enter": "assistant::Edit",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"save": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel",
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-alt-p": "assistant::OpenPromptLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-e": "agent::ChatMode",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"bindings": {
|
||||
"up": "agent::FocusUp",
|
||||
"right": "agent::FocusRight",
|
||||
"left": "agent::FocusLeft",
|
||||
"down": "agent::FocusDown",
|
||||
"backspace": "agent::RemoveFocusedContext",
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"backspace": "agent::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"bindings": {
|
||||
@@ -695,15 +704,6 @@
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Prompt",
|
||||
"bindings": {
|
||||
"left": "menu::SelectPrevious",
|
||||
"right": "menu::SelectNext",
|
||||
"h": "menu::SelectPrevious",
|
||||
"l": "menu::SelectNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
@@ -920,7 +920,6 @@
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
// Overrides for conflicting keybindings
|
||||
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
|
||||
@@ -1005,7 +1005,6 @@
|
||||
"alt-right": ["terminal::SendText", "\u001bf"],
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
|
||||
@@ -830,13 +830,5 @@
|
||||
// and Windows.
|
||||
"alt-l": "editor::AcceptEditPrediction"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Fixes https://github.com/zed-industries/zed/issues/29095 by ensuring that
|
||||
// the last binding for editor::ToggleComments is not ctrl-c.
|
||||
"context": "hack_to_fix_ctrl-c",
|
||||
"bindings": {
|
||||
"g c": "editor::ToggleComments"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8,6 +8,16 @@ You are a highly skilled software engineer with extensive knowledge in many prog
|
||||
4. NEVER lie or make things up.
|
||||
5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing.
|
||||
|
||||
## Searching and Reading
|
||||
|
||||
If you are unsure about the answer to the user's request or how to satiate their request, you should gather more information.
|
||||
This can be done with additional tool calls, asking clarifying questions, etc.
|
||||
|
||||
For example, if you've performed a semantic search, and the results may not fully answer the user's request, or merit gathering more information, feel free to call more tools. Similarly, if you've performed an edit that may partially
|
||||
satiate the user's query, but you're not confident, gather more information or use more tools before ending your turn.
|
||||
|
||||
Bias towards not asking the user for help if you can find the answer yourself.
|
||||
|
||||
## Tool Use
|
||||
|
||||
1. Make sure to adhere to the tools schema.
|
||||
@@ -16,22 +26,6 @@ You are a highly skilled software engineer with extensive knowledge in many prog
|
||||
4. Use only the tools that are currently available.
|
||||
5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
||||
|
||||
## Searching and Reading
|
||||
|
||||
If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions.
|
||||
|
||||
{{! TODO: If there are files, we should mention it but otherwise omit that fact }}
|
||||
If appropriate, use tool calls to explore the current project, which contains the following root directories:
|
||||
|
||||
{{#each worktrees}}
|
||||
- `{{root_name}}`
|
||||
{{/each}}
|
||||
|
||||
- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.
|
||||
- When looking for symbols in the project, prefer the `grep` tool.
|
||||
- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project.
|
||||
- Bias towards not asking the user for help if you can find the answer yourself.
|
||||
|
||||
## Fixing Diagnostics
|
||||
|
||||
1. Make 1-2 attempts at fixing diagnostics, then defer to the user.
|
||||
@@ -56,13 +50,19 @@ Otherwise, follow debugging best practices:
|
||||
Operating System: {{os}}
|
||||
Default Shell: {{shell}}
|
||||
|
||||
{{#if (or has_rules has_default_user_rules)}}
|
||||
The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files:
|
||||
|
||||
{{#each worktrees}}
|
||||
- `{{root_name}}` (absolute path: `{{abs_path}}`)
|
||||
{{/each}}
|
||||
|
||||
{{#if has_rules}}
|
||||
|
||||
## User's Custom Instructions
|
||||
|
||||
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the tool use guidelines.
|
||||
|
||||
{{#if has_rules}}
|
||||
There are project rules that apply to these root directories:
|
||||
There are rules that apply to these root directories:
|
||||
{{#each worktrees}}
|
||||
{{#if rules_file}}
|
||||
`{{root_name}}/{{rules_file.path_in_worktree}}`:
|
||||
@@ -72,17 +72,3 @@ There are project rules that apply to these root directories:
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if has_user_rules}}
|
||||
The user has specified the following rules that should be applied:
|
||||
{{#each user_rules}}
|
||||
|
||||
{{#if title}}
|
||||
Rules title: {{title}}
|
||||
{{/if}}
|
||||
``````
|
||||
{{contents}}}
|
||||
``````
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -181,6 +181,8 @@
|
||||
"current_line_highlight": "all",
|
||||
// Whether to highlight all occurrences of the selected text in an editor.
|
||||
"selection_highlight": true,
|
||||
// The debounce delay before querying highlights based on the selected text.
|
||||
"selection_highlight_debounce": 50,
|
||||
// The debounce delay before querying highlights from the language
|
||||
// server based on the current cursor location.
|
||||
"lsp_highlight_debounce": 75,
|
||||
@@ -212,7 +214,14 @@
|
||||
// The default number of lines to expand excerpts in the multibuffer by.
|
||||
"expand_excerpt_lines": 3,
|
||||
// Globs to match against file paths to determine if a file is private.
|
||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
||||
"private_files": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
],
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
"use_on_type_format": true,
|
||||
@@ -648,7 +657,7 @@
|
||||
"now": true,
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"grep": true,
|
||||
"regex_search": true,
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
@@ -672,7 +681,7 @@
|
||||
"now": false,
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"grep": true,
|
||||
"regex_search": true,
|
||||
"rename": false,
|
||||
"symbol_info": false,
|
||||
"terminal": true,
|
||||
@@ -712,7 +721,9 @@
|
||||
// The list of language servers to use (or disable) for all languages.
|
||||
//
|
||||
// This is typically customized on a per-language basis.
|
||||
"language_servers": ["..."],
|
||||
"language_servers": [
|
||||
"..."
|
||||
],
|
||||
// When to automatically save edited buffers. This setting can
|
||||
// take four values.
|
||||
//
|
||||
@@ -908,7 +919,9 @@
|
||||
// for files that are not tracked by git, but are still important to your project. Note that globs
|
||||
// that are overly broad can slow down Zed's file scanning. `file_scan_exclusions` takes
|
||||
// precedence over these inclusions.
|
||||
"file_scan_inclusions": [".env*"],
|
||||
"file_scan_inclusions": [
|
||||
".env*"
|
||||
],
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
@@ -960,7 +973,15 @@
|
||||
// Any addition to this list will be merged with the default list.
|
||||
// Globs are matched relative to the worktree root,
|
||||
// except when starting with a slash (/) or equivalent in Windows.
|
||||
"disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
|
||||
"disabled_globs": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/.dev.vars",
|
||||
"**/secrets.yml"
|
||||
],
|
||||
// When to show edit predictions previews in buffer.
|
||||
// This setting takes two possible values:
|
||||
// 1. Display predictions inline when there are no language server completions available.
|
||||
@@ -1093,7 +1114,12 @@
|
||||
// Default directories to search for virtual environments, relative
|
||||
// to the current working directory. We recommend overriding this
|
||||
// in your project's settings, rather than globally.
|
||||
"directories": [".env", "env", ".venv", "venv"],
|
||||
"directories": [
|
||||
".env",
|
||||
"env",
|
||||
".venv",
|
||||
"venv"
|
||||
],
|
||||
// Can also be `csh`, `fish`, `nushell` and `power_shell`
|
||||
"activate_script": "default"
|
||||
}
|
||||
@@ -1157,8 +1183,15 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
|
||||
"Shell Script": [".env.*"]
|
||||
"JSONC": [
|
||||
"**/.zed/**/*.json",
|
||||
"**/zed/**/*.json",
|
||||
"**/Zed/**/*.json",
|
||||
"**/.vscode/**/*.json"
|
||||
],
|
||||
"Shell Script": [
|
||||
".env.*"
|
||||
]
|
||||
},
|
||||
// By default use a recent system version of node, or install our own.
|
||||
// You can override this to use a version of node that is not in $PATH with:
|
||||
@@ -1231,10 +1264,15 @@
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Astro": {
|
||||
"language_servers": ["astro-language-server", "..."],
|
||||
"language_servers": [
|
||||
"astro-language-server",
|
||||
"..."
|
||||
],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-astro"]
|
||||
"plugins": [
|
||||
"prettier-plugin-astro"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Blade": {
|
||||
@@ -1270,10 +1308,19 @@
|
||||
"ensure_final_newline_on_save": false
|
||||
},
|
||||
"Elixir": {
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
"language_servers": [
|
||||
"elixir-ls",
|
||||
"!next-ls",
|
||||
"!lexical",
|
||||
"..."
|
||||
]
|
||||
},
|
||||
"Erlang": {
|
||||
"language_servers": ["erlang-ls", "!elp", "..."]
|
||||
"language_servers": [
|
||||
"erlang-ls",
|
||||
"!elp",
|
||||
"..."
|
||||
]
|
||||
},
|
||||
"Git Commit": {
|
||||
"allow_rewrap": "anywhere"
|
||||
@@ -1289,7 +1336,12 @@
|
||||
}
|
||||
},
|
||||
"HEEX": {
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
"language_servers": [
|
||||
"elixir-ls",
|
||||
"!next-ls",
|
||||
"!lexical",
|
||||
"..."
|
||||
]
|
||||
},
|
||||
"HTML": {
|
||||
"prettier": {
|
||||
@@ -1299,11 +1351,17 @@
|
||||
"Java": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-java"]
|
||||
"plugins": [
|
||||
"prettier-plugin-java"
|
||||
]
|
||||
}
|
||||
},
|
||||
"JavaScript": {
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"language_servers": [
|
||||
"!typescript-language-server",
|
||||
"vtsls",
|
||||
"..."
|
||||
],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -1321,7 +1379,10 @@
|
||||
"LaTeX": {
|
||||
"format_on_save": "on",
|
||||
"formatter": "language_server",
|
||||
"language_servers": ["texlab", "..."],
|
||||
"language_servers": [
|
||||
"texlab",
|
||||
"..."
|
||||
],
|
||||
"prettier": {
|
||||
"allowed": false
|
||||
}
|
||||
@@ -1336,10 +1397,16 @@
|
||||
}
|
||||
},
|
||||
"PHP": {
|
||||
"language_servers": ["phpactor", "!intelephense", "..."],
|
||||
"language_servers": [
|
||||
"phpactor",
|
||||
"!intelephense",
|
||||
"..."
|
||||
],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-php"],
|
||||
"plugins": [
|
||||
"@prettier/plugin-php"
|
||||
],
|
||||
"parser": "php"
|
||||
}
|
||||
},
|
||||
@@ -1347,7 +1414,12 @@
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Ruby": {
|
||||
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "..."]
|
||||
"language_servers": [
|
||||
"solargraph",
|
||||
"!ruby-lsp",
|
||||
"!rubocop",
|
||||
"..."
|
||||
]
|
||||
},
|
||||
"SCSS": {
|
||||
"prettier": {
|
||||
@@ -1357,21 +1429,36 @@
|
||||
"SQL": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-sql"]
|
||||
"plugins": [
|
||||
"prettier-plugin-sql"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Starlark": {
|
||||
"language_servers": ["starpls", "!buck2-lsp", "..."]
|
||||
"language_servers": [
|
||||
"starpls",
|
||||
"!buck2-lsp",
|
||||
"..."
|
||||
]
|
||||
},
|
||||
"Svelte": {
|
||||
"language_servers": ["svelte-language-server", "..."],
|
||||
"language_servers": [
|
||||
"svelte-language-server",
|
||||
"..."
|
||||
],
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["prettier-plugin-svelte"]
|
||||
"plugins": [
|
||||
"prettier-plugin-svelte"
|
||||
]
|
||||
}
|
||||
},
|
||||
"TSX": {
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"language_servers": [
|
||||
"!typescript-language-server",
|
||||
"vtsls",
|
||||
"..."
|
||||
],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -1382,13 +1469,20 @@
|
||||
}
|
||||
},
|
||||
"TypeScript": {
|
||||
"language_servers": ["!typescript-language-server", "vtsls", "..."],
|
||||
"language_servers": [
|
||||
"!typescript-language-server",
|
||||
"vtsls",
|
||||
"..."
|
||||
],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Vue.js": {
|
||||
"language_servers": ["vue-language-server", "..."],
|
||||
"language_servers": [
|
||||
"vue-language-server",
|
||||
"..."
|
||||
],
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -1396,7 +1490,9 @@
|
||||
"XML": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-xml"]
|
||||
"plugins": [
|
||||
"@prettier/plugin-xml"
|
||||
]
|
||||
}
|
||||
},
|
||||
"YAML": {
|
||||
@@ -1405,7 +1501,10 @@
|
||||
}
|
||||
},
|
||||
"Zig": {
|
||||
"language_servers": ["zls", "..."]
|
||||
"language_servers": [
|
||||
"zls",
|
||||
"..."
|
||||
]
|
||||
}
|
||||
},
|
||||
// Different settings for specific language models.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::context::{AssistantContext, ContextId, RULES_ICON, format_context_as_string};
|
||||
use crate::context::{AssistantContext, ContextId, format_context_as_string};
|
||||
use crate::context_picker::MentionLink;
|
||||
use crate::thread::{
|
||||
LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent,
|
||||
ThreadFeedback,
|
||||
LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
|
||||
ThreadEvent, ThreadFeedback,
|
||||
};
|
||||
use crate::thread_store::{RulesLoadingError, ThreadStore};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse};
|
||||
@@ -42,7 +42,6 @@ use ui::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{OpenOptions, Workspace};
|
||||
use zed_actions::assistant::OpenPromptLibrary;
|
||||
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
@@ -133,23 +132,18 @@ impl RenderedMessage {
|
||||
}
|
||||
|
||||
fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) {
|
||||
match segment {
|
||||
MessageSegment::Thinking { text, .. } => {
|
||||
self.segments.push(RenderedMessageSegment::Thinking {
|
||||
content: parse_markdown(text.into(), self.language_registry.clone(), cx),
|
||||
scroll_handle: ScrollHandle::default(),
|
||||
})
|
||||
}
|
||||
MessageSegment::Text(text) => {
|
||||
self.segments
|
||||
.push(RenderedMessageSegment::Text(parse_markdown(
|
||||
text.into(),
|
||||
self.language_registry.clone(),
|
||||
cx,
|
||||
)))
|
||||
}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
let rendered_segment = match segment {
|
||||
MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
|
||||
content: parse_markdown(text.into(), self.language_registry.clone(), cx),
|
||||
scroll_handle: ScrollHandle::default(),
|
||||
},
|
||||
MessageSegment::Text(text) => RenderedMessageSegment::Text(parse_markdown(
|
||||
text.into(),
|
||||
self.language_registry.clone(),
|
||||
cx,
|
||||
)),
|
||||
};
|
||||
self.segments.push(rendered_segment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +260,14 @@ fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tool_use_markdown(
|
||||
text: SharedString,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Markdown> {
|
||||
cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))
|
||||
}
|
||||
|
||||
fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
@@ -500,6 +502,7 @@ fn render_markdown_code_block(
|
||||
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
|
||||
|
||||
let codeblock_header = h_flex()
|
||||
.group("codeblock_header")
|
||||
.py_1()
|
||||
.pl_1p5()
|
||||
.pr_1()
|
||||
@@ -514,7 +517,7 @@ fn render_markdown_code_block(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div().visible_on_hover("codeblock_container").child(
|
||||
div().visible_on_hover("codeblock_header").child(
|
||||
IconButton::new(
|
||||
("copy-markdown-code", ix),
|
||||
if codeblock_was_copied {
|
||||
@@ -596,7 +599,6 @@ fn render_markdown_code_block(
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.group("codeblock_container")
|
||||
.my_2()
|
||||
.overflow_hidden()
|
||||
.rounded_lg()
|
||||
@@ -680,12 +682,6 @@ fn open_markdown_link(
|
||||
}
|
||||
}),
|
||||
Some(MentionLink::Fetch(url)) => cx.open_url(&url),
|
||||
Some(MentionLink::Rules(prompt_id)) => window.dispatch_action(
|
||||
Box::new(OpenPromptLibrary {
|
||||
prompt_to_select: Some(prompt_id.0),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
None => cx.open_url(&text),
|
||||
}
|
||||
}
|
||||
@@ -859,34 +855,21 @@ impl ActiveThread {
|
||||
tool_output: SharedString,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let rendered = self
|
||||
.rendered_tool_uses
|
||||
.entry(tool_use_id.clone())
|
||||
.or_insert_with(|| RenderedToolUse {
|
||||
label: cx.new(|cx| {
|
||||
Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
|
||||
}),
|
||||
input: cx.new(|cx| {
|
||||
Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
|
||||
}),
|
||||
output: cx.new(|cx| {
|
||||
Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
|
||||
}),
|
||||
});
|
||||
|
||||
rendered.label.update(cx, |this, cx| {
|
||||
this.replace(tool_label, cx);
|
||||
});
|
||||
rendered.input.update(cx, |this, cx| {
|
||||
let input = format!(
|
||||
"```json\n{}\n```",
|
||||
serde_json::to_string_pretty(tool_input).unwrap_or_default()
|
||||
);
|
||||
this.replace(input, cx);
|
||||
});
|
||||
rendered.output.update(cx, |this, cx| {
|
||||
this.replace(tool_output, cx);
|
||||
});
|
||||
let rendered = RenderedToolUse {
|
||||
label: render_tool_use_markdown(tool_label.into(), self.language_registry.clone(), cx),
|
||||
input: render_tool_use_markdown(
|
||||
format!(
|
||||
"```json\n{}\n```",
|
||||
serde_json::to_string_pretty(tool_input).unwrap_or_default()
|
||||
)
|
||||
.into(),
|
||||
self.language_registry.clone(),
|
||||
cx,
|
||||
),
|
||||
output: render_tool_use_markdown(tool_output, self.language_registry.clone(), cx),
|
||||
};
|
||||
self.rendered_tool_uses
|
||||
.insert(tool_use_id.clone(), rendered);
|
||||
}
|
||||
|
||||
fn handle_thread_event(
|
||||
@@ -979,19 +962,6 @@ impl ActiveThread {
|
||||
);
|
||||
}
|
||||
}
|
||||
ThreadEvent::StreamedToolUse {
|
||||
tool_use_id,
|
||||
ui_text,
|
||||
input,
|
||||
} => {
|
||||
self.render_tool_use_markdown(
|
||||
tool_use_id.clone(),
|
||||
ui_text.clone(),
|
||||
input,
|
||||
"".into(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
ThreadEvent::ToolFinished {
|
||||
pending_tool_use, ..
|
||||
} => {
|
||||
@@ -1241,8 +1211,6 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: vec![content.into()],
|
||||
@@ -1308,8 +1276,7 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model.model, cx)
|
||||
thread.send_to_model(model.model, RequestKind::Chat, cx)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
@@ -2496,15 +2463,13 @@ impl ActiveThread {
|
||||
let edit_tools = tool_use.needs_confirmation;
|
||||
|
||||
let status_icons = div().child(match &tool_use.status {
|
||||
ToolUseStatus::NeedsConfirmation => {
|
||||
ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
|
||||
let icon = Icon::new(IconName::Warning)
|
||||
.color(Color::Warning)
|
||||
.size(IconSize::Small);
|
||||
icon.into_any_element()
|
||||
}
|
||||
ToolUseStatus::Pending
|
||||
| ToolUseStatus::InputStillStreaming
|
||||
| ToolUseStatus::Running => {
|
||||
ToolUseStatus::Running => {
|
||||
let icon = Icon::new(IconName::ArrowCircle)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small);
|
||||
@@ -2590,7 +2555,7 @@ impl ActiveThread {
|
||||
}),
|
||||
)),
|
||||
),
|
||||
ToolUseStatus::InputStillStreaming | ToolUseStatus::Running => container.child(
|
||||
ToolUseStatus::Running => container.child(
|
||||
results_content_container().child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -2983,113 +2948,53 @@ impl ActiveThread {
|
||||
return div().into_any();
|
||||
};
|
||||
|
||||
let user_rules_text = if project_context.user_rules.is_empty() {
|
||||
None
|
||||
} else if project_context.user_rules.len() == 1 {
|
||||
let user_rules = &project_context.user_rules[0];
|
||||
|
||||
match user_rules.title.as_ref() {
|
||||
Some(title) => Some(format!("Using \"{title}\" user rule")),
|
||||
None => Some("Using user rule".into()),
|
||||
}
|
||||
} else {
|
||||
Some(format!(
|
||||
"Using {} user rules",
|
||||
project_context.user_rules.len()
|
||||
))
|
||||
};
|
||||
|
||||
let first_user_rules_id = project_context
|
||||
.user_rules
|
||||
.first()
|
||||
.map(|user_rules| user_rules.uuid.0);
|
||||
|
||||
let rules_files = project_context
|
||||
.worktrees
|
||||
.iter()
|
||||
.filter_map(|worktree| worktree.rules_file.as_ref())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let rules_file_text = match rules_files.as_slice() {
|
||||
&[] => None,
|
||||
&[rules_file] => Some(format!(
|
||||
"Using project {:?} file",
|
||||
rules_file.path_in_worktree
|
||||
)),
|
||||
rules_files => Some(format!("Using {} project rules files", rules_files.len())),
|
||||
let label_text = match rules_files.as_slice() {
|
||||
&[] => return div().into_any(),
|
||||
&[rules_file] => {
|
||||
format!("Using {:?} file", rules_file.path_in_worktree)
|
||||
}
|
||||
rules_files => {
|
||||
format!("Using {} rules files", rules_files.len())
|
||||
}
|
||||
};
|
||||
|
||||
if user_rules_text.is_none() && rules_file_text.is_none() {
|
||||
return div().into_any();
|
||||
}
|
||||
|
||||
v_flex()
|
||||
div()
|
||||
.pt_2()
|
||||
.px_2p5()
|
||||
.gap_1()
|
||||
.when_some(user_rules_text, |parent, user_rules_text| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
Icon::new(RULES_ICON)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(user_rules_text)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.truncate()
|
||||
.buffer_font(cx)
|
||||
.ml_1p5()
|
||||
.mr_0p5(),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-prompt-library", IconName::ArrowUpRightAlt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
// TODO: Figure out a way to pass focus handle here so we can display the `OpenPromptLibrary` keybinding
|
||||
.tooltip(Tooltip::text("View User Rules"))
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(OpenPromptLibrary {
|
||||
prompt_to_select: first_user_rules_id,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when_some(rules_file_text, |parent, rules_file_text| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(rules_file_text)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx)
|
||||
.ml_1p5()
|
||||
.mr_0p5(),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-rule", IconName::ArrowUpRightAlt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.on_click(cx.listener(Self::handle_open_rules))
|
||||
.tooltip(Tooltip::text("View Rules")),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(label_text)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-rule", IconName::ArrowUpRightAlt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.on_click(cx.listener(Self::handle_open_rules))
|
||||
.tooltip(Tooltip::text("View Rules")),
|
||||
),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -3339,12 +3244,6 @@ pub(crate) fn open_context(
|
||||
}
|
||||
})
|
||||
}
|
||||
AssistantContext::Rules(rules_context) => window.dispatch_action(
|
||||
Box::new(OpenPromptLibrary {
|
||||
prompt_to_select: Some(rules_context.prompt_id.0),
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -922,7 +922,6 @@ mod tests {
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AssistantSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
@@ -952,8 +951,7 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ pub use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
|
||||
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, Thread, ThreadEvent};
|
||||
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
|
||||
pub use crate::thread_store::ThreadStore;
|
||||
pub use agent_diff::{AgentDiff, AgentDiffToolbar};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{
|
||||
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
|
||||
Switch, SwitchColor, Tooltip, prelude::*,
|
||||
Switch, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
@@ -132,11 +132,7 @@ impl AssistantConfiguration {
|
||||
.cloned();
|
||||
|
||||
v_flex()
|
||||
.pt_3()
|
||||
.pb_1()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
@@ -148,7 +144,7 @@ impl AssistantConfiguration {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(provider_name.clone()).size(LabelSize::Large)),
|
||||
.child(Label::new(provider_name.clone())),
|
||||
)
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
@@ -173,12 +169,20 @@ impl AssistantConfiguration {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.map(|parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(div().child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
)))),
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.p(DynamicSpacing::Base08.rems(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.map(|parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(div().child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
)))),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_provider_configuration_section(
|
||||
@@ -195,7 +199,7 @@ impl AssistantConfiguration {
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers"))
|
||||
.child(Headline::new("LLM Providers").size(HeadlineSize::Small))
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
@@ -211,16 +215,21 @@ impl AssistantConfiguration {
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;
|
||||
|
||||
const HEADING: &str = "Allow running editing tools without asking for confirmation";
|
||||
const HEADING: &str = "Allow running tools without asking for confirmation";
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(Headline::new("General Settings"))
|
||||
.child(Headline::new("General Settings").size(HeadlineSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2p5()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
@@ -236,7 +245,6 @@ impl AssistantConfiguration {
|
||||
"always-allow-tool-actions-switch",
|
||||
always_allow_tool_actions.into(),
|
||||
)
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
@@ -269,7 +277,10 @@ impl AssistantConfiguration {
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||
.child(
|
||||
Headline::new("Model Context Protocol (MCP) Servers")
|
||||
.size(HeadlineSize::Small),
|
||||
)
|
||||
.child(Label::new(SUBHEADING).color(Color::Muted)),
|
||||
)
|
||||
.children(context_servers.into_iter().map(|context_server| {
|
||||
@@ -290,9 +301,9 @@ impl AssistantConfiguration {
|
||||
v_flex()
|
||||
.id(SharedString::from(context_server.id()))
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.rounded_sm()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().background.opacity(0.25))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.p_1()
|
||||
@@ -333,44 +344,41 @@ impl AssistantConfiguration {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Switch::new("context-server-switch", is_running.into())
|
||||
.color(SwitchColor::Accent)
|
||||
.on_click({
|
||||
let context_server_manager =
|
||||
self.context_server_manager.clone();
|
||||
let context_server = context_server.clone();
|
||||
move |state, _window, cx| match state {
|
||||
ToggleState::Unselected
|
||||
| ToggleState::Indeterminate => {
|
||||
context_server_manager.update(cx, |this, cx| {
|
||||
this.stop_server(context_server.clone(), cx)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
ToggleState::Selected => {
|
||||
cx.spawn({
|
||||
let context_server_manager =
|
||||
context_server_manager.clone();
|
||||
let context_server = context_server.clone();
|
||||
async move |cx| {
|
||||
if let Some(start_server_task) =
|
||||
context_server_manager
|
||||
.update(cx, |this, cx| {
|
||||
this.start_server(
|
||||
context_server,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
start_server_task.await.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Switch::new("context-server-switch", is_running.into()).on_click({
|
||||
let context_server_manager =
|
||||
self.context_server_manager.clone();
|
||||
let context_server = context_server.clone();
|
||||
move |state, _window, cx| match state {
|
||||
ToggleState::Unselected | ToggleState::Indeterminate => {
|
||||
context_server_manager.update(cx, |this, cx| {
|
||||
this.stop_server(context_server.clone(), cx)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
}),
|
||||
ToggleState::Selected => {
|
||||
cx.spawn({
|
||||
let context_server_manager =
|
||||
context_server_manager.clone();
|
||||
let context_server = context_server.clone();
|
||||
async move |cx| {
|
||||
if let Some(start_server_task) =
|
||||
context_server_manager
|
||||
.update(cx, |this, cx| {
|
||||
this.start_server(
|
||||
context_server,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
start_server_task.await.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.map(|parent| {
|
||||
@@ -378,28 +386,34 @@ impl AssistantConfiguration {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(v_flex().py_1p5().px_1().gap_1().children(
|
||||
tools.into_iter().enumerate().map(|(ix, tool)| {
|
||||
parent.child(v_flex().children(tools.into_iter().enumerate().map(
|
||||
|(ix, tool)| {
|
||||
h_flex()
|
||||
.id(("tool-item", ix))
|
||||
.px_1()
|
||||
.id("tool-item")
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.rounded_sm()
|
||||
.when(ix < tool_count - 1, |element| {
|
||||
element
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
})
|
||||
.child(
|
||||
Label::new(tool.name())
|
||||
.buffer_font(cx)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Ignored),
|
||||
IconButton::new(("tool-description", ix), IconName::Info)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text(tool.description())),
|
||||
)
|
||||
.tooltip(Tooltip::text(tool.description()))
|
||||
}),
|
||||
))
|
||||
},
|
||||
)))
|
||||
})
|
||||
}))
|
||||
.child(
|
||||
@@ -408,7 +422,7 @@ impl AssistantConfiguration {
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex().w_full().child(
|
||||
Button::new("add-context-server", "Add Custom Server")
|
||||
Button::new("add-context-server", "Add MCPs Directly")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.full_width()
|
||||
|
||||
@@ -2,7 +2,7 @@ use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
|
||||
use serde_json::json;
|
||||
use settings::update_settings_file;
|
||||
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use ui_input::SingleLineInput;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
@@ -34,9 +34,9 @@ impl AddContextServerModal {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let name_editor =
|
||||
cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
|
||||
cx.new(|cx| SingleLineInput::new(window, cx, "Your server name").label("Name"));
|
||||
let command_editor = cx.new(|cx| {
|
||||
SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
|
||||
SingleLineInput::new(window, cx, "Command").label("Command to run the context server")
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -46,7 +46,7 @@ impl AddContextServerModal {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
|
||||
fn confirm(&mut self, cx: &mut Context<Self>) {
|
||||
let name = self
|
||||
.name_editor
|
||||
.read(cx)
|
||||
@@ -96,7 +96,7 @@ impl AddContextServerModal {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
|
||||
fn cancel(&mut self, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
@@ -112,68 +112,38 @@ impl Focusable for AddContextServerModal {
|
||||
impl EventEmitter<DismissEvent> for AddContextServerModal {}
|
||||
|
||||
impl Render for AddContextServerModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_name_empty = self.name_editor.read(cx).is_empty(cx);
|
||||
let is_command_empty = self.command_editor.read(cx).is_empty(cx);
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.key_context("AddContextServerModal")
|
||||
.on_action(
|
||||
cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|this, _: &menu::Confirm, _window, cx| {
|
||||
this.confirm(&menu::Confirm, cx)
|
||||
}),
|
||||
)
|
||||
.on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(cx)))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
|
||||
.child(
|
||||
Modal::new("add-context-server", None)
|
||||
.header(ModalHeader::new().headline("Add MCP Server"))
|
||||
.header(ModalHeader::new().headline("Add Context Server"))
|
||||
.section(
|
||||
Section::new().child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(self.name_editor.clone())
|
||||
.child(self.command_editor.clone()),
|
||||
),
|
||||
Section::new()
|
||||
.child(self.name_editor.clone())
|
||||
.child(self.command_editor.clone()),
|
||||
)
|
||||
.footer(
|
||||
ModalFooter::new()
|
||||
.start_slot(
|
||||
Button::new("cancel", "Cancel")
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.cancel(&menu::Cancel, cx)
|
||||
})),
|
||||
Button::new("cancel", "Cancel").on_click(
|
||||
cx.listener(|this, _event, _window, cx| this.cancel(cx)),
|
||||
),
|
||||
)
|
||||
.end_slot(
|
||||
Button::new("add-server", "Add Server")
|
||||
.disabled(is_name_empty || is_command_empty)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.map(|button| {
|
||||
if is_name_empty {
|
||||
button.tooltip(Tooltip::text("Name is required"))
|
||||
@@ -183,9 +153,9 @@ impl Render for AddContextServerModal {
|
||||
button
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.confirm(&menu::Confirm, cx)
|
||||
})),
|
||||
.on_click(
|
||||
cx.listener(|this, _event, _window, cx| this.confirm(cx)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use assistant_settings::AssistantSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
@@ -9,12 +9,17 @@ use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
|
||||
pub use language_model_selector::ModelType;
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ModelType {
|
||||
Default,
|
||||
InlineAssistant,
|
||||
}
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
model_type: ModelType,
|
||||
}
|
||||
|
||||
impl AssistantModelSelector {
|
||||
@@ -58,13 +63,13 @@ impl AssistantModelSelector {
|
||||
}
|
||||
}
|
||||
},
|
||||
model_type,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
menu_handle,
|
||||
focus_handle,
|
||||
model_type,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +82,11 @@ impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let model = self.selector.read(cx).active_model(cx);
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let model = match self.model_type {
|
||||
ModelType::Default => model_registry.default_model(),
|
||||
ModelType::InlineAssistant => model_registry.inline_assistant_model(),
|
||||
};
|
||||
let (model_name, model_icon) = match model {
|
||||
Some(model) => (model.model.name().0, Some(model.provider.icon())),
|
||||
_ => (SharedString::from("No model selected"), None),
|
||||
|
||||
@@ -25,7 +25,7 @@ use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::{PromptBuilder, PromptId, UserPromptId};
|
||||
use prompt_store::PromptBuilder;
|
||||
use proto::Plan;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use time::UtcOffset;
|
||||
@@ -47,7 +47,7 @@ use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::UsageBanner;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
|
||||
AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
|
||||
OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
|
||||
};
|
||||
|
||||
@@ -79,11 +79,11 @@ pub fn init(cx: &mut App) {
|
||||
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, action: &OpenPromptLibrary, window, cx| {
|
||||
.register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_prompt_library(action, window, cx)
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -213,7 +213,7 @@ impl AssistantPanel {
|
||||
let project = workspace.project().clone();
|
||||
ThreadStore::load(project, tools.clone(), prompt_builder.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
|
||||
let context_store = workspace
|
||||
@@ -488,7 +488,7 @@ impl AssistantPanel {
|
||||
|
||||
fn deploy_prompt_library(
|
||||
&mut self,
|
||||
action: &OpenPromptLibrary,
|
||||
_: &OpenPromptLibrary,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -502,9 +502,6 @@ impl AssistantPanel {
|
||||
None,
|
||||
))
|
||||
}),
|
||||
action.prompt_to_select.map(|uuid| PromptId::User {
|
||||
uuid: UserPromptId(uuid),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -1122,19 +1119,17 @@ impl AssistantPanel {
|
||||
"New Text Thread",
|
||||
NewTextThread.boxed_clone(),
|
||||
)
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary::default()))
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.separator()
|
||||
.header("MCPs")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
"Install MCPs",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
}),
|
||||
)
|
||||
.action("Add Custom Server", Box::new(AddContextServer))
|
||||
},
|
||||
))
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::context::attach_context_to_message;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
@@ -131,12 +131,7 @@ impl BufferCodegen {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn start(
|
||||
&mut self,
|
||||
primary_model: Arc<dyn LanguageModel>,
|
||||
user_prompt: String,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
pub fn start(&mut self, user_prompt: String, cx: &mut Context<Self>) -> Result<()> {
|
||||
let alternative_models = LanguageModelRegistry::read_global(cx)
|
||||
.inline_alternative_models()
|
||||
.to_vec();
|
||||
@@ -160,6 +155,11 @@ impl BufferCodegen {
|
||||
}));
|
||||
}
|
||||
|
||||
let primary_model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.context("no active model")?
|
||||
.model;
|
||||
|
||||
for (model, alternative) in iter::once(primary_model)
|
||||
.chain(alternative_models)
|
||||
.zip(&self.alternatives)
|
||||
@@ -425,8 +425,6 @@ impl CodegenAlternative {
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
|
||||
@@ -4,7 +4,6 @@ use gpui::{App, Entity, SharedString};
|
||||
use language::{Buffer, File};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::{ProjectPath, Worktree};
|
||||
use prompt_store::UserPromptId;
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::{Anchor, BufferId};
|
||||
@@ -13,8 +12,6 @@ use util::post_inc;
|
||||
|
||||
use crate::thread::Thread;
|
||||
|
||||
pub const RULES_ICON: IconName = IconName::Context;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct ContextId(pub(crate) usize);
|
||||
|
||||
@@ -23,7 +20,6 @@ impl ContextId {
|
||||
Self(post_inc(&mut self.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
@@ -31,7 +27,6 @@ pub enum ContextKind {
|
||||
Excerpt,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
Rules,
|
||||
}
|
||||
|
||||
impl ContextKind {
|
||||
@@ -43,7 +38,6 @@ impl ContextKind {
|
||||
ContextKind::Excerpt => IconName::Code,
|
||||
ContextKind::FetchedUrl => IconName::Globe,
|
||||
ContextKind::Thread => IconName::MessageBubbles,
|
||||
ContextKind::Rules => RULES_ICON,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +50,6 @@ pub enum AssistantContext {
|
||||
FetchedUrl(FetchedUrlContext),
|
||||
Thread(ThreadContext),
|
||||
Excerpt(ExcerptContext),
|
||||
Rules(RulesContext),
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
@@ -68,7 +61,6 @@ impl AssistantContext {
|
||||
Self::FetchedUrl(url) => url.id,
|
||||
Self::Thread(thread) => thread.id,
|
||||
Self::Excerpt(excerpt) => excerpt.id,
|
||||
Self::Rules(rules) => rules.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,14 +168,6 @@ pub struct ExcerptContext {
|
||||
pub context_buffer: ContextBuffer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RulesContext {
|
||||
pub id: ContextId,
|
||||
pub prompt_id: UserPromptId,
|
||||
pub title: SharedString,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
/// Formats a collection of contexts into a string representation
|
||||
pub fn format_context_as_string<'a>(
|
||||
contexts: impl Iterator<Item = &'a AssistantContext>,
|
||||
@@ -195,7 +179,6 @@ pub fn format_context_as_string<'a>(
|
||||
let mut excerpt_context = Vec::new();
|
||||
let mut fetch_context = Vec::new();
|
||||
let mut thread_context = Vec::new();
|
||||
let mut rules_context = Vec::new();
|
||||
|
||||
for context in contexts {
|
||||
match context {
|
||||
@@ -205,7 +188,6 @@ pub fn format_context_as_string<'a>(
|
||||
AssistantContext::Excerpt(context) => excerpt_context.push(context),
|
||||
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
|
||||
AssistantContext::Thread(context) => thread_context.push(context),
|
||||
AssistantContext::Rules(context) => rules_context.push(context),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +197,6 @@ pub fn format_context_as_string<'a>(
|
||||
&& excerpt_context.is_empty()
|
||||
&& fetch_context.is_empty()
|
||||
&& thread_context.is_empty()
|
||||
&& rules_context.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -282,18 +263,6 @@ pub fn format_context_as_string<'a>(
|
||||
result.push_str("</conversation_threads>\n");
|
||||
}
|
||||
|
||||
if !rules_context.is_empty() {
|
||||
result.push_str(
|
||||
"<user_rules>\n\
|
||||
The user has specified the following rules that should be applied:\n\n",
|
||||
);
|
||||
for context in &rules_context {
|
||||
result.push_str(&context.text);
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str("</user_rules>\n");
|
||||
}
|
||||
|
||||
result.push_str("</context>\n");
|
||||
Some(result)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
mod completion_provider;
|
||||
mod fetch_context_picker;
|
||||
mod file_context_picker;
|
||||
mod rules_context_picker;
|
||||
mod symbol_context_picker;
|
||||
mod thread_context_picker;
|
||||
|
||||
@@ -19,22 +18,17 @@ use gpui::{
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::{Entry, ProjectPath};
|
||||
use prompt_store::UserPromptId;
|
||||
use rules_context_picker::RulesContextEntry;
|
||||
use symbol_context_picker::SymbolContextPicker;
|
||||
use thread_context_picker::{ThreadContextEntry, render_thread_context_entry};
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::AssistantPanel;
|
||||
use crate::context::RULES_ICON;
|
||||
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
|
||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||
use crate::context_picker::rules_context_picker::RulesContextPicker;
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::ThreadId;
|
||||
@@ -46,7 +40,6 @@ enum ContextPickerMode {
|
||||
Symbol,
|
||||
Fetch,
|
||||
Thread,
|
||||
Rules,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ContextPickerMode {
|
||||
@@ -58,7 +51,6 @@ impl TryFrom<&str> for ContextPickerMode {
|
||||
"symbol" => Ok(Self::Symbol),
|
||||
"fetch" => Ok(Self::Fetch),
|
||||
"thread" => Ok(Self::Thread),
|
||||
"rules" => Ok(Self::Rules),
|
||||
_ => Err(format!("Invalid context picker mode: {}", value)),
|
||||
}
|
||||
}
|
||||
@@ -71,7 +63,6 @@ impl ContextPickerMode {
|
||||
Self::Symbol => "symbol",
|
||||
Self::Fetch => "fetch",
|
||||
Self::Thread => "thread",
|
||||
Self::Rules => "rules",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +72,6 @@ impl ContextPickerMode {
|
||||
Self::Symbol => "Symbols",
|
||||
Self::Fetch => "Fetch",
|
||||
Self::Thread => "Threads",
|
||||
Self::Rules => "Rules",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +81,6 @@ impl ContextPickerMode {
|
||||
Self::Symbol => IconName::Code,
|
||||
Self::Fetch => IconName::Globe,
|
||||
Self::Thread => IconName::MessageBubbles,
|
||||
Self::Rules => RULES_ICON,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +92,6 @@ enum ContextPickerState {
|
||||
Symbol(Entity<SymbolContextPicker>),
|
||||
Fetch(Entity<FetchContextPicker>),
|
||||
Thread(Entity<ThreadContextPicker>),
|
||||
Rules(Entity<RulesContextPicker>),
|
||||
}
|
||||
|
||||
pub(super) struct ContextPicker {
|
||||
@@ -265,19 +253,6 @@ impl ContextPicker {
|
||||
}));
|
||||
}
|
||||
}
|
||||
ContextPickerMode::Rules => {
|
||||
if let Some(thread_store) = self.thread_store.as_ref() {
|
||||
self.mode = ContextPickerState::Rules(cx.new(|cx| {
|
||||
RulesContextPicker::new(
|
||||
thread_store.clone(),
|
||||
context_picker.clone(),
|
||||
self.context_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -406,7 +381,6 @@ impl ContextPicker {
|
||||
ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,7 +395,6 @@ impl Focusable for ContextPicker {
|
||||
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
|
||||
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
||||
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
|
||||
ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -437,9 +410,6 @@ impl Render for ContextPicker {
|
||||
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
|
||||
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
||||
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
|
||||
ContextPickerState::Rules(user_rules_picker) => {
|
||||
parent.child(user_rules_picker.clone())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -461,7 +431,6 @@ fn supported_context_picker_modes(
|
||||
];
|
||||
if thread_store.is_some() {
|
||||
modes.push(ContextPickerMode::Thread);
|
||||
modes.push(ContextPickerMode::Rules);
|
||||
}
|
||||
modes
|
||||
}
|
||||
@@ -657,7 +626,6 @@ pub enum MentionLink {
|
||||
Symbol(ProjectPath, String),
|
||||
Fetch(String),
|
||||
Thread(ThreadId),
|
||||
Rules(UserPromptId),
|
||||
}
|
||||
|
||||
impl MentionLink {
|
||||
@@ -665,16 +633,14 @@ impl MentionLink {
|
||||
const SYMBOL: &str = "@symbol";
|
||||
const THREAD: &str = "@thread";
|
||||
const FETCH: &str = "@fetch";
|
||||
const RULES: &str = "@rules";
|
||||
|
||||
const SEPARATOR: &str = ":";
|
||||
|
||||
pub fn is_valid(url: &str) -> bool {
|
||||
url.starts_with(Self::FILE)
|
||||
|| url.starts_with(Self::SYMBOL)
|
||||
|| url.starts_with(Self::THREAD)
|
||||
|| url.starts_with(Self::FETCH)
|
||||
|| url.starts_with(Self::RULES)
|
||||
|| url.starts_with(Self::THREAD)
|
||||
}
|
||||
|
||||
pub fn for_file(file_name: &str, full_path: &str) -> String {
|
||||
@@ -691,16 +657,12 @@ impl MentionLink {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn for_thread(thread: &ThreadContextEntry) -> String {
|
||||
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
|
||||
}
|
||||
|
||||
pub fn for_fetch(url: &str) -> String {
|
||||
format!("[@{}]({}:{})", url, Self::FETCH, url)
|
||||
}
|
||||
|
||||
pub fn for_rules(rules: &RulesContextEntry) -> String {
|
||||
format!("[@{}]({}:{})", rules.title, Self::RULES, rules.prompt_id.0)
|
||||
pub fn for_thread(thread: &ThreadContextEntry) -> String {
|
||||
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
|
||||
}
|
||||
|
||||
pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
|
||||
@@ -744,10 +706,6 @@ impl MentionLink {
|
||||
Some(MentionLink::Thread(thread_id))
|
||||
}
|
||||
Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
|
||||
Self::RULES => {
|
||||
let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
|
||||
Some(MentionLink::Rules(prompt_id))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,11 @@ use http_client::HttpClientWithUrl;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
|
||||
use prompt_store::PromptId;
|
||||
use rope::Point;
|
||||
use text::{Anchor, ToPoint};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::RULES_ICON;
|
||||
use crate::context_picker::file_context_picker::search_files;
|
||||
use crate::context_picker::symbol_context_picker::search_symbols;
|
||||
use crate::context_store::ContextStore;
|
||||
@@ -28,7 +26,6 @@ use crate::thread_store::ThreadStore;
|
||||
|
||||
use super::fetch_context_picker::fetch_url_content;
|
||||
use super::file_context_picker::FileMatch;
|
||||
use super::rules_context_picker::{RulesContextEntry, search_rules};
|
||||
use super::symbol_context_picker::SymbolMatch;
|
||||
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
|
||||
use super::{
|
||||
@@ -41,7 +38,6 @@ pub(crate) enum Match {
|
||||
File(FileMatch),
|
||||
Thread(ThreadMatch),
|
||||
Fetch(SharedString),
|
||||
Rules(RulesContextEntry),
|
||||
Mode(ModeMatch),
|
||||
}
|
||||
|
||||
@@ -58,7 +54,6 @@ impl Match {
|
||||
Match::Thread(_) => 1.,
|
||||
Match::Symbol(_) => 1.,
|
||||
Match::Fetch(_) => 1.,
|
||||
Match::Rules(_) => 1.,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,21 +112,6 @@ fn search(
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
}
|
||||
Some(ContextPickerMode::Rules) => {
|
||||
if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
|
||||
let search_rules_task =
|
||||
search_rules(query.clone(), cancellation_flag.clone(), thread_store, cx);
|
||||
cx.background_spawn(async move {
|
||||
search_rules_task
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Match::Rules)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
} else {
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if query.is_empty() {
|
||||
let mut matches = recent_entries
|
||||
@@ -307,60 +287,6 @@ impl ContextPickerCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_for_rules(
|
||||
rules: RulesContextEntry,
|
||||
excerpt_id: ExcerptId,
|
||||
source_range: Range<Anchor>,
|
||||
editor: Entity<Editor>,
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
) -> Completion {
|
||||
let new_text = MentionLink::for_rules(&rules);
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
new_text,
|
||||
label: CodeLabel::plain(rules.title.to_string(), None),
|
||||
documentation: None,
|
||||
insert_text_mode: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(RULES_ICON.path().into()),
|
||||
confirm: Some(confirm_completion_callback(
|
||||
RULES_ICON.path().into(),
|
||||
rules.title.clone(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len,
|
||||
editor.clone(),
|
||||
move |cx| {
|
||||
let prompt_uuid = rules.prompt_id;
|
||||
let prompt_id = PromptId::User { uuid: prompt_uuid };
|
||||
let context_store = context_store.clone();
|
||||
let Some(prompt_store) = thread_store.read(cx).prompt_store() else {
|
||||
log::error!("Can't add user rules as prompt store is missing.");
|
||||
return;
|
||||
};
|
||||
let prompt_store = prompt_store.read(cx);
|
||||
let Some(metadata) = prompt_store.metadata(prompt_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(title) = metadata.title else {
|
||||
return;
|
||||
};
|
||||
let text_task = prompt_store.load(prompt_id, cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let text = text_task.await?;
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
context_store.add_rules(prompt_uuid, title, text, false, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_for_fetch(
|
||||
source_range: Range<Anchor>,
|
||||
url_to_fetch: SharedString,
|
||||
@@ -667,17 +593,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
thread_store,
|
||||
))
|
||||
}
|
||||
Match::Rules(user_rules) => {
|
||||
let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
|
||||
Some(Self::completion_for_rules(
|
||||
user_rules,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
thread_store,
|
||||
))
|
||||
}
|
||||
Match::Fetch(url) => Some(Self::completion_for_fetch(
|
||||
source_range.clone(),
|
||||
url,
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use prompt_store::{PromptId, UserPromptId};
|
||||
use ui::{ListItem, prelude::*};
|
||||
|
||||
use crate::context::RULES_ICON;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::{self, ContextStore};
|
||||
use crate::thread_store::ThreadStore;
|
||||
|
||||
pub struct RulesContextPicker {
|
||||
picker: Entity<Picker<RulesContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl RulesContextPicker {
|
||||
pub fn new(
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let delegate = RulesContextPickerDelegate::new(thread_store, context_picker, context_store);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
RulesContextPicker { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for RulesContextPicker {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RulesContextPicker {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RulesContextEntry {
|
||||
pub prompt_id: UserPromptId,
|
||||
pub title: SharedString,
|
||||
}
|
||||
|
||||
pub struct RulesContextPickerDelegate {
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
matches: Vec<RulesContextEntry>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl RulesContextPickerDelegate {
|
||||
pub fn new(
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
context_store: WeakEntity<context_store::ContextStore>,
|
||||
) -> Self {
|
||||
RulesContextPickerDelegate {
|
||||
thread_store,
|
||||
context_picker,
|
||||
context_store,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for RulesContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search available rules…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = search_rules(query, Arc::new(AtomicBool::default()), thread_store, cx);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = search_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = 0;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(entry) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let prompt_id = entry.prompt_id;
|
||||
|
||||
let load_rules_task = thread_store.update(cx, |thread_store, cx| {
|
||||
thread_store.load_rules(prompt_id, cx)
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (metadata, text) = load_rules_task.await?;
|
||||
let Some(title) = metadata.title else {
|
||||
return Err(anyhow!("Encountered user rule with no title when attempting to add it to agent context."));
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.add_rules(prompt_id, title, text, true, cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let thread = &self.matches[ix];
|
||||
|
||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
render_thread_context_entry(thread, self.context_store.clone(), cx),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_thread_context_entry(
|
||||
user_rules: &RulesContextEntry,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
let added = context_store.upgrade().map_or(false, |ctx_store| {
|
||||
ctx_store
|
||||
.read(cx)
|
||||
.includes_user_rules(&user_rules.prompt_id)
|
||||
.is_some()
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.max_w_72()
|
||||
.child(
|
||||
Icon::new(RULES_ICON)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(user_rules.title.clone()).truncate()),
|
||||
)
|
||||
.when(added, |el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn search_rules(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<RulesContextEntry>> {
|
||||
let Some(prompt_store) = thread_store.read(cx).prompt_store() else {
|
||||
return Task::ready(vec![]);
|
||||
};
|
||||
let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
|
||||
cx.background_spawn(async move {
|
||||
search_task
|
||||
.await
|
||||
.into_iter()
|
||||
.flat_map(|metadata| {
|
||||
// Default prompts are filtered out as they are automatically included.
|
||||
if metadata.default {
|
||||
None
|
||||
} else {
|
||||
match metadata.id {
|
||||
PromptId::EditWorkflow => None,
|
||||
PromptId::User { uuid } => Some(RulesContextEntry {
|
||||
prompt_id: uuid,
|
||||
title: metadata.title?,
|
||||
}),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
@@ -103,11 +103,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||
let Some(threads) = self.thread_store.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = search_threads(query, Arc::new(AtomicBool::default()), thread_store, cx);
|
||||
let search_task = search_threads(query, Arc::new(AtomicBool::default()), threads, cx);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = search_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -217,15 +217,15 @@ pub(crate) fn search_threads(
|
||||
thread_store: Entity<ThreadStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<ThreadMatch>> {
|
||||
let threads = thread_store
|
||||
.read(cx)
|
||||
.threads()
|
||||
.into_iter()
|
||||
.map(|thread| ThreadContextEntry {
|
||||
id: thread.id,
|
||||
summary: thread.summary,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let threads = thread_store.update(cx, |this, _cx| {
|
||||
this.threads()
|
||||
.into_iter()
|
||||
.map(|thread| ThreadContextEntry {
|
||||
id: thread.id,
|
||||
summary: thread.summary,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_spawn(async move {
|
||||
|
||||
@@ -9,7 +9,6 @@ use futures::{self, Future, FutureExt, future};
|
||||
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
|
||||
use language::{Buffer, File};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::UserPromptId;
|
||||
use rope::{Point, Rope};
|
||||
use text::{Anchor, BufferId, OffsetRangeExt};
|
||||
use util::{ResultExt as _, maybe};
|
||||
@@ -17,7 +16,7 @@ use util::{ResultExt as _, maybe};
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{
|
||||
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
|
||||
ExcerptContext, FetchedUrlContext, FileContext, RulesContext, SymbolContext, ThreadContext,
|
||||
ExcerptContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||
};
|
||||
use crate::context_strip::SuggestedContext;
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
@@ -26,6 +25,7 @@ pub struct ContextStore {
|
||||
project: WeakEntity<Project>,
|
||||
context: Vec<AssistantContext>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
|
||||
next_context_id: ContextId,
|
||||
files: BTreeMap<BufferId, ContextId>,
|
||||
directories: HashMap<ProjectPath, ContextId>,
|
||||
@@ -35,7 +35,6 @@ pub struct ContextStore {
|
||||
threads: HashMap<ThreadId, ContextId>,
|
||||
thread_summary_tasks: Vec<Task<()>>,
|
||||
fetched_urls: HashMap<String, ContextId>,
|
||||
user_rules: HashMap<UserPromptId, ContextId>,
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
@@ -56,7 +55,6 @@ impl ContextStore {
|
||||
threads: HashMap::default(),
|
||||
thread_summary_tasks: Vec::new(),
|
||||
fetched_urls: HashMap::default(),
|
||||
user_rules: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +72,6 @@ impl ContextStore {
|
||||
self.directories.clear();
|
||||
self.threads.clear();
|
||||
self.fetched_urls.clear();
|
||||
self.user_rules.clear();
|
||||
}
|
||||
|
||||
pub fn add_file_from_path(
|
||||
@@ -393,42 +390,6 @@ impl ContextStore {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_rules(
|
||||
&mut self,
|
||||
prompt_id: UserPromptId,
|
||||
title: impl Into<SharedString>,
|
||||
text: impl Into<SharedString>,
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) {
|
||||
if let Some(context_id) = self.includes_user_rules(&prompt_id) {
|
||||
if remove_if_exists {
|
||||
self.remove_context(context_id, cx);
|
||||
}
|
||||
} else {
|
||||
self.insert_user_rules(prompt_id, title, text, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_user_rules(
|
||||
&mut self,
|
||||
prompt_id: UserPromptId,
|
||||
title: impl Into<SharedString>,
|
||||
text: impl Into<SharedString>,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
|
||||
self.user_rules.insert(prompt_id, id);
|
||||
self.context.push(AssistantContext::Rules(RulesContext {
|
||||
id,
|
||||
prompt_id,
|
||||
title: title.into(),
|
||||
text: text.into(),
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_fetched_url(
|
||||
&mut self,
|
||||
url: String,
|
||||
@@ -557,9 +518,6 @@ impl ContextStore {
|
||||
AssistantContext::Thread(_) => {
|
||||
self.threads.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
AssistantContext::Rules(RulesContext { prompt_id, .. }) => {
|
||||
self.user_rules.remove(&prompt_id);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -656,10 +614,6 @@ impl ContextStore {
|
||||
self.threads.get(thread_id).copied()
|
||||
}
|
||||
|
||||
pub fn includes_user_rules(&self, prompt_id: &UserPromptId) -> Option<ContextId> {
|
||||
self.user_rules.get(prompt_id).copied()
|
||||
}
|
||||
|
||||
pub fn includes_url(&self, url: &str) -> Option<ContextId> {
|
||||
self.fetched_urls.get(url).copied()
|
||||
}
|
||||
@@ -687,8 +641,7 @@ impl ContextStore {
|
||||
| AssistantContext::Symbol(_)
|
||||
| AssistantContext::Excerpt(_)
|
||||
| AssistantContext::FetchedUrl(_)
|
||||
| AssistantContext::Thread(_)
|
||||
| AssistantContext::Rules(_) => None,
|
||||
| AssistantContext::Thread(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -923,10 +876,6 @@ pub fn refresh_context_store_text(
|
||||
// and doing the caching properly could be tricky (unless it's already handled by
|
||||
// the HttpClient?).
|
||||
AssistantContext::FetchedUrl(_) => {}
|
||||
AssistantContext::Rules(user_rules_context) => {
|
||||
let context_store = context_store.clone();
|
||||
return Some(refresh_user_rules(context_store, user_rules_context, cx));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
@@ -1077,45 +1026,6 @@ fn refresh_thread_text(
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh_user_rules(
|
||||
context_store: Entity<ContextStore>,
|
||||
user_rules_context: &RulesContext,
|
||||
cx: &App,
|
||||
) -> Task<()> {
|
||||
let id = user_rules_context.id;
|
||||
let prompt_id = user_rules_context.prompt_id;
|
||||
let Some(thread_store) = context_store.read(cx).thread_store.as_ref() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
let Ok(load_task) = thread_store.read_with(cx, |thread_store, cx| {
|
||||
thread_store.load_rules(prompt_id, cx)
|
||||
}) else {
|
||||
return Task::ready(());
|
||||
};
|
||||
cx.spawn(async move |cx| {
|
||||
if let Ok((metadata, text)) = load_task.await {
|
||||
if let Some(title) = metadata.title.clone() {
|
||||
context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
context_store.replace_context(AssistantContext::Rules(RulesContext {
|
||||
id,
|
||||
prompt_id,
|
||||
title,
|
||||
text: text.into(),
|
||||
}));
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
}
|
||||
context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.remove_context(id, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh_context_buffer(
|
||||
context_buffer: &ContextBuffer,
|
||||
cx: &App,
|
||||
|
||||
@@ -24,7 +24,6 @@ use gpui::{
|
||||
WeakEntity, Window, point,
|
||||
};
|
||||
use language::{Buffer, Point, Selection, TransactionId};
|
||||
use language_model::ConfiguredModel;
|
||||
use language_model::{LanguageModelRegistry, report_assistant_event};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
@@ -1222,15 +1221,9 @@ impl InlineAssistant {
|
||||
self.prompt_history.pop_front();
|
||||
}
|
||||
|
||||
let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
assist
|
||||
.codegen
|
||||
.update(cx, |codegen, cx| codegen.start(model, user_prompt, cx))
|
||||
.update(cx, |codegen, cx| codegen.start(user_prompt, cx))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
||||
use crate::buffer_codegen::BufferCodegen;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
@@ -20,7 +20,7 @@ use gpui::{
|
||||
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::{ModelType, ToggleModelSelector};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use parking_lot::Mutex;
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::assistant_model_selector::ModelType;
|
||||
use crate::context::{AssistantContext, format_context_as_string};
|
||||
use crate::context::format_context_as_string;
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashSet;
|
||||
@@ -34,7 +34,7 @@ use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_store::{ContextStore, refresh_context_store_text};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::profile_selector::ProfileSelector;
|
||||
use crate::thread::{Thread, TokenUsageRatio};
|
||||
use crate::thread::{RequestKind, Thread, TokenUsageRatio};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{
|
||||
AgentDiff, Chat, ChatMode, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
|
||||
@@ -234,7 +234,7 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
self.set_editor_is_expanded(false, cx);
|
||||
self.send_to_model(window, cx);
|
||||
self.send_to_model(RequestKind::Chat, window, cx);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
@@ -249,7 +249,12 @@ impl MessageEditor {
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn send_to_model(
|
||||
&mut self,
|
||||
request_kind: RequestKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {
|
||||
return;
|
||||
@@ -288,21 +293,6 @@ impl MessageEditor {
|
||||
})
|
||||
.log_err();
|
||||
|
||||
context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
let excerpt_ids = context_store
|
||||
.context()
|
||||
.iter()
|
||||
.filter(|ctx| matches!(ctx, AssistantContext::Excerpt(_)))
|
||||
.map(|ctx| ctx.id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for id in excerpt_ids {
|
||||
context_store.remove_context(id, cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(wait_for_summaries) = context_store
|
||||
.update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
|
||||
.log_err()
|
||||
@@ -325,8 +315,7 @@ impl MessageEditor {
|
||||
// Send to model after summaries are done
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model, cx);
|
||||
thread.send_to_model(model, request_kind, cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
@@ -340,7 +329,7 @@ impl MessageEditor {
|
||||
|
||||
if cancelled {
|
||||
self.set_editor_is_expanded(false, cx);
|
||||
self.send_to_model(window, cx);
|
||||
self.send_to_model(RequestKind::Chat, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1009,8 +998,6 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: vec![content.into()],
|
||||
|
||||
@@ -261,8 +261,6 @@ impl TerminalInlineAssistant {
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -38,7 +38,14 @@ use crate::thread_store::{
|
||||
SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
|
||||
SerializedToolUse, SharedProjectContext,
|
||||
};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState, USING_TOOL_MARKER};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState, USING_TOOL_MARKER};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
Chat,
|
||||
/// Used when summarizing a thread.
|
||||
Summarize,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema,
|
||||
@@ -63,24 +70,6 @@ impl From<&str> for ThreadId {
|
||||
}
|
||||
}
|
||||
|
||||
/// The ID of the user prompt that initiated a request.
|
||||
///
|
||||
/// This equates to the user physically submitting a message to the model (e.g., by pressing the Enter key).
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptId(Arc<str>);
|
||||
|
||||
impl PromptId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4().to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PromptId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct MessageId(pub(crate) usize);
|
||||
|
||||
@@ -106,21 +95,12 @@ impl Message {
|
||||
self.segments.iter().all(|segment| segment.should_display())
|
||||
}
|
||||
|
||||
pub fn push_thinking(&mut self, text: &str, signature: Option<String>) {
|
||||
if let Some(MessageSegment::Thinking {
|
||||
text: segment,
|
||||
signature: current_signature,
|
||||
}) = self.segments.last_mut()
|
||||
{
|
||||
if let Some(signature) = signature {
|
||||
*current_signature = Some(signature);
|
||||
}
|
||||
pub fn push_thinking(&mut self, text: &str) {
|
||||
if let Some(MessageSegment::Thinking(segment)) = self.segments.last_mut() {
|
||||
segment.push_str(text);
|
||||
} else {
|
||||
self.segments.push(MessageSegment::Thinking {
|
||||
text: text.to_string(),
|
||||
signature,
|
||||
});
|
||||
self.segments
|
||||
.push(MessageSegment::Thinking(text.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,12 +122,11 @@ impl Message {
|
||||
for segment in &self.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => result.push_str(text),
|
||||
MessageSegment::Thinking { text, .. } => {
|
||||
result.push_str("<think>\n");
|
||||
MessageSegment::Thinking(text) => {
|
||||
result.push_str("<think>");
|
||||
result.push_str(text);
|
||||
result.push_str("\n</think>");
|
||||
result.push_str("</think>");
|
||||
}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,22 +137,24 @@ impl Message {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MessageSegment {
|
||||
Text(String),
|
||||
Thinking {
|
||||
text: String,
|
||||
signature: Option<String>,
|
||||
},
|
||||
RedactedThinking(Vec<u8>),
|
||||
Thinking(String),
|
||||
}
|
||||
|
||||
impl MessageSegment {
|
||||
pub fn text_mut(&mut self) -> &mut String {
|
||||
match self {
|
||||
Self::Text(text) => text,
|
||||
Self::Thinking(text) => text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_display(&self) -> bool {
|
||||
// We add USING_TOOL_MARKER when making a request that includes tool uses
|
||||
// without non-whitespace text around them, and this can cause the model
|
||||
// to mimic the pattern, so we consider those segments not displayable.
|
||||
match self {
|
||||
Self::Text(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
|
||||
Self::Thinking { text, .. } => text.is_empty() || text.trim() == USING_TOOL_MARKER,
|
||||
Self::RedactedThinking(_) => false,
|
||||
Self::Thinking(text) => text.is_empty() || text.trim() == USING_TOOL_MARKER,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,7 +274,6 @@ pub struct Thread {
|
||||
detailed_summary_state: DetailedSummaryState,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
last_prompt_id: PromptId,
|
||||
context: BTreeMap<ContextId, AssistantContext>,
|
||||
context_by_message: HashMap<MessageId, Vec<ContextId>>,
|
||||
project_context: SharedProjectContext,
|
||||
@@ -343,7 +323,6 @@ impl Thread {
|
||||
detailed_summary_state: DetailedSummaryState::NotGenerated,
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
last_prompt_id: PromptId::new(),
|
||||
context: BTreeMap::default(),
|
||||
context_by_message: HashMap::default(),
|
||||
project_context: system_prompt,
|
||||
@@ -409,11 +388,8 @@ impl Thread {
|
||||
.into_iter()
|
||||
.map(|segment| match segment {
|
||||
SerializedMessageSegment::Text { text } => MessageSegment::Text(text),
|
||||
SerializedMessageSegment::Thinking { text, signature } => {
|
||||
MessageSegment::Thinking { text, signature }
|
||||
}
|
||||
SerializedMessageSegment::RedactedThinking { data } => {
|
||||
MessageSegment::RedactedThinking(data)
|
||||
SerializedMessageSegment::Thinking { text } => {
|
||||
MessageSegment::Thinking(text)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
@@ -421,7 +397,6 @@ impl Thread {
|
||||
})
|
||||
.collect(),
|
||||
next_message_id,
|
||||
last_prompt_id: PromptId::new(),
|
||||
context: BTreeMap::default(),
|
||||
context_by_message: HashMap::default(),
|
||||
project_context,
|
||||
@@ -470,10 +445,6 @@ impl Thread {
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn advance_prompt_id(&mut self) {
|
||||
self.last_prompt_id = PromptId::new();
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<SharedString> {
|
||||
self.summary.clone()
|
||||
}
|
||||
@@ -774,9 +745,7 @@ impl Thread {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::FetchedUrl(_)
|
||||
| AssistantContext::Thread(_)
|
||||
| AssistantContext::Rules(_) => {}
|
||||
AssistantContext::FetchedUrl(_) | AssistantContext::Thread(_) => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -868,10 +837,9 @@ impl Thread {
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(content) => text.push_str(content),
|
||||
MessageSegment::Thinking { text: content, .. } => {
|
||||
MessageSegment::Thinking(content) => {
|
||||
text.push_str(&format!("<think>{}</think>", content))
|
||||
}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
}
|
||||
}
|
||||
text.push('\n');
|
||||
@@ -901,16 +869,8 @@ impl Thread {
|
||||
MessageSegment::Text(text) => {
|
||||
SerializedMessageSegment::Text { text: text.clone() }
|
||||
}
|
||||
MessageSegment::Thinking { text, signature } => {
|
||||
SerializedMessageSegment::Thinking {
|
||||
text: text.clone(),
|
||||
signature: signature.clone(),
|
||||
}
|
||||
}
|
||||
MessageSegment::RedactedThinking(data) => {
|
||||
SerializedMessageSegment::RedactedThinking {
|
||||
data: data.clone(),
|
||||
}
|
||||
MessageSegment::Thinking(text) => {
|
||||
SerializedMessageSegment::Thinking { text: text.clone() }
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
@@ -944,8 +904,13 @@ impl Thread {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_to_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
||||
let mut request = self.to_completion_request(cx);
|
||||
pub fn send_to_model(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
request_kind: RequestKind,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut request = self.to_completion_request(request_kind, cx);
|
||||
if model.supports_tools() {
|
||||
request.tools = {
|
||||
let mut tools = Vec::new();
|
||||
@@ -984,10 +949,12 @@ impl Thread {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn to_completion_request(&self, cx: &mut Context<Self>) -> LanguageModelRequest {
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
request_kind: RequestKind,
|
||||
cx: &App,
|
||||
) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: Some(self.id.to_string()),
|
||||
prompt_id: Some(self.last_prompt_id.to_string()),
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
@@ -995,33 +962,20 @@ impl Thread {
|
||||
};
|
||||
|
||||
if let Some(project_context) = self.project_context.borrow().as_ref() {
|
||||
match self
|
||||
if let Some(system_prompt) = self
|
||||
.prompt_builder
|
||||
.generate_assistant_system_prompt(project_context)
|
||||
.context("failed to generate assistant system prompt")
|
||||
.log_err()
|
||||
{
|
||||
Err(err) => {
|
||||
let message = format!("{err:?}").into();
|
||||
log::error!("{message}");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Error generating system prompt".into(),
|
||||
message,
|
||||
}));
|
||||
}
|
||||
Ok(system_prompt) => {
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
});
|
||||
}
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let message = "Context for system prompt unexpectedly not ready.".into();
|
||||
log::error!("{message}");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Error generating system prompt".into(),
|
||||
message,
|
||||
}));
|
||||
log::error!("project_context not set.")
|
||||
}
|
||||
|
||||
for message in &self.messages {
|
||||
@@ -1031,42 +985,34 @@ impl Thread {
|
||||
cache: false,
|
||||
};
|
||||
|
||||
self.tool_use
|
||||
.attach_tool_results(message.id, &mut request_message);
|
||||
match request_kind {
|
||||
RequestKind::Chat => {
|
||||
self.tool_use
|
||||
.attach_tool_results(message.id, &mut request_message);
|
||||
}
|
||||
RequestKind::Summarize => {
|
||||
// We don't care about tool use during summarization.
|
||||
if self.tool_use.message_has_tool_results(message.id) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !message.context.is_empty() {
|
||||
if !message.segments.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(message.context.to_string()));
|
||||
.push(MessageContent::Text(message.to_string()));
|
||||
}
|
||||
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => {
|
||||
if !text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(text.into()));
|
||||
}
|
||||
}
|
||||
MessageSegment::Thinking { text, signature } => {
|
||||
if !text.is_empty() {
|
||||
request_message.content.push(MessageContent::Thinking {
|
||||
text: text.into(),
|
||||
signature: signature.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
MessageSegment::RedactedThinking(data) => {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::RedactedThinking(data.clone()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
self.tool_use
|
||||
.attach_tool_uses(message.id, &mut request_message);
|
||||
match request_kind {
|
||||
RequestKind::Chat => {
|
||||
self.tool_use
|
||||
.attach_tool_uses(message.id, &mut request_message);
|
||||
}
|
||||
RequestKind::Summarize => {
|
||||
// We don't care about tool use during summarization.
|
||||
}
|
||||
};
|
||||
|
||||
request.messages.push(request_message);
|
||||
}
|
||||
@@ -1081,54 +1027,6 @@ impl Thread {
|
||||
request
|
||||
}
|
||||
|
||||
fn to_summarize_request(&self, added_user_message: String) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
for message in &self.messages {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: message.role,
|
||||
content: Vec::new(),
|
||||
cache: false,
|
||||
};
|
||||
|
||||
// Skip tool results during summarization.
|
||||
if self.tool_use.message_has_tool_results(message.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => request_message
|
||||
.content
|
||||
.push(MessageContent::Text(text.clone())),
|
||||
MessageSegment::Thinking { .. } => {}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if request_message.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
request.messages.push(request_message);
|
||||
}
|
||||
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text(added_user_message)],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
fn attached_tracked_files_state(
|
||||
&self,
|
||||
messages: &mut Vec<LanguageModelRequestMessage>,
|
||||
@@ -1181,12 +1079,6 @@ impl Thread {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let prompt_id = self.last_prompt_id.clone();
|
||||
let tool_use_metadata = ToolUseMetadata {
|
||||
model: model.clone(),
|
||||
thread_id: self.id.clone(),
|
||||
prompt_id: prompt_id.clone(),
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |thread, cx| {
|
||||
let stream_completion_future = model.stream_completion_with_usage(request, &cx);
|
||||
@@ -1255,13 +1147,10 @@ impl Thread {
|
||||
};
|
||||
}
|
||||
}
|
||||
LanguageModelCompletionEvent::Thinking {
|
||||
text: chunk,
|
||||
signature,
|
||||
} => {
|
||||
LanguageModelCompletionEvent::Thinking(chunk) => {
|
||||
if let Some(last_message) = thread.messages.last_mut() {
|
||||
if last_message.role == Role::Assistant {
|
||||
last_message.push_thinking(&chunk, signature);
|
||||
last_message.push_thinking(&chunk);
|
||||
cx.emit(ThreadEvent::StreamedAssistantThinking(
|
||||
last_message.id,
|
||||
chunk,
|
||||
@@ -1274,10 +1163,7 @@ impl Thread {
|
||||
// will result in duplicating the text of the chunk in the rendered Markdown.
|
||||
thread.insert_message(
|
||||
Role::Assistant,
|
||||
vec![MessageSegment::Thinking {
|
||||
text: chunk.to_string(),
|
||||
signature,
|
||||
}],
|
||||
vec![MessageSegment::Thinking(chunk.to_string())],
|
||||
cx,
|
||||
);
|
||||
};
|
||||
@@ -1293,27 +1179,11 @@ impl Thread {
|
||||
thread.insert_message(Role::Assistant, vec![], cx)
|
||||
});
|
||||
|
||||
let tool_use_id = tool_use.id.clone();
|
||||
let streamed_input = if tool_use.is_input_complete {
|
||||
None
|
||||
} else {
|
||||
Some((&tool_use.input).clone())
|
||||
};
|
||||
|
||||
let ui_text = thread.tool_use.request_tool_use(
|
||||
thread.tool_use.request_tool_use(
|
||||
last_assistant_message_id,
|
||||
tool_use,
|
||||
tool_use_metadata.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(input) = streamed_input {
|
||||
cx.emit(ThreadEvent::StreamedToolUse {
|
||||
tool_use_id,
|
||||
ui_text,
|
||||
input,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1332,12 +1202,7 @@ impl Thread {
|
||||
.pending_completions
|
||||
.retain(|completion| completion.id != pending_completion_id);
|
||||
|
||||
// If there is a response without tool use, summarize the message. Otherwise,
|
||||
// allow two tool uses before summarizing.
|
||||
if thread.summary.is_none()
|
||||
&& thread.messages.len() >= 2
|
||||
&& (!thread.has_pending_tool_uses() || thread.messages.len() >= 6)
|
||||
{
|
||||
if thread.summary.is_none() && thread.messages.len() >= 2 {
|
||||
thread.summarize(cx);
|
||||
}
|
||||
})?;
|
||||
@@ -1419,7 +1284,6 @@ impl Thread {
|
||||
telemetry::event!(
|
||||
"Assistant Thread Completion",
|
||||
thread_id = thread.id().to_string(),
|
||||
prompt_id = prompt_id,
|
||||
model = model.telemetry_id(),
|
||||
model_provider = model.provider_id().to_string(),
|
||||
input_tokens = usage.input_tokens,
|
||||
@@ -1447,12 +1311,18 @@ impl Thread {
|
||||
return;
|
||||
}
|
||||
|
||||
let added_user_message = "Generate a concise 3-7 word title for this conversation, omitting punctuation. \
|
||||
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
|
||||
If the conversation is about a specific subject, include it in the title. \
|
||||
Be descriptive. DO NOT speak in the first person.";
|
||||
|
||||
let request = self.to_summarize_request(added_user_message.into());
|
||||
let mut request = self.to_completion_request(RequestKind::Summarize, cx);
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
"Generate a concise 3-7 word title for this conversation, omitting punctuation. \
|
||||
Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. \
|
||||
If the conversation is about a specific subject, include it in the title. \
|
||||
Be descriptive. DO NOT speak in the first person."
|
||||
.into(),
|
||||
],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
self.pending_summary = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
@@ -1514,14 +1384,21 @@ impl Thread {
|
||||
return None;
|
||||
}
|
||||
|
||||
let added_user_message = "Generate a detailed summary of this conversation. Include:\n\
|
||||
1. A brief overview of what was discussed\n\
|
||||
2. Key facts or information discovered\n\
|
||||
3. Outcomes or conclusions reached\n\
|
||||
4. Any action items or next steps if any\n\
|
||||
Format it in Markdown with headings and bullet points.";
|
||||
let mut request = self.to_completion_request(RequestKind::Summarize, cx);
|
||||
|
||||
let request = self.to_summarize_request(added_user_message.into());
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
"Generate a detailed summary of this conversation. Include:\n\
|
||||
1. A brief overview of what was discussed\n\
|
||||
2. Key facts or information discovered\n\
|
||||
3. Outcomes or conclusions reached\n\
|
||||
4. Any action items or next steps if any\n\
|
||||
Format it in Markdown with headings and bullet points."
|
||||
.into(),
|
||||
],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
let task = cx.spawn(async move |thread, cx| {
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
@@ -1569,7 +1446,7 @@ impl Thread {
|
||||
|
||||
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) -> Vec<PendingToolUse> {
|
||||
self.auto_capture_telemetry(cx);
|
||||
let request = self.to_completion_request(cx);
|
||||
let request = self.to_completion_request(RequestKind::Chat, cx);
|
||||
let messages = Arc::new(request.messages);
|
||||
let pending_tool_uses = self
|
||||
.tool_use
|
||||
@@ -1681,7 +1558,7 @@ impl Thread {
|
||||
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
|
||||
self.attach_tool_results(cx);
|
||||
if !canceled {
|
||||
self.send_to_model(model, cx);
|
||||
self.send_to_model(model, RequestKind::Chat, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1694,8 +1571,6 @@ impl Thread {
|
||||
|
||||
/// Insert an empty message to be populated with tool results upon send.
|
||||
pub fn attach_tool_results(&mut self, cx: &mut Context<Self>) {
|
||||
// Tool results are assumed to be waiting on the next message id, so they will populate
|
||||
// this empty message before sending to model. Would prefer this to be more straightforward.
|
||||
self.insert_message(Role::User, vec![], cx);
|
||||
self.auto_capture_telemetry(cx);
|
||||
}
|
||||
@@ -1976,10 +1851,9 @@ impl Thread {
|
||||
for segment in &message.segments {
|
||||
match segment {
|
||||
MessageSegment::Text(text) => writeln!(markdown, "{}\n", text)?,
|
||||
MessageSegment::Thinking { text, .. } => {
|
||||
writeln!(markdown, "<think>\n{}\n</think>\n", text)?
|
||||
MessageSegment::Thinking(text) => {
|
||||
writeln!(markdown, "<think>{}</think>\n", text)?
|
||||
}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2204,11 +2078,6 @@ pub enum ThreadEvent {
|
||||
StreamedCompletion,
|
||||
StreamedAssistantText(MessageId, String),
|
||||
StreamedAssistantThinking(MessageId, String),
|
||||
StreamedToolUse {
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
ui_text: Arc<str>,
|
||||
input: serde_json::Value,
|
||||
},
|
||||
Stopped(Result<StopReason, Arc<anyhow::Error>>),
|
||||
MessageAdded(MessageId),
|
||||
MessageEdited(MessageId),
|
||||
@@ -2311,7 +2180,9 @@ fn main() {{
|
||||
assert_eq!(message.context, expected_context);
|
||||
|
||||
// Check message in request
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
let expected_full_message = format!("{}Please explain this code", expected_context);
|
||||
@@ -2401,7 +2272,9 @@ fn main() {{
|
||||
assert!(message3.context.contains("file3.rs"));
|
||||
|
||||
// Check entire request to make sure all contexts are properly included
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
// The request should contain all 3 messages
|
||||
assert_eq!(request.messages.len(), 4);
|
||||
@@ -2451,7 +2324,9 @@ fn main() {{
|
||||
assert_eq!(message.context, "");
|
||||
|
||||
// Check message in request
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 2);
|
||||
assert_eq!(
|
||||
@@ -2469,7 +2344,9 @@ fn main() {{
|
||||
assert_eq!(message2.context, "");
|
||||
|
||||
// Check that both messages appear in the request
|
||||
let request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
assert_eq!(request.messages.len(), 3);
|
||||
assert_eq!(
|
||||
@@ -2509,7 +2386,9 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Create a request and check that it doesn't have a stale buffer warning yet
|
||||
let initial_request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
let initial_request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
// Make sure we don't have a stale file warning yet
|
||||
let has_stale_warning = initial_request.messages.iter().any(|msg| {
|
||||
@@ -2537,7 +2416,9 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Create a new request and check for the stale buffer warning
|
||||
let new_request = thread.update(cx, |thread, cx| thread.to_completion_request(cx));
|
||||
let new_request = thread.read_with(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
// We should have a stale file warning as the last message
|
||||
let last_message = new_request
|
||||
@@ -2564,7 +2445,6 @@ fn main() {{
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AssistantSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
@@ -2604,8 +2484,7 @@ fn main() {{
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
@@ -12,9 +12,8 @@ use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use fs::Fs;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{
|
||||
App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
|
||||
Subscription, Task, prelude::*,
|
||||
@@ -23,10 +22,7 @@ use heed::Database;
|
||||
use heed::types::SerdeBincode;
|
||||
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
|
||||
use project::{Project, Worktree};
|
||||
use prompt_store::{
|
||||
ProjectContext, PromptBuilder, PromptId, PromptMetadata, PromptStore, PromptsUpdatedEvent,
|
||||
RulesFileContext, UserPromptId, UserRulesContext, WorktreeContext,
|
||||
};
|
||||
use prompt_store::{ProjectContext, PromptBuilder, RulesFileContext, WorktreeContext};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use util::ResultExt as _;
|
||||
@@ -62,13 +58,10 @@ pub struct ThreadStore {
|
||||
project: Entity<Project>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
|
||||
threads: Vec<SerializedThreadMetadata>,
|
||||
project_context: SharedProjectContext,
|
||||
reload_system_prompt_tx: mpsc::Sender<()>,
|
||||
_reload_system_prompt_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -84,22 +77,12 @@ impl ThreadStore {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
let prompt_store = PromptStore::global(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let prompt_store = prompt_store.await.ok();
|
||||
let (thread_store, ready_rx) = cx.update(|cx| {
|
||||
let mut option_ready_rx = None;
|
||||
let thread_store = cx.new(|cx| {
|
||||
let (thread_store, ready_rx) =
|
||||
Self::new(project, tools, prompt_builder, prompt_store, cx);
|
||||
option_ready_rx = Some(ready_rx);
|
||||
thread_store
|
||||
});
|
||||
(thread_store, option_ready_rx.take().unwrap())
|
||||
})?;
|
||||
ready_rx.await?;
|
||||
Ok(thread_store)
|
||||
) -> Task<Entity<Self>> {
|
||||
let thread_store = cx.new(|cx| Self::new(project, tools, prompt_builder, cx));
|
||||
let reload = thread_store.update(cx, |store, cx| store.reload_system_prompt(cx));
|
||||
cx.foreground_executor().spawn(async move {
|
||||
reload.await;
|
||||
thread_store
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,83 +90,43 @@ impl ThreadStore {
|
||||
project: Entity<Project>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> (Self, oneshot::Receiver<()>) {
|
||||
) -> Self {
|
||||
let context_server_factory_registry = ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
});
|
||||
|
||||
let mut subscriptions = vec![
|
||||
let settings_subscription =
|
||||
cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
|
||||
this.load_default_profile(cx);
|
||||
}),
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
];
|
||||
|
||||
if let Some(prompt_store) = prompt_store.as_ref() {
|
||||
subscriptions.push(cx.subscribe(
|
||||
prompt_store,
|
||||
|this, _prompt_store, PromptsUpdatedEvent, _cx| {
|
||||
this.enqueue_system_prompt_reload();
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// This channel and task prevent concurrent and redundant loading of the system prompt.
|
||||
let (reload_system_prompt_tx, mut reload_system_prompt_rx) = mpsc::channel(1);
|
||||
let (ready_tx, ready_rx) = oneshot::channel();
|
||||
let mut ready_tx = Some(ready_tx);
|
||||
let reload_system_prompt_task = cx.spawn({
|
||||
let prompt_store = prompt_store.clone();
|
||||
async move |thread_store, cx| {
|
||||
loop {
|
||||
let Some(reload_task) = thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
thread_store.reload_system_prompt(prompt_store.clone(), cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
reload_task.await;
|
||||
if let Some(ready_tx) = ready_tx.take() {
|
||||
ready_tx.send(()).ok();
|
||||
}
|
||||
reload_system_prompt_rx.next().await;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
let project_subscription = cx.subscribe(&project, Self::handle_project_event);
|
||||
|
||||
let this = Self {
|
||||
project,
|
||||
tools,
|
||||
prompt_builder,
|
||||
prompt_store,
|
||||
context_server_manager,
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
threads: Vec::new(),
|
||||
project_context: SharedProjectContext::default(),
|
||||
reload_system_prompt_tx,
|
||||
_reload_system_prompt_task: reload_system_prompt_task,
|
||||
_subscriptions: subscriptions,
|
||||
_subscriptions: vec![settings_subscription, project_subscription],
|
||||
};
|
||||
this.load_default_profile(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
(this, ready_rx)
|
||||
this
|
||||
}
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
_project: Entity<Project>,
|
||||
event: &project::Event,
|
||||
_cx: &mut Context<Self>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
|
||||
self.enqueue_system_prompt_reload();
|
||||
self.reload_system_prompt(cx).detach();
|
||||
}
|
||||
project::Event::WorktreeUpdatedEntries(_, items) => {
|
||||
if items.iter().any(|(path, _, _)| {
|
||||
@@ -191,25 +134,16 @@ impl ThreadStore {
|
||||
.iter()
|
||||
.any(|name| path.as_ref() == Path::new(name))
|
||||
}) {
|
||||
self.enqueue_system_prompt_reload();
|
||||
self.reload_system_prompt(cx).detach();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn enqueue_system_prompt_reload(&mut self) {
|
||||
self.reload_system_prompt_tx.try_send(()).ok();
|
||||
}
|
||||
|
||||
// Note that this should only be called from `reload_system_prompt_task`.
|
||||
fn reload_system_prompt(
|
||||
&self,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<()> {
|
||||
pub fn reload_system_prompt(&self, cx: &mut Context<Self>) -> Task<()> {
|
||||
let project = self.project.read(cx);
|
||||
let worktree_tasks = project
|
||||
let tasks = project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| {
|
||||
Self::load_worktree_info_for_system_prompt(
|
||||
@@ -219,23 +153,10 @@ impl ThreadStore {
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let default_user_rules_task = match prompt_store {
|
||||
None => Task::ready(vec![]),
|
||||
Some(prompt_store) => prompt_store.read_with(cx, |prompt_store, cx| {
|
||||
let prompts = prompt_store.default_prompt_metadata();
|
||||
let load_tasks = prompts.into_iter().map(|prompt_metadata| {
|
||||
let contents = prompt_store.load(prompt_metadata.id, cx);
|
||||
async move { (contents.await, prompt_metadata) }
|
||||
});
|
||||
cx.background_spawn(future::join_all(load_tasks))
|
||||
}),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (worktrees, default_user_rules) =
|
||||
future::join(future::join_all(worktree_tasks), default_user_rules_task).await;
|
||||
|
||||
let worktrees = worktrees
|
||||
let results = futures::future::join_all(tasks).await;
|
||||
let worktrees = results
|
||||
.into_iter()
|
||||
.map(|(worktree, rules_error)| {
|
||||
if let Some(rules_error) = rules_error {
|
||||
@@ -244,33 +165,8 @@ impl ThreadStore {
|
||||
worktree
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let default_user_rules = default_user_rules
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: match prompt_metadata.id {
|
||||
PromptId::User { uuid } => uuid,
|
||||
PromptId::EditWorkflow => return None,
|
||||
},
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
Err(err) => {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(RulesLoadingError {
|
||||
message: format!("{err:?}").into(),
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
*this.project_context.0.borrow_mut() =
|
||||
Some(ProjectContext::new(worktrees, default_user_rules));
|
||||
*this.project_context.0.borrow_mut() = Some(ProjectContext::new(worktrees));
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -282,12 +178,14 @@ impl ThreadStore {
|
||||
cx: &App,
|
||||
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
|
||||
let root_name = worktree.root_name().into();
|
||||
let abs_path = worktree.abs_path();
|
||||
|
||||
let rules_task = Self::load_worktree_rules_file(fs, worktree, cx);
|
||||
let Some(rules_task) = rules_task else {
|
||||
return Task::ready((
|
||||
WorktreeContext {
|
||||
root_name,
|
||||
abs_path,
|
||||
rules_file: None,
|
||||
},
|
||||
None,
|
||||
@@ -306,6 +204,7 @@ impl ThreadStore {
|
||||
};
|
||||
let worktree_info = WorktreeContext {
|
||||
root_name,
|
||||
abs_path,
|
||||
rules_file,
|
||||
};
|
||||
(worktree_info, rules_file_error)
|
||||
@@ -349,27 +248,6 @@ impl ThreadStore {
|
||||
self.context_server_manager.clone()
|
||||
}
|
||||
|
||||
pub fn prompt_store(&self) -> Option<Entity<PromptStore>> {
|
||||
self.prompt_store.clone()
|
||||
}
|
||||
|
||||
pub fn load_rules(
|
||||
&self,
|
||||
prompt_id: UserPromptId,
|
||||
cx: &App,
|
||||
) -> Task<Result<(PromptMetadata, String)>> {
|
||||
let prompt_id = PromptId::User { uuid: prompt_id };
|
||||
let Some(prompt_store) = self.prompt_store.as_ref() else {
|
||||
return Task::ready(Err(anyhow!("Prompt store unexpectedly missing.")));
|
||||
};
|
||||
let prompt_store = prompt_store.read(cx);
|
||||
let Some(metadata) = prompt_store.metadata(prompt_id) else {
|
||||
return Task::ready(Err(anyhow!("User rules not found in library.")));
|
||||
};
|
||||
let text_task = prompt_store.load(prompt_id, cx);
|
||||
cx.background_spawn(async move { Ok((metadata, text_task.await?)) })
|
||||
}
|
||||
|
||||
pub fn tools(&self) -> Entity<ToolWorkingSet> {
|
||||
self.tools.clone()
|
||||
}
|
||||
@@ -684,18 +562,9 @@ pub struct SerializedMessage {
|
||||
#[serde(tag = "type")]
|
||||
pub enum SerializedMessageSegment {
|
||||
#[serde(rename = "text")]
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
Text { text: String },
|
||||
#[serde(rename = "thinking")]
|
||||
Thinking {
|
||||
text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
signature: Option<String>,
|
||||
},
|
||||
RedactedThinking {
|
||||
data: Vec<u8>,
|
||||
},
|
||||
Thinking { text: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -7,13 +7,13 @@ use futures::FutureExt as _;
|
||||
use futures::future::Shared;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
|
||||
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolResult,
|
||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
|
||||
};
|
||||
use ui::IconName;
|
||||
use util::truncate_lines_to_byte_limit;
|
||||
|
||||
use crate::thread::{MessageId, PromptId, ThreadId};
|
||||
use crate::thread::MessageId;
|
||||
use crate::thread_store::SerializedMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -36,7 +36,6 @@ pub struct ToolUseState {
|
||||
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
|
||||
tool_use_metadata_by_id: HashMap<LanguageModelToolUseId, ToolUseMetadata>,
|
||||
}
|
||||
|
||||
impl ToolUseState {
|
||||
@@ -48,7 +47,6 @@ impl ToolUseState {
|
||||
tool_results: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
tool_result_cards: HashMap::default(),
|
||||
tool_use_metadata_by_id: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +73,6 @@ impl ToolUseState {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
input: tool_use.input.clone(),
|
||||
is_input_complete: true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -177,9 +174,6 @@ impl ToolUseState {
|
||||
PendingToolUseStatus::Error(ref err) => {
|
||||
ToolUseStatus::Error(err.clone().into())
|
||||
}
|
||||
PendingToolUseStatus::InputStillStreaming => {
|
||||
ToolUseStatus::InputStillStreaming
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ToolUseStatus::Pending
|
||||
@@ -196,12 +190,7 @@ impl ToolUseState {
|
||||
tool_uses.push(ToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
ui_text: self.tool_ui_label(
|
||||
&tool_use.name,
|
||||
&tool_use.input,
|
||||
tool_use.is_input_complete,
|
||||
cx,
|
||||
),
|
||||
ui_text: self.tool_ui_label(&tool_use.name, &tool_use.input, cx),
|
||||
input: tool_use.input.clone(),
|
||||
status,
|
||||
icon,
|
||||
@@ -216,15 +205,10 @@ impl ToolUseState {
|
||||
&self,
|
||||
tool_name: &str,
|
||||
input: &serde_json::Value,
|
||||
is_input_complete: bool,
|
||||
cx: &App,
|
||||
) -> SharedString {
|
||||
if let Some(tool) = self.tools.read(cx).tool(tool_name, cx) {
|
||||
if is_input_complete {
|
||||
tool.ui_text(input).into()
|
||||
} else {
|
||||
tool.still_streaming_ui_text(input).into()
|
||||
}
|
||||
tool.ui_text(input).into()
|
||||
} else {
|
||||
format!("Unknown tool {tool_name:?}").into()
|
||||
}
|
||||
@@ -270,52 +254,20 @@ impl ToolUseState {
|
||||
&mut self,
|
||||
assistant_message_id: MessageId,
|
||||
tool_use: LanguageModelToolUse,
|
||||
metadata: ToolUseMetadata,
|
||||
cx: &App,
|
||||
) -> Arc<str> {
|
||||
let tool_uses = self
|
||||
.tool_uses_by_assistant_message
|
||||
) {
|
||||
self.tool_uses_by_assistant_message
|
||||
.entry(assistant_message_id)
|
||||
.or_default();
|
||||
.or_default()
|
||||
.push(tool_use.clone());
|
||||
|
||||
let mut existing_tool_use_found = false;
|
||||
|
||||
for existing_tool_use in tool_uses.iter_mut() {
|
||||
if existing_tool_use.id == tool_use.id {
|
||||
*existing_tool_use = tool_use.clone();
|
||||
existing_tool_use_found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !existing_tool_use_found {
|
||||
tool_uses.push(tool_use.clone());
|
||||
}
|
||||
|
||||
let status = if tool_use.is_input_complete {
|
||||
self.tool_use_metadata_by_id
|
||||
.insert(tool_use.id.clone(), metadata);
|
||||
|
||||
// The tool use is being requested by the Assistant, so we want to
|
||||
// attach the tool results to the next user message.
|
||||
let next_user_message_id = MessageId(assistant_message_id.0 + 1);
|
||||
self.tool_uses_by_user_message
|
||||
.entry(next_user_message_id)
|
||||
.or_default()
|
||||
.push(tool_use.id.clone());
|
||||
|
||||
PendingToolUseStatus::Idle
|
||||
} else {
|
||||
PendingToolUseStatus::InputStillStreaming
|
||||
};
|
||||
|
||||
let ui_text: Arc<str> = self
|
||||
.tool_ui_label(
|
||||
&tool_use.name,
|
||||
&tool_use.input,
|
||||
tool_use.is_input_complete,
|
||||
cx,
|
||||
)
|
||||
.into();
|
||||
// The tool use is being requested by the Assistant, so we want to
|
||||
// attach the tool results to the next user message.
|
||||
let next_user_message_id = MessageId(assistant_message_id.0 + 1);
|
||||
self.tool_uses_by_user_message
|
||||
.entry(next_user_message_id)
|
||||
.or_default()
|
||||
.push(tool_use.id.clone());
|
||||
|
||||
self.pending_tool_uses_by_id.insert(
|
||||
tool_use.id.clone(),
|
||||
@@ -323,13 +275,13 @@ impl ToolUseState {
|
||||
assistant_message_id,
|
||||
id: tool_use.id,
|
||||
name: tool_use.name.clone(),
|
||||
ui_text: ui_text.clone(),
|
||||
ui_text: self
|
||||
.tool_ui_label(&tool_use.name, &tool_use.input, cx)
|
||||
.into(),
|
||||
input: tool_use.input,
|
||||
status,
|
||||
status: PendingToolUseStatus::Idle,
|
||||
},
|
||||
);
|
||||
|
||||
ui_text
|
||||
}
|
||||
|
||||
pub fn run_pending_tool(
|
||||
@@ -375,21 +327,7 @@ impl ToolUseState {
|
||||
output: Result<String>,
|
||||
cx: &App,
|
||||
) -> Option<PendingToolUse> {
|
||||
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
|
||||
|
||||
telemetry::event!(
|
||||
"Agent Tool Finished",
|
||||
model = metadata
|
||||
.as_ref()
|
||||
.map(|metadata| metadata.model.telemetry_id()),
|
||||
model_provider = metadata
|
||||
.as_ref()
|
||||
.map(|metadata| metadata.model.provider_id().to_string()),
|
||||
thread_id = metadata.as_ref().map(|metadata| metadata.thread_id.clone()),
|
||||
prompt_id = metadata.as_ref().map(|metadata| metadata.prompt_id.clone()),
|
||||
tool_name,
|
||||
success = output.is_ok()
|
||||
);
|
||||
telemetry::event!("Agent Tool Finished", tool_name, success = output.is_ok());
|
||||
|
||||
match output {
|
||||
Ok(tool_result) => {
|
||||
@@ -539,7 +477,6 @@ pub struct Confirmation {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PendingToolUseStatus {
|
||||
InputStillStreaming,
|
||||
Idle,
|
||||
NeedsConfirmation(Arc<Confirmation>),
|
||||
Running { _task: Shared<Task<()>> },
|
||||
@@ -559,10 +496,3 @@ impl PendingToolUseStatus {
|
||||
matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolUseMetadata {
|
||||
pub model: Arc<dyn LanguageModel>,
|
||||
pub thread_id: ThreadId,
|
||||
pub prompt_id: PromptId,
|
||||
}
|
||||
|
||||
@@ -354,16 +354,6 @@ impl AddedContext {
|
||||
.read(cx)
|
||||
.is_generating_detailed_summary(),
|
||||
},
|
||||
|
||||
AssistantContext::Rules(user_rules_context) => AddedContext {
|
||||
id: user_rules_context.id,
|
||||
kind: ContextKind::Rules,
|
||||
name: user_rules_context.title.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
summarizing: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +74,6 @@ pub enum Model {
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn default_fast() -> Self {
|
||||
Self::Claude3_5Haiku
|
||||
}
|
||||
|
||||
pub fn from_id(id: &str) -> Result<Self> {
|
||||
if id.starts_with("claude-3-5-sonnet") {
|
||||
Ok(Self::Claude3_5Sonnet)
|
||||
@@ -511,15 +507,6 @@ pub enum RequestContent {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<CacheControl>,
|
||||
},
|
||||
#[serde(rename = "thinking")]
|
||||
Thinking {
|
||||
thinking: String,
|
||||
signature: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_control: Option<CacheControl>,
|
||||
},
|
||||
#[serde(rename = "redacted_thinking")]
|
||||
RedactedThinking { data: String },
|
||||
#[serde(rename = "image")]
|
||||
Image {
|
||||
source: ImageSource,
|
||||
|
||||
@@ -8,7 +8,7 @@ mod terminal_inline_assistant;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AssistantSettings, LanguageModelSelection};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
@@ -161,38 +161,71 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
|
||||
fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
// Default model - used as fallback
|
||||
let active_model_provider_name =
|
||||
LanguageModelProviderId::from(settings.default_model.provider.clone());
|
||||
let active_model_id = LanguageModelId::from(settings.default_model.model.clone());
|
||||
|
||||
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
|
||||
language_model::SelectedModel {
|
||||
provider: LanguageModelProviderId::from(selection.provider.clone()),
|
||||
model: LanguageModelId::from(selection.model.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
let default = to_selected_model(&settings.default_model);
|
||||
let inline_assistant = settings
|
||||
// Inline assistant model
|
||||
let inline_assistant_model = settings
|
||||
.inline_assistant_model
|
||||
.as_ref()
|
||||
.map(to_selected_model);
|
||||
let commit_message = settings
|
||||
.unwrap_or(&settings.default_model);
|
||||
let inline_assistant_provider_name =
|
||||
LanguageModelProviderId::from(inline_assistant_model.provider.clone());
|
||||
let inline_assistant_model_id = LanguageModelId::from(inline_assistant_model.model.clone());
|
||||
|
||||
// Commit message model
|
||||
let commit_message_model = settings
|
||||
.commit_message_model
|
||||
.as_ref()
|
||||
.map(to_selected_model);
|
||||
let thread_summary = settings
|
||||
.unwrap_or(&settings.default_model);
|
||||
let commit_message_provider_name =
|
||||
LanguageModelProviderId::from(commit_message_model.provider.clone());
|
||||
let commit_message_model_id = LanguageModelId::from(commit_message_model.model.clone());
|
||||
|
||||
// Thread summary model
|
||||
let thread_summary_model = settings
|
||||
.thread_summary_model
|
||||
.as_ref()
|
||||
.map(to_selected_model);
|
||||
.unwrap_or(&settings.default_model);
|
||||
let thread_summary_provider_name =
|
||||
LanguageModelProviderId::from(thread_summary_model.provider.clone());
|
||||
let thread_summary_model_id = LanguageModelId::from(thread_summary_model.model.clone());
|
||||
|
||||
let inline_alternatives = settings
|
||||
.inline_alternatives
|
||||
.iter()
|
||||
.map(to_selected_model)
|
||||
.map(|alternative| {
|
||||
(
|
||||
LanguageModelProviderId::from(alternative.provider.clone()),
|
||||
LanguageModelId::from(alternative.model.clone()),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_default_model(Some(&default), cx);
|
||||
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
|
||||
registry.select_commit_message_model(commit_message.as_ref(), cx);
|
||||
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
|
||||
// Set the default model
|
||||
registry.select_default_model(&active_model_provider_name, &active_model_id, cx);
|
||||
|
||||
// Set the specific models
|
||||
registry.select_inline_assistant_model(
|
||||
&inline_assistant_provider_name,
|
||||
&inline_assistant_model_id,
|
||||
cx,
|
||||
);
|
||||
registry.select_commit_message_model(
|
||||
&commit_message_provider_name,
|
||||
&commit_message_model_id,
|
||||
cx,
|
||||
);
|
||||
registry.select_thread_summary_model(
|
||||
&thread_summary_provider_name,
|
||||
&thread_summary_model_id,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Set the alternatives
|
||||
registry.select_inline_alternative_models(inline_alternatives, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ use language_model::{
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::{PromptBuilder, PromptId, UserPromptId};
|
||||
use prompt_store::PromptBuilder;
|
||||
|
||||
use search::{BufferSearchBar, buffer_search::DivRegistrar};
|
||||
use settings::{Settings, update_settings_file};
|
||||
@@ -58,11 +58,11 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(AssistantPanel::show_configuration)
|
||||
.register_action(AssistantPanel::create_new_context)
|
||||
.register_action(AssistantPanel::restart_context_servers)
|
||||
.register_action(|workspace, action: &OpenPromptLibrary, window, cx| {
|
||||
.register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_prompt_library(action, window, cx)
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -272,10 +272,7 @@ impl AssistantPanel {
|
||||
menu.context(focus_handle.clone())
|
||||
.action("New Chat", Box::new(NewChat))
|
||||
.action("History", Box::new(DeployHistory))
|
||||
.action(
|
||||
"Prompt Library",
|
||||
Box::new(OpenPromptLibrary::default()),
|
||||
)
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary))
|
||||
.action("Configure", Box::new(ShowConfiguration))
|
||||
.action(zoom_label, Box::new(ToggleZoom))
|
||||
}))
|
||||
@@ -1046,7 +1043,7 @@ impl AssistantPanel {
|
||||
|
||||
fn deploy_prompt_library(
|
||||
&mut self,
|
||||
action: &OpenPromptLibrary,
|
||||
_: &OpenPromptLibrary,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1060,9 +1057,6 @@ impl AssistantPanel {
|
||||
None,
|
||||
))
|
||||
}),
|
||||
action.prompt_to_select.map(|uuid| PromptId::User {
|
||||
uuid: UserPromptId(uuid),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
@@ -37,7 +37,7 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role, report_assistant_event,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, LspAction, ProjectTransaction};
|
||||
@@ -1766,7 +1766,6 @@ impl PromptEditor {
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -2979,8 +2978,6 @@ impl CodegenAlternative {
|
||||
});
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -19,7 +19,7 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
Role, report_assistant_event,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use std::{
|
||||
@@ -292,8 +292,6 @@ impl TerminalInlineAssistant {
|
||||
});
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
@@ -755,7 +753,6 @@ impl PromptEditor {
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -2373,7 +2373,7 @@ impl AssistantContext {
|
||||
LanguageModelCompletionEvent::Stop(reason) => {
|
||||
stop_reason = reason;
|
||||
}
|
||||
LanguageModelCompletionEvent::Thinking { text: chunk, .. } => {
|
||||
LanguageModelCompletionEvent::Thinking(chunk) => {
|
||||
if thought_process_stack.is_empty() {
|
||||
let start =
|
||||
buffer.anchor_before(message_old_end_offset);
|
||||
@@ -2555,8 +2555,6 @@ impl AssistantContext {
|
||||
}
|
||||
|
||||
let mut completion_request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -39,7 +39,7 @@ use language_model::{
|
||||
Role,
|
||||
};
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ModelType, ToggleModelSelector,
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::Picker;
|
||||
@@ -298,7 +298,6 @@ impl ContextEditor {
|
||||
move |settings, _| settings.set_model(model.clone()),
|
||||
);
|
||||
},
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1814,10 +1813,7 @@ impl ContextEditor {
|
||||
.selections
|
||||
.all_adjusted(cx)
|
||||
.into_iter()
|
||||
.filter_map(|s| {
|
||||
(!s.is_empty())
|
||||
.then(|| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
||||
})
|
||||
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
Some((selections, buffer))
|
||||
|
||||
@@ -54,9 +54,9 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
cx.background_spawn(async move {
|
||||
let store = store.await?;
|
||||
let prompts = store.read_with(cx, |store, _cx| store.default_prompt_metadata())?;
|
||||
let prompts = store.default_prompt_metadata();
|
||||
|
||||
let mut text = String::new();
|
||||
text.push('\n');
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::{
|
||||
};
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use prompt_store::{PromptMetadata, PromptStore};
|
||||
use prompt_store::PromptStore;
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
@@ -43,12 +43,8 @@ impl SlashCommand for PromptSlashCommand {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
let query = arguments.to_owned().join(" ");
|
||||
cx.spawn(async move |cx| {
|
||||
let cancellation_flag = Arc::new(AtomicBool::default());
|
||||
let prompts: Vec<PromptMetadata> = store
|
||||
.await?
|
||||
.read_with(cx, |store, cx| store.search(query, cancellation_flag, cx))?
|
||||
.await;
|
||||
cx.background_spawn(async move {
|
||||
let prompts = store.await?.search(query).await;
|
||||
Ok(prompts
|
||||
.into_iter()
|
||||
.filter_map(|prompt| {
|
||||
@@ -81,18 +77,14 @@ impl SlashCommand for PromptSlashCommand {
|
||||
|
||||
let store = PromptStore::global(cx);
|
||||
let title = SharedString::from(title.clone());
|
||||
let prompt = cx.spawn({
|
||||
let prompt = cx.background_spawn({
|
||||
let title = title.clone();
|
||||
async move |cx| {
|
||||
async move {
|
||||
let store = store.await?;
|
||||
let body = store
|
||||
.read_with(cx, |store, cx| {
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
anyhow::Ok(store.load(prompt_id, cx))
|
||||
})??
|
||||
.await?;
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
let body = store.load(prompt_id).await?;
|
||||
anyhow::Ok(body)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,7 +30,6 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToolUseStatus {
|
||||
InputStillStreaming,
|
||||
NeedsConfirmation,
|
||||
Pending,
|
||||
Running,
|
||||
@@ -42,7 +41,6 @@ impl ToolUseStatus {
|
||||
pub fn text(&self) -> SharedString {
|
||||
match self {
|
||||
ToolUseStatus::NeedsConfirmation => "".into(),
|
||||
ToolUseStatus::InputStillStreaming => "".into(),
|
||||
ToolUseStatus::Pending => "".into(),
|
||||
ToolUseStatus::Running => "".into(),
|
||||
ToolUseStatus::Finished(out) => out.clone(),
|
||||
@@ -150,12 +148,6 @@ pub trait Tool: 'static + Send + Sync {
|
||||
/// Returns markdown to be displayed in the UI for this tool.
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String;
|
||||
|
||||
/// Returns markdown to be displayed in the UI for this tool, while the input JSON is still streaming
|
||||
/// (so information may be missing).
|
||||
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
|
||||
self.ui_text(input)
|
||||
}
|
||||
|
||||
/// Runs the tool with the provided input.
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
|
||||
@@ -9,13 +9,13 @@ mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_file_tool;
|
||||
mod fetch_tool;
|
||||
mod grep_tool;
|
||||
mod list_directory_tool;
|
||||
mod move_path_tool;
|
||||
mod now_tool;
|
||||
mod open_tool;
|
||||
mod path_search_tool;
|
||||
mod read_file_tool;
|
||||
mod regex_search_tool;
|
||||
mod rename_tool;
|
||||
mod replace;
|
||||
mod schema;
|
||||
@@ -44,12 +44,12 @@ use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::diagnostics_tool::DiagnosticsTool;
|
||||
use crate::edit_file_tool::EditFileTool;
|
||||
use crate::fetch_tool::FetchTool;
|
||||
use crate::grep_tool::GrepTool;
|
||||
use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::open_tool::OpenTool;
|
||||
use crate::path_search_tool::PathSearchTool;
|
||||
use crate::read_file_tool::ReadFileTool;
|
||||
use crate::regex_search_tool::RegexSearchTool;
|
||||
use crate::rename_tool::RenameTool;
|
||||
use crate::symbol_info_tool::SymbolInfoTool;
|
||||
use crate::terminal_tool::TerminalTool;
|
||||
@@ -77,7 +77,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(ContentsTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(GrepTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
registry.register_tool(RenameTool);
|
||||
registry.register_tool(ThinkingTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
|
||||
@@ -43,7 +43,7 @@ pub struct BatchToolInput {
|
||||
/// }
|
||||
/// },
|
||||
/// {
|
||||
/// "name": "grep",
|
||||
/// "name": "regex_search",
|
||||
/// "input": {
|
||||
/// "regex": "fn run\\("
|
||||
/// }
|
||||
@@ -91,7 +91,7 @@ pub struct BatchToolInput {
|
||||
/// {
|
||||
/// "invocations": [
|
||||
/// {
|
||||
/// "name": "grep",
|
||||
/// "name": "regex_search",
|
||||
/// "input": {
|
||||
/// "regex": "impl Database"
|
||||
/// }
|
||||
|
||||
@@ -33,18 +33,8 @@ pub struct CreateFileToolInput {
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct PartialInput {
|
||||
#[serde(default)]
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
contents: String,
|
||||
}
|
||||
|
||||
pub struct CreateFileTool;
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Create file";
|
||||
|
||||
impl Tool for CreateFileTool {
|
||||
fn name(&self) -> String {
|
||||
"create_file".into()
|
||||
@@ -72,14 +62,7 @@ impl Tool for CreateFileTool {
|
||||
let path = MarkdownString::inline_code(&input.path);
|
||||
format!("Create file {path}")
|
||||
}
|
||||
Err(_) => DEFAULT_UI_TEXT.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<PartialInput>(input.clone()).ok() {
|
||||
Some(input) if !input.path.is_empty() => input.path,
|
||||
_ => DEFAULT_UI_TEXT.to_string(),
|
||||
Err(_) => "Create file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,60 +111,3 @@ impl Tool for CreateFileTool {
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path() {
|
||||
let tool = CreateFileTool;
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "src/main.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_without_path() {
|
||||
let tool = CreateFileTool;
|
||||
let input = json!({
|
||||
"path": "",
|
||||
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_null() {
|
||||
let tool = CreateFileTool;
|
||||
let input = serde_json::Value::Null;
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_text_with_valid_input() {
|
||||
let tool = CreateFileTool;
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
|
||||
});
|
||||
|
||||
assert_eq!(tool.ui_text(&input), "Create file `src/main.rs`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_text_with_invalid_input() {
|
||||
let tool = CreateFileTool;
|
||||
let input = json!({
|
||||
"invalid": "field"
|
||||
});
|
||||
|
||||
assert_eq!(tool.ui_text(&input), DEFAULT_UI_TEXT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,22 +47,8 @@ pub struct EditFileToolInput {
|
||||
pub new_string: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct PartialInput {
|
||||
#[serde(default)]
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
display_description: String,
|
||||
#[serde(default)]
|
||||
old_string: String,
|
||||
#[serde(default)]
|
||||
new_string: String,
|
||||
}
|
||||
|
||||
pub struct EditFileTool;
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Edit file";
|
||||
|
||||
impl Tool for EditFileTool {
|
||||
fn name(&self) -> String {
|
||||
"edit_file".into()
|
||||
@@ -91,22 +77,6 @@ impl Tool for EditFileTool {
|
||||
}
|
||||
}
|
||||
|
||||
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
|
||||
if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
|
||||
let description = input.display_description.trim();
|
||||
if !description.is_empty() {
|
||||
return description.to_string();
|
||||
}
|
||||
|
||||
let path = input.path.trim();
|
||||
if !path.is_empty() {
|
||||
return path.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_UI_TEXT.to_string()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
@@ -211,69 +181,3 @@ impl Tool for EditFileTool {
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "src/main.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_description() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "Fix error handling");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path_and_description() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "Fix error handling");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_no_path_or_description() {
|
||||
let tool = EditFileTool;
|
||||
let input = json!({
|
||||
"path": "",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_null() {
|
||||
let tool = EditFileTool;
|
||||
let input = serde_json::Value::Null;
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
|
||||
}
|
||||
}
|
||||
|
||||
268
crates/assistant_tools/src/find_replace_file_tool.rs
Normal file
268
crates/assistant_tools/src/find_replace_file_tool.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
use crate::replace::replace_exact;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FindReplaceFileToolInput {
|
||||
/// The path of the file to modify.
|
||||
///
|
||||
/// WARNING: When specifying which file path need changing, you MUST
|
||||
/// start each path with one of the project's root directories.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - backend
|
||||
/// - frontend
|
||||
///
|
||||
/// <example>
|
||||
/// `backend/src/main.rs`
|
||||
///
|
||||
/// Notice how the file path starts with root-1. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// `frontend/db.js`
|
||||
/// </example>
|
||||
pub path: PathBuf,
|
||||
|
||||
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
pub display_description: String,
|
||||
|
||||
/// The unique string to find in the file. This string cannot be empty;
|
||||
/// if the string is empty, the tool call will fail. Remember, do not use this tool
|
||||
/// to create new files from scratch, or to overwrite existing files! Use a different
|
||||
/// approach if you want to do that.
|
||||
///
|
||||
/// If this string appears more than once in the file, this tool call will fail,
|
||||
/// so it is absolutely critical that you verify ahead of time that the string
|
||||
/// is unique. You can search within the file to verify this.
|
||||
///
|
||||
/// To make the string more likely to be unique, include a minimum of 3 lines of context
|
||||
/// before the string you actually want to find, as well as a minimum of 3 lines of
|
||||
/// context after the string you want to find. (These lines of context should appear
|
||||
/// in the `replace` string as well.) If 3 lines of context is not enough to obtain
|
||||
/// a string that appears only once in the file, then double the number of context lines
|
||||
/// until the string becomes unique. (Start with 3 lines before and 3 lines after
|
||||
/// though, because too much context is needlessly costly.)
|
||||
///
|
||||
/// Do not alter the context lines of code in any way, and make sure to preserve all
|
||||
/// whitespace and indentation for all lines of code. This string must be exactly as
|
||||
/// it appears in the file, because this tool will do a literal find/replace, and if
|
||||
/// even one character in this string is different in any way from how it appears
|
||||
/// in the file, then the tool call will fail.
|
||||
///
|
||||
/// If you get an error that the `find` string was not found, this means that either
|
||||
/// you made a mistake, or that the file has changed since you last looked at it.
|
||||
/// Either way, when this happens, you should retry doing this tool call until it
|
||||
/// succeeds, up to 3 times. Each time you retry, you should take another look at
|
||||
/// the exact text of the file in question, to make sure that you are searching for
|
||||
/// exactly the right string. Regardless of whether it was because you made a mistake
|
||||
/// or because the file changed since you last looked at it, you should be extra
|
||||
/// careful when retrying in this way. It's a bad experience for the user if
|
||||
/// this `find` string isn't found, so be super careful to get it exactly right!
|
||||
///
|
||||
/// <example>
|
||||
/// If a file contains this code:
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
|
||||
/// // Check if user exists first
|
||||
/// let user = database.find_user(user_id)?;
|
||||
///
|
||||
/// // This is the part we want to modify
|
||||
/// if user.role == "admin" {
|
||||
/// return Ok(true);
|
||||
/// }
|
||||
///
|
||||
/// // Check other permissions
|
||||
/// check_custom_permissions(user_id)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Your find string should include at least 3 lines of context before and after the part
|
||||
/// you want to change:
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
|
||||
/// // Check if user exists first
|
||||
/// let user = database.find_user(user_id)?;
|
||||
///
|
||||
/// // This is the part we want to modify
|
||||
/// if user.role == "admin" {
|
||||
/// return Ok(true);
|
||||
/// }
|
||||
///
|
||||
/// // Check other permissions
|
||||
/// check_custom_permissions(user_id)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// And your replace string might look like:
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
|
||||
/// // Check if user exists first
|
||||
/// let user = database.find_user(user_id)?;
|
||||
///
|
||||
/// // This is the part we want to modify
|
||||
/// if user.role == "admin" || user.role == "superuser" {
|
||||
/// return Ok(true);
|
||||
/// }
|
||||
///
|
||||
/// // Check other permissions
|
||||
/// check_custom_permissions(user_id)
|
||||
/// }
|
||||
/// ```
|
||||
/// </example>
|
||||
pub find: String,
|
||||
|
||||
/// The string to replace the one unique occurrence of the find string with.
|
||||
pub replace: String,
|
||||
}
|
||||
|
||||
pub struct FindReplaceFileTool;
|
||||
|
||||
impl Tool for FindReplaceFileTool {
|
||||
fn name(&self) -> String {
|
||||
"find_replace_file".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("find_replace_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Pencil
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<FindReplaceFileToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<FindReplaceFileToolInput>(input.clone()) {
|
||||
Ok(input) => input.display_description,
|
||||
Err(_) => "Edit file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&input.path, cx)
|
||||
.context("Path not found in project")
|
||||
})??;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
if input.find.is_empty() {
|
||||
return Err(anyhow!("`find` string cannot be empty. Use a different tool if you want to create a file."));
|
||||
}
|
||||
|
||||
if input.find == input.replace {
|
||||
return Err(anyhow!("The `find` and `replace` strings are identical, so no changes would be made."));
|
||||
}
|
||||
|
||||
let result = cx
|
||||
.background_spawn(async move {
|
||||
// Try to match exactly
|
||||
let diff = replace_exact(&input.find, &input.replace, &snapshot)
|
||||
.await
|
||||
// If that fails, try being flexible about indentation
|
||||
.or_else(|| replace_with_flexible_indent(&input.find, &input.replace, &snapshot))?;
|
||||
|
||||
if diff.edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let old_text = snapshot.text();
|
||||
|
||||
Some((old_text, diff))
|
||||
})
|
||||
.await;
|
||||
|
||||
let Some((old_text, diff)) = result else {
|
||||
let err = buffer.read_with(cx, |buffer, _cx| {
|
||||
let file_exists = buffer
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists());
|
||||
|
||||
if !file_exists {
|
||||
anyhow!("{} does not exist", input.path.display())
|
||||
} else if buffer.is_empty() {
|
||||
anyhow!(
|
||||
"{} is empty, so the provided `find` string wasn't found.",
|
||||
input.path.display()
|
||||
)
|
||||
} else {
|
||||
anyhow!("Failed to match the provided `find` string")
|
||||
}
|
||||
})?;
|
||||
|
||||
return Err(err)
|
||||
};
|
||||
|
||||
let snapshot = cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx)
|
||||
});
|
||||
let snapshot = buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.apply_diff(diff, cx);
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.snapshot()
|
||||
});
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), cx)
|
||||
});
|
||||
snapshot
|
||||
})?;
|
||||
|
||||
project.update( cx, |project, cx| {
|
||||
project.save_buffer(buffer, cx)
|
||||
})?.await?;
|
||||
|
||||
let diff_str = cx.background_spawn(async move {
|
||||
let new_text = snapshot.text();
|
||||
language::unified_diff(&old_text, &new_text)
|
||||
}).await;
|
||||
|
||||
|
||||
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
|
||||
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::OffsetRangeExt;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{
|
||||
Project,
|
||||
search::{SearchQuery, SearchResult},
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, fmt::Write, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GrepToolInput {
|
||||
/// A regex pattern to search for in the entire project. Note that the regex
|
||||
/// will be parsed by the Rust `regex` crate.
|
||||
pub regex: String,
|
||||
|
||||
/// A glob pattern for the paths of files to include in the search.
|
||||
/// Supports standard glob patterns like "**/*.rs" or "src/**/*.ts".
|
||||
/// If omitted, all files in the project will be searched.
|
||||
pub include_pattern: Option<String>,
|
||||
|
||||
/// Optional starting position for paginated results (0-based).
|
||||
/// When not provided, starts from the beginning.
|
||||
#[serde(default)]
|
||||
pub offset: u32,
|
||||
|
||||
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
|
||||
#[serde(default)]
|
||||
pub case_sensitive: bool,
|
||||
}
|
||||
|
||||
impl GrepToolInput {
|
||||
/// Which page of search results this is.
|
||||
pub fn page(&self) -> u32 {
|
||||
1 + (self.offset / RESULTS_PER_PAGE)
|
||||
}
|
||||
}
|
||||
|
||||
const RESULTS_PER_PAGE: u32 = 20;
|
||||
|
||||
pub struct GrepTool;
|
||||
|
||||
impl Tool for GrepTool {
|
||||
fn name(&self) -> String {
|
||||
"grep".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./grep_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Regex
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<GrepToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<GrepToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
let regex_str = MarkdownString::inline_code(&input.regex);
|
||||
let case_info = if input.case_sensitive {
|
||||
" (case-sensitive)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
if page > 1 {
|
||||
format!("Get page {page} of search results for regex {regex_str}{case_info}")
|
||||
} else {
|
||||
format!("Search files for regex {regex_str}{case_info}")
|
||||
}
|
||||
}
|
||||
Err(_) => "Search with regex".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
|
||||
let input = match serde_json::from_value::<GrepToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
return Task::ready(Err(anyhow!("Failed to parse input: {}", error))).into();
|
||||
}
|
||||
};
|
||||
|
||||
let include_matcher = match PathMatcher::new(
|
||||
input
|
||||
.include_pattern
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
) {
|
||||
Ok(matcher) => matcher,
|
||||
Err(error) => {
|
||||
return Task::ready(Err(anyhow!("invalid include glob pattern: {}", error))).into();
|
||||
}
|
||||
};
|
||||
|
||||
let query = match SearchQuery::regex(
|
||||
&input.regex,
|
||||
false,
|
||||
input.case_sensitive,
|
||||
false,
|
||||
false,
|
||||
include_matcher,
|
||||
PathMatcher::default(), // For now, keep it simple and don't enable an exclude pattern.
|
||||
true, // Always match file include pattern against *full project paths* that start with a project root.
|
||||
None,
|
||||
) {
|
||||
Ok(query) => query,
|
||||
Err(error) => return Task::ready(Err(error)).into(),
|
||||
};
|
||||
|
||||
let results = project.update(cx, |project, cx| project.search(query, cx));
|
||||
|
||||
cx.spawn(async move|cx| {
|
||||
futures::pin_mut!(results);
|
||||
|
||||
let mut output = String::new();
|
||||
let mut skips_remaining = input.offset;
|
||||
let mut matches_found = 0;
|
||||
let mut has_more_matches = false;
|
||||
|
||||
while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
|
||||
if ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.read_with(cx, |buffer, cx| -> Result<(), anyhow::Error> {
|
||||
if let Some(path) = buffer.file().map(|file| file.full_path(cx)) {
|
||||
let mut file_header_written = false;
|
||||
let mut ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut point_range = range.to_point(buffer);
|
||||
point_range.start.row =
|
||||
point_range.start.row.saturating_sub(CONTEXT_LINES);
|
||||
point_range.start.column = 0;
|
||||
point_range.end.row = cmp::min(
|
||||
buffer.max_point().row,
|
||||
point_range.end.row + CONTEXT_LINES,
|
||||
);
|
||||
point_range.end.column = buffer.line_len(point_range.end.row);
|
||||
point_range
|
||||
})
|
||||
.peekable();
|
||||
|
||||
while let Some(mut range) = ranges.next() {
|
||||
if skips_remaining > 0 {
|
||||
skips_remaining -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// We'd already found a full page of matches, and we just found one more.
|
||||
if matches_found >= RESULTS_PER_PAGE {
|
||||
has_more_matches = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
while let Some(next_range) = ranges.peek() {
|
||||
if range.end.row >= next_range.start.row {
|
||||
range.end = next_range.end;
|
||||
ranges.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !file_header_written {
|
||||
writeln!(output, "\n## Matches in {}", path.display())?;
|
||||
file_header_written = true;
|
||||
}
|
||||
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
writeln!(output, "\n### Lines {start_line}-{end_line}\n```")?;
|
||||
output.extend(buffer.text_for_range(range));
|
||||
output.push_str("\n```\n");
|
||||
|
||||
matches_found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})??;
|
||||
}
|
||||
|
||||
if matches_found == 0 {
|
||||
Ok("No matches found".to_string())
|
||||
} else if has_more_matches {
|
||||
Ok(format!(
|
||||
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
|
||||
input.offset + 1,
|
||||
input.offset + matches_found,
|
||||
input.offset + RESULTS_PER_PAGE,
|
||||
))
|
||||
} else {
|
||||
Ok(format!("Found {matches_found} matches:\n{output}"))
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use project::{FakeFs, Project};
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
serde_json::json!({
|
||||
"src": {
|
||||
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
|
||||
"utils": {
|
||||
"helper.rs": "fn helper() {\n println!(\"I'm a helper!\");\n}",
|
||||
},
|
||||
},
|
||||
"tests": {
|
||||
"test_main.rs": "fn test_main() {\n assert!(true);\n}",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
// Test with include pattern for Rust files inside the root of the project
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "println".to_string(),
|
||||
include_pattern: Some("root/**/*.rs".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(result.contains("main.rs"), "Should find matches in main.rs");
|
||||
assert!(
|
||||
result.contains("helper.rs"),
|
||||
"Should find matches in helper.rs"
|
||||
);
|
||||
assert!(
|
||||
!result.contains("test_main.rs"),
|
||||
"Should not include test_main.rs even though it's a .rs file (because it doesn't have the pattern)"
|
||||
);
|
||||
|
||||
// Test with include pattern for src directory only
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "fn".to_string(),
|
||||
include_pattern: Some("root/**/src/**".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(
|
||||
result.contains("main.rs"),
|
||||
"Should find matches in src/main.rs"
|
||||
);
|
||||
assert!(
|
||||
result.contains("helper.rs"),
|
||||
"Should find matches in src/utils/helper.rs"
|
||||
);
|
||||
assert!(
|
||||
!result.contains("test_main.rs"),
|
||||
"Should not include test_main.rs as it's not in src directory"
|
||||
);
|
||||
|
||||
// Test with empty include pattern (should default to all files)
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "fn".to_string(),
|
||||
include_pattern: None,
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(result.contains("main.rs"), "Should find matches in main.rs");
|
||||
assert!(
|
||||
result.contains("helper.rs"),
|
||||
"Should find matches in helper.rs"
|
||||
);
|
||||
assert!(
|
||||
result.contains("test_main.rs"),
|
||||
"Should include test_main.rs"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
serde_json::json!({
|
||||
"case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
// Test case-insensitive search (default)
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "uppercase".to_string(),
|
||||
include_pattern: Some("**/*.txt".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: false,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(
|
||||
result.contains("UPPERCASE"),
|
||||
"Case-insensitive search should match uppercase"
|
||||
);
|
||||
|
||||
// Test case-sensitive search
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "uppercase".to_string(),
|
||||
include_pattern: Some("**/*.txt".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: true,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(
|
||||
!result.contains("UPPERCASE"),
|
||||
"Case-sensitive search should not match uppercase"
|
||||
);
|
||||
|
||||
// Test case-sensitive search
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "LOWERCASE".to_string(),
|
||||
include_pattern: Some("**/*.txt".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: true,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
|
||||
assert!(
|
||||
!result.contains("lowercase"),
|
||||
"Case-sensitive search should match lowercase"
|
||||
);
|
||||
|
||||
// Test case-sensitive search for lowercase pattern
|
||||
let input = serde_json::to_value(GrepToolInput {
|
||||
regex: "lowercase".to_string(),
|
||||
include_pattern: Some("**/*.txt".to_string()),
|
||||
offset: 0,
|
||||
case_sensitive: true,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let result = run_grep_tool(input, project.clone(), cx).await;
|
||||
assert!(
|
||||
result.contains("lowercase"),
|
||||
"Case-sensitive search should match lowercase text"
|
||||
);
|
||||
}
|
||||
|
||||
async fn run_grep_tool(
|
||||
input: serde_json::Value,
|
||||
project: Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> String {
|
||||
let tool = Arc::new(GrepTool);
|
||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
||||
let task = cx.update(|cx| tool.run(input, &[], project, action_log, cx));
|
||||
|
||||
match task.output.await {
|
||||
Ok(result) => result,
|
||||
Err(e) => panic!("Failed to run grep tool: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
Searches the contents of files in the project with a regular expression
|
||||
|
||||
- Prefer this tool to path search when searching for symbols in the project, because you won't need to guess what path it's in.
|
||||
- Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.)
|
||||
- Pass an `include_pattern` if you know how to narrow your search on the files system
|
||||
- Never use this tool to search for paths. Only search file contents with this tool.
|
||||
- Use this tool when you need to find files containing specific patterns
|
||||
- Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||
@@ -1 +1 @@
|
||||
Lists files and directories in a given path. Prefer the `grep` or `path_search` tools when searching the codebase.
|
||||
Lists files and directories in a given path. Prefer the `regex_search` or `path_search` tools when searching the codebase.
|
||||
|
||||
@@ -2,6 +2,6 @@ Fast file pattern matching tool that works with any codebase size
|
||||
|
||||
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
||||
- Returns matching file paths sorted alphabetically
|
||||
- Prefer the `grep` tool to this tool when searching for symbols unless you have specific information about paths.
|
||||
- Prefer the `regex_search` tool to this tool when searching for symbols unless you have specific information about paths.
|
||||
- Use this tool when you need to find files by name patterns
|
||||
- Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
Reads the content of the given file in the project.
|
||||
|
||||
- Never attempt to read a path that hasn't been previously mentioned.
|
||||
If the file is too big to read all at once, and neither a start line
|
||||
nor an end line was specified, then this returns an outline of the
|
||||
file's symbols (with line numbers) instead of the file's contents,
|
||||
so that it can be called again with line ranges.
|
||||
|
||||
206
crates/assistant_tools/src/regex_search_tool.rs
Normal file
206
crates/assistant_tools/src/regex_search_tool.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::OffsetRangeExt;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{
|
||||
Project,
|
||||
search::{SearchQuery, SearchResult},
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, fmt::Write, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RegexSearchToolInput {
|
||||
/// A regex pattern to search for in the entire project. Note that the regex
|
||||
/// will be parsed by the Rust `regex` crate.
|
||||
pub regex: String,
|
||||
|
||||
/// Optional starting position for paginated results (0-based).
|
||||
/// When not provided, starts from the beginning.
|
||||
#[serde(default)]
|
||||
pub offset: u32,
|
||||
|
||||
/// Whether the regex is case-sensitive. Defaults to false (case-insensitive).
|
||||
#[serde(default)]
|
||||
pub case_sensitive: bool,
|
||||
}
|
||||
|
||||
impl RegexSearchToolInput {
|
||||
/// Which page of search results this is.
|
||||
pub fn page(&self) -> u32 {
|
||||
1 + (self.offset / RESULTS_PER_PAGE)
|
||||
}
|
||||
}
|
||||
|
||||
const RESULTS_PER_PAGE: u32 = 20;
|
||||
|
||||
pub struct RegexSearchTool;
|
||||
|
||||
impl Tool for RegexSearchTool {
|
||||
fn name(&self) -> String {
|
||||
"regex_search".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./regex_search_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Regex
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<RegexSearchToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<RegexSearchToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
let regex_str = MarkdownString::inline_code(&input.regex);
|
||||
let case_info = if input.case_sensitive {
|
||||
" (case-sensitive)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
if page > 1 {
|
||||
format!("Get page {page} of search results for regex {regex_str}{case_info}")
|
||||
} else {
|
||||
format!("Search files for regex {regex_str}{case_info}")
|
||||
}
|
||||
}
|
||||
Err(_) => "Search with regex".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
|
||||
let (offset, regex, case_sensitive) =
|
||||
match serde_json::from_value::<RegexSearchToolInput>(input) {
|
||||
Ok(input) => (input.offset, input.regex, input.case_sensitive),
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let query = match SearchQuery::regex(
|
||||
®ex,
|
||||
false,
|
||||
case_sensitive,
|
||||
false,
|
||||
false,
|
||||
PathMatcher::default(),
|
||||
PathMatcher::default(),
|
||||
None,
|
||||
) {
|
||||
Ok(query) => query,
|
||||
Err(error) => return Task::ready(Err(error)).into(),
|
||||
};
|
||||
|
||||
let results = project.update(cx, |project, cx| project.search(query, cx));
|
||||
|
||||
cx.spawn(async move|cx| {
|
||||
futures::pin_mut!(results);
|
||||
|
||||
let mut output = String::new();
|
||||
let mut skips_remaining = offset;
|
||||
let mut matches_found = 0;
|
||||
let mut has_more_matches = false;
|
||||
|
||||
while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
|
||||
if ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.read_with(cx, |buffer, cx| -> Result<(), anyhow::Error> {
|
||||
if let Some(path) = buffer.file().map(|file| file.full_path(cx)) {
|
||||
let mut file_header_written = false;
|
||||
let mut ranges = ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let mut point_range = range.to_point(buffer);
|
||||
point_range.start.row =
|
||||
point_range.start.row.saturating_sub(CONTEXT_LINES);
|
||||
point_range.start.column = 0;
|
||||
point_range.end.row = cmp::min(
|
||||
buffer.max_point().row,
|
||||
point_range.end.row + CONTEXT_LINES,
|
||||
);
|
||||
point_range.end.column = buffer.line_len(point_range.end.row);
|
||||
point_range
|
||||
})
|
||||
.peekable();
|
||||
|
||||
while let Some(mut range) = ranges.next() {
|
||||
if skips_remaining > 0 {
|
||||
skips_remaining -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// We'd already found a full page of matches, and we just found one more.
|
||||
if matches_found >= RESULTS_PER_PAGE {
|
||||
has_more_matches = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
while let Some(next_range) = ranges.peek() {
|
||||
if range.end.row >= next_range.start.row {
|
||||
range.end = next_range.end;
|
||||
ranges.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !file_header_written {
|
||||
writeln!(output, "\n## Matches in {}", path.display())?;
|
||||
file_header_written = true;
|
||||
}
|
||||
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
writeln!(output, "\n### Lines {start_line}-{end_line}\n```")?;
|
||||
output.extend(buffer.text_for_range(range));
|
||||
output.push_str("\n```\n");
|
||||
|
||||
matches_found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})??;
|
||||
}
|
||||
|
||||
if matches_found == 0 {
|
||||
Ok("No matches found".to_string())
|
||||
} else if has_more_matches {
|
||||
Ok(format!(
|
||||
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
|
||||
offset + 1,
|
||||
offset + matches_found,
|
||||
offset + RESULTS_PER_PAGE,
|
||||
))
|
||||
} else {
|
||||
Ok(format!("Found {matches_found} matches:\n{output}"))
|
||||
}
|
||||
}).into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
Searches the entire project for the given regular expression.
|
||||
|
||||
- Prefer this tool when searching for files containing symbols in the project.
|
||||
- Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.)
|
||||
- Use this tool when you need to find files containing specific patterns
|
||||
- Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages.
|
||||
@@ -14,7 +14,6 @@ pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> O
|
||||
true,
|
||||
PathMatcher::new(iter::empty::<&str>()).ok()?,
|
||||
PathMatcher::new(iter::empty::<&str>()).ok()?,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.log_err()?;
|
||||
@@ -59,8 +58,10 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
|
||||
|
||||
let max_row = buffer.max_point().row;
|
||||
|
||||
'windows: for start_row in 0..max_row + 1 {
|
||||
let end_row = start_row + old_lines.len().saturating_sub(1) as u32;
|
||||
'windows: for start_row in 0..max_row.saturating_sub(old_lines.len() as u32 - 1) {
|
||||
let mut common_leading = None;
|
||||
|
||||
let end_row = start_row + old_lines.len() as u32 - 1;
|
||||
|
||||
if end_row > max_row {
|
||||
// The buffer ends before fully matching the pattern
|
||||
@@ -75,14 +76,6 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
|
||||
let mut window_lines = window_text.lines();
|
||||
let mut old_lines_iter = old_lines.iter();
|
||||
|
||||
let mut common_mismatch = None;
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum Mismatch {
|
||||
OverIndented(String),
|
||||
UnderIndented(String),
|
||||
}
|
||||
|
||||
while let (Some(window_line), Some(old_line)) = (window_lines.next(), old_lines_iter.next())
|
||||
{
|
||||
let line_trimmed = window_line.trim_start();
|
||||
@@ -95,24 +88,18 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
|
||||
continue;
|
||||
}
|
||||
|
||||
let line_mismatch = if window_line.len() > old_line.len() {
|
||||
let prefix = window_line[..window_line.len() - old_line.len()].to_string();
|
||||
Mismatch::UnderIndented(prefix)
|
||||
} else {
|
||||
let prefix = old_line[..old_line.len() - window_line.len()].to_string();
|
||||
Mismatch::OverIndented(prefix)
|
||||
};
|
||||
let line_leading = &window_line[..window_line.len() - old_line.len()];
|
||||
|
||||
match &common_mismatch {
|
||||
Some(common_mismatch) if common_mismatch != &line_mismatch => {
|
||||
match &common_leading {
|
||||
Some(common_leading) if common_leading != line_leading => {
|
||||
continue 'windows;
|
||||
}
|
||||
Some(_) => (),
|
||||
None => common_mismatch = Some(line_mismatch),
|
||||
None => common_leading = Some(line_leading.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(common_mismatch) = &common_mismatch {
|
||||
if let Some(common_leading) = common_leading {
|
||||
let line_ending = buffer.line_ending();
|
||||
let replacement = new_lines
|
||||
.iter()
|
||||
@@ -120,13 +107,7 @@ pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapsho
|
||||
if new_line.trim().is_empty() {
|
||||
new_line.to_string()
|
||||
} else {
|
||||
match common_mismatch {
|
||||
Mismatch::UnderIndented(prefix) => prefix.to_string() + new_line,
|
||||
Mismatch::OverIndented(prefix) => new_line
|
||||
.strip_prefix(prefix)
|
||||
.unwrap_or(new_line)
|
||||
.to_string(),
|
||||
}
|
||||
common_leading.to_string() + new_line
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -168,123 +149,14 @@ fn lines_with_min_indent(input: &str) -> (Vec<&str>, usize) {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod replace_exact_tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::prelude::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn basic(cx: &mut TestAppContext) {
|
||||
let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
|
||||
assert_eq!(result, Some("let x = 42;".to_string()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn no_match(cx: &mut TestAppContext) {
|
||||
let result = test_replace_exact(cx, "let x = 41;", "let y = 42;", "let y = 43;").await;
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn multi_line(cx: &mut TestAppContext) {
|
||||
let whole = "fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}";
|
||||
let old_text = " let x = 41;\n println!(\"x = {}\", x);";
|
||||
let new_text = " let x = 42;\n println!(\"x = {}\", x);";
|
||||
let result = test_replace_exact(cx, whole, old_text, new_text).await;
|
||||
assert_eq!(
|
||||
result,
|
||||
Some("fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn multiple_occurrences(cx: &mut TestAppContext) {
|
||||
let whole = "let x = 41;\nlet y = 41;\nlet z = 41;";
|
||||
let result = test_replace_exact(cx, whole, "let x = 41;", "let x = 42;").await;
|
||||
assert_eq!(
|
||||
result,
|
||||
Some("let x = 42;\nlet y = 41;\nlet z = 41;".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn empty_buffer(cx: &mut TestAppContext) {
|
||||
let result = test_replace_exact(cx, "", "let x = 41;", "let x = 42;").await;
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn partial_match(cx: &mut TestAppContext) {
|
||||
let whole = "let x = 41; let y = 42;";
|
||||
let result = test_replace_exact(cx, whole, "let x = 41", "let x = 42").await;
|
||||
assert_eq!(result, Some("let x = 42; let y = 42;".to_string()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn whitespace_sensitive(cx: &mut TestAppContext) {
|
||||
let result = test_replace_exact(cx, "let x = 41;", " let x = 41;", "let x = 42;").await;
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn entire_buffer(cx: &mut TestAppContext) {
|
||||
let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
|
||||
assert_eq!(result, Some("let x = 42;".to_string()));
|
||||
}
|
||||
|
||||
async fn test_replace_exact(
|
||||
cx: &mut TestAppContext,
|
||||
whole: &str,
|
||||
old: &str,
|
||||
new: &str,
|
||||
) -> Option<String> {
|
||||
let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
|
||||
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact(old, new, &buffer_snapshot).await;
|
||||
diff.map(|diff| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod flexible_indent_tests {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::prelude::*;
|
||||
use unindent::Unindent;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_underindented_single_line(cx: &mut TestAppContext) {
|
||||
let cur = " let a = 41;".to_string();
|
||||
let old = " let a = 41;".to_string();
|
||||
let new = " let a = 42;".to_string();
|
||||
let exp = " let a = 42;".to_string();
|
||||
|
||||
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
|
||||
|
||||
assert_eq!(result, Some(exp.to_string()))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_overindented_single_line(cx: &mut TestAppContext) {
|
||||
let cur = " let a = 41;".to_string();
|
||||
let old = " let a = 41;".to_string();
|
||||
let new = " let a = 42;".to_string();
|
||||
let exp = " let a = 42;".to_string();
|
||||
|
||||
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
|
||||
|
||||
assert_eq!(result, Some(exp.to_string()))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_underindented_multi_line(cx: &mut TestAppContext) {
|
||||
fn test_replace_consistent_indentation(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
fn test() {
|
||||
let x = 5;
|
||||
@@ -321,33 +193,6 @@ mod flexible_indent_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_overindented_multi_line(cx: &mut TestAppContext) {
|
||||
let cur = r#"
|
||||
fn foo() {
|
||||
let a = 41;
|
||||
let b = 3.13;
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
// 6 space indent instead of 4
|
||||
let old = " let a = 41;\n let b = 3.13;";
|
||||
let new = " let a = 42;\n let b = 3.14;";
|
||||
|
||||
let expected = r#"
|
||||
fn foo() {
|
||||
let a = 42;
|
||||
let b = 3.14;
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
|
||||
|
||||
assert_eq!(result, Some(expected.to_string()))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_inconsistent_indentation(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
@@ -420,6 +265,7 @@ mod flexible_indent_tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_no_match(cx: &mut TestAppContext) {
|
||||
// Test with no match
|
||||
let whole = r#"
|
||||
fn test() {
|
||||
let x = 5;
|
||||
@@ -470,71 +316,6 @@ mod flexible_indent_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_whole_is_shorter_than_old(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
let x = 5;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = r#"
|
||||
let x = 5;
|
||||
let y = 10;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
let x = 5;
|
||||
let y = 20;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_old_is_empty(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
fn test() {
|
||||
let x = 5;
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = "";
|
||||
let new = r#"
|
||||
let y = 10;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_whole_is_empty(cx: &mut TestAppContext) {
|
||||
let whole = "";
|
||||
let old = r#"
|
||||
let x = 5;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
let x = 10;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lines_with_min_indent() {
|
||||
// Empty string
|
||||
@@ -722,133 +503,6 @@ mod flexible_indent_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_basic(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
assert_eq!(diff.edits.len(), 1);
|
||||
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
});
|
||||
|
||||
assert_eq!(result, "let x = 42;");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_no_match(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact("let y = 42;", "let y = 43;", &snapshot).await;
|
||||
assert!(diff.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_multi_line(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| {
|
||||
language::Buffer::local(
|
||||
"fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}",
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let old_text = " let x = 41;\n println!(\"x = {}\", x);";
|
||||
let new_text = " let x = 42;\n println!(\"x = {}\", x);";
|
||||
let diff = replace_exact(old_text, new_text, &snapshot).await;
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
"fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_multiple_occurrences(cx: &mut TestAppContext) {
|
||||
let buffer =
|
||||
cx.new(|cx| language::Buffer::local("let x = 41;\nlet y = 41;\nlet z = 41;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
// Should replace only the first occurrence
|
||||
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
});
|
||||
|
||||
assert_eq!(result, "let x = 42;\nlet y = 41;\nlet z = 41;");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_empty_buffer(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
|
||||
assert!(diff.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_partial_match(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("let x = 41; let y = 42;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
// Verify substring replacement actually works
|
||||
let diff = replace_exact("let x = 41", "let x = 42", &snapshot).await;
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
});
|
||||
|
||||
assert_eq!(result, "let x = 42; let y = 42;");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_whitespace_sensitive(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact(" let x = 41;", "let x = 42;", &snapshot).await;
|
||||
assert!(diff.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_entire_buffer(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
});
|
||||
|
||||
assert_eq!(result, "let x = 42;");
|
||||
}
|
||||
|
||||
fn test_replace_with_flexible_indent(
|
||||
cx: &mut TestAppContext,
|
||||
whole: &str,
|
||||
|
||||
@@ -84,10 +84,6 @@ pub enum Model {
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn default_fast() -> Self {
|
||||
Self::Claude3_5Haiku
|
||||
}
|
||||
|
||||
pub fn from_id(id: &str) -> anyhow::Result<Self> {
|
||||
if id.starts_with("claude-3-5-sonnet-v2") {
|
||||
Ok(Self::Claude3_5SonnetV2)
|
||||
|
||||
@@ -128,7 +128,6 @@ serde_json.workspace = true
|
||||
session = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
sqlx = { version = "0.8", features = ["sqlite"] }
|
||||
task.workspace = true
|
||||
theme.workspace = true
|
||||
unindent.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -449,29 +449,6 @@ async fn manage_billing_subscription(
|
||||
let stripe_subscription =
|
||||
Subscription::retrieve(&stripe_client, &subscription_id, &[]).await?;
|
||||
|
||||
let is_on_zed_pro_trial = stripe_subscription.status == SubscriptionStatus::Trialing
|
||||
&& stripe_subscription.items.data.iter().any(|item| {
|
||||
item.price
|
||||
.as_ref()
|
||||
.map_or(false, |price| price.id == zed_pro_price_id)
|
||||
});
|
||||
if is_on_zed_pro_trial {
|
||||
// If the user is already on a Zed Pro trial and wants to upgrade to Pro, we just need to end their trial early.
|
||||
Subscription::update(
|
||||
&stripe_client,
|
||||
&stripe_subscription.id,
|
||||
stripe::UpdateSubscription {
|
||||
trial_end: Some(stripe::Scheduled::now()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(Json(ManageBillingSubscriptionResponse {
|
||||
billing_portal_session_url: None,
|
||||
}));
|
||||
}
|
||||
|
||||
let subscription_item_to_update = stripe_subscription
|
||||
.items
|
||||
.data
|
||||
@@ -479,7 +456,10 @@ async fn manage_billing_subscription(
|
||||
.find_map(|item| {
|
||||
let price = item.price.as_ref()?;
|
||||
|
||||
if price.id == zed_free_price_id {
|
||||
if price.id == zed_free_price_id
|
||||
|| (price.id == zed_pro_price_id
|
||||
&& stripe_subscription.status == SubscriptionStatus::Trialing)
|
||||
{
|
||||
Some(item.id.clone())
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use call::ActiveCall;
|
||||
use dap::DebugRequestType;
|
||||
use dap::requests::{Initialize, Launch, StackTrace};
|
||||
use dap::{SourceBreakpoint, requests::SetBreakpoints};
|
||||
use dap::DebugRequestType;
|
||||
use dap::{requests::SetBreakpoints, SourceBreakpoint};
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use debugger_ui::session::DebugSession;
|
||||
use editor::Editor;
|
||||
@@ -13,7 +13,7 @@ use std::{
|
||||
path::Path,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use workspace::{Workspace, dock::Panel};
|
||||
use workspace::{dock::Panel, Workspace};
|
||||
|
||||
use super::{TestClient, TestServer};
|
||||
|
||||
|
||||
@@ -5091,7 +5091,6 @@ async fn test_project_search(
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
|
||||
@@ -882,7 +882,6 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use crate::tests::TestServer;
|
||||
use call::ActiveCall;
|
||||
use collections::{HashMap, HashSet};
|
||||
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use dap::DapRegistry;
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{
|
||||
AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
|
||||
VisualContext,
|
||||
};
|
||||
use http_client::BlockedHttpClient;
|
||||
use language::{
|
||||
@@ -26,7 +24,6 @@ use project::{
|
||||
};
|
||||
use remote::SshRemoteClient;
|
||||
use remote_server::{HeadlessAppState, HeadlessProject};
|
||||
use rpc::proto;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{path::Path, sync::Arc};
|
||||
@@ -89,6 +86,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
debug_adapters: Arc::new(DapRegistry::fake()),
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
@@ -256,6 +254,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
debug_adapters: Arc::new(DapRegistry::fake()),
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
@@ -461,6 +460,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
http_client: remote_http_client,
|
||||
node_runtime: NodeRuntime::unavailable(),
|
||||
languages,
|
||||
debug_adapters: Arc::new(DapRegistry::fake()),
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
@@ -579,108 +579,3 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
"Prettier formatting was not applied to client buffer after host's request"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_remote_server_debugger(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
command_palette_hooks::init(cx);
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::try_init().ok();
|
||||
}
|
||||
});
|
||||
server_cx.update(|cx| {
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
});
|
||||
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
path!("/code"),
|
||||
json!({
|
||||
"lib.rs": "fn one() -> usize { 1 }"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// User A connects to the remote project via SSH.
|
||||
server_cx.update(HeadlessProject::init);
|
||||
let remote_http_client = Arc::new(BlockedHttpClient);
|
||||
let node = NodeRuntime::unavailable();
|
||||
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
||||
let _headless_project = server_cx.new(|cx| {
|
||||
client::init_settings(cx);
|
||||
HeadlessProject::new(
|
||||
HeadlessAppState {
|
||||
session: server_ssh,
|
||||
fs: remote_fs.clone(),
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
|
||||
let mut server = TestServer::start(server_cx.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
cx_a.update(|cx| {
|
||||
debugger_ui::init(cx);
|
||||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
|
||||
let debugger_panel = workspace
|
||||
.update_in(cx_a, |_workspace, window, cx| {
|
||||
cx.spawn_in(window, DebugPanel::load)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
workspace.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.add_panel(debugger_panel, window, cx);
|
||||
});
|
||||
|
||||
cx_a.run_until_parked();
|
||||
let debug_panel = workspace
|
||||
.update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
||||
.unwrap();
|
||||
|
||||
let workspace_window = cx_a
|
||||
.window_handle()
|
||||
.downcast::<workspace::Workspace>()
|
||||
.unwrap();
|
||||
|
||||
let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
|
||||
cx_a.run_until_parked();
|
||||
debug_panel.update(cx_a, |debug_panel, cx| {
|
||||
assert_eq!(
|
||||
debug_panel.active_session().unwrap().read(cx).session(cx),
|
||||
session
|
||||
)
|
||||
});
|
||||
|
||||
session.update(cx_a, |session, _| {
|
||||
assert_eq!(session.binary().command, "ssh");
|
||||
});
|
||||
|
||||
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(session.read(cx).session_id(), cx)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
client_ssh.update(cx_a, |a, _| {
|
||||
a.shutdown_processes(Some(proto::ShutdownRemoteServer {}))
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use client::{
|
||||
use clock::FakeSystemClock;
|
||||
use collab_ui::channel_view::ChannelView;
|
||||
use collections::{HashMap, HashSet};
|
||||
|
||||
use dap::DapRegistry;
|
||||
use fs::FakeFs;
|
||||
use futures::{StreamExt as _, channel::oneshot};
|
||||
use git::GitHostingProviderRegistry;
|
||||
@@ -275,12 +275,14 @@ impl TestServer {
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let debug_adapters = Arc::new(DapRegistry::default());
|
||||
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
|
||||
let app_state = Arc::new(workspace::AppState {
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
workspace_store,
|
||||
languages: language_registry,
|
||||
debug_adapters,
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _| Default::default(),
|
||||
node_runtime: NodeRuntime::unavailable(),
|
||||
@@ -796,6 +798,7 @@ impl TestClient {
|
||||
self.app_state.node_runtime.clone(),
|
||||
self.app_state.user_store.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.debug_adapters.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
None,
|
||||
cx,
|
||||
|
||||
@@ -37,7 +37,6 @@ static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
|
||||
false,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
|
||||
@@ -166,9 +166,6 @@ impl ComponentPreview {
|
||||
|
||||
component_preview.update_component_list(cx);
|
||||
|
||||
let focus_handle = component_preview.filter_editor.read(cx).focus_handle(cx);
|
||||
window.focus(&focus_handle);
|
||||
|
||||
component_preview
|
||||
}
|
||||
|
||||
@@ -782,13 +779,10 @@ impl Item for ComponentPreview {
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
self.workspace_id = workspace.database_id();
|
||||
|
||||
let focus_handle = self.filter_editor.read(cx).focus_handle(cx);
|
||||
window.focus(&focus_handle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,6 @@ pub enum Model {
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn default_fast() -> Self {
|
||||
Self::Claude3_7Sonnet
|
||||
}
|
||||
|
||||
pub fn uses_streaming(&self) -> bool {
|
||||
match self {
|
||||
Self::Gpt4o
|
||||
|
||||
@@ -39,7 +39,6 @@ log.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
proto.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -3,8 +3,7 @@ use anyhow::{Context as _, Result, anyhow};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
|
||||
use dap_types::StartDebuggingRequestArguments;
|
||||
use futures::io::BufReader;
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
pub use http_client::{HttpClient, github::latest_github_release};
|
||||
@@ -14,10 +13,16 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::WorktreeId;
|
||||
use smol::{self, fs::File, lock::Mutex};
|
||||
use std::{
|
||||
borrow::Borrow, collections::HashSet, ffi::OsStr, fmt::Debug, net::Ipv4Addr, ops::Deref,
|
||||
path::PathBuf, sync::Arc,
|
||||
borrow::Borrow,
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::{OsStr, OsString},
|
||||
fmt::Debug,
|
||||
net::Ipv4Addr,
|
||||
ops::Deref,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{DebugTaskDefinition, TcpArgumentsTemplate};
|
||||
use task::DebugTaskDefinition;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -88,91 +93,17 @@ pub struct TcpArguments {
|
||||
pub port: u16,
|
||||
pub timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl TcpArguments {
|
||||
pub fn from_proto(proto: proto::TcpHost) -> anyhow::Result<Self> {
|
||||
let host = TcpArgumentsTemplate::from_proto(proto)?;
|
||||
Ok(TcpArguments {
|
||||
host: host.host.ok_or_else(|| anyhow!("missing host"))?,
|
||||
port: host.port.ok_or_else(|| anyhow!("missing port"))?,
|
||||
timeout: host.timeout,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::TcpHost {
|
||||
TcpArgumentsTemplate {
|
||||
host: Some(self.host),
|
||||
port: Some(self.port),
|
||||
timeout: self.timeout,
|
||||
}
|
||||
.to_proto()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DebugAdapterBinary {
|
||||
pub adapter_name: DebugAdapterName,
|
||||
pub command: String,
|
||||
pub arguments: Vec<String>,
|
||||
pub envs: HashMap<String, String>,
|
||||
pub arguments: Option<Vec<OsString>>,
|
||||
pub envs: Option<HashMap<String, String>>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub connection: Option<TcpArguments>,
|
||||
pub request_args: StartDebuggingRequestArguments,
|
||||
}
|
||||
|
||||
impl DebugAdapterBinary {
|
||||
pub fn from_proto(binary: proto::DebugAdapterBinary) -> anyhow::Result<Self> {
|
||||
let request = match binary.launch_type() {
|
||||
proto::debug_adapter_binary::LaunchType::Launch => {
|
||||
StartDebuggingRequestArgumentsRequest::Launch
|
||||
}
|
||||
proto::debug_adapter_binary::LaunchType::Attach => {
|
||||
StartDebuggingRequestArgumentsRequest::Attach
|
||||
}
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: binary.command,
|
||||
arguments: binary.arguments,
|
||||
envs: binary.envs.into_iter().collect(),
|
||||
connection: binary
|
||||
.connection
|
||||
.map(TcpArguments::from_proto)
|
||||
.transpose()?,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: serde_json::from_str(&binary.configuration)?,
|
||||
request,
|
||||
},
|
||||
cwd: binary.cwd.map(|cwd| cwd.into()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::DebugAdapterBinary {
|
||||
proto::DebugAdapterBinary {
|
||||
command: self.command.clone(),
|
||||
arguments: self.arguments.clone(),
|
||||
envs: self
|
||||
.envs
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
cwd: self
|
||||
.cwd
|
||||
.as_ref()
|
||||
.map(|cwd| cwd.to_string_lossy().to_string()),
|
||||
connection: self.connection.as_ref().map(|c| c.to_proto()),
|
||||
launch_type: match self.request_args.request {
|
||||
StartDebuggingRequestArgumentsRequest::Launch => {
|
||||
proto::debug_adapter_binary::LaunchType::Launch.into()
|
||||
}
|
||||
StartDebuggingRequestArgumentsRequest::Attach => {
|
||||
proto::debug_adapter_binary::LaunchType::Attach.into()
|
||||
}
|
||||
},
|
||||
configuration: self.request_args.configuration.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AdapterVersion {
|
||||
pub tag_name: String,
|
||||
@@ -387,22 +318,22 @@ impl FakeAdapter {
|
||||
|
||||
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
|
||||
use serde_json::json;
|
||||
use task::DebugRequest;
|
||||
use task::DebugRequestType;
|
||||
|
||||
let value = json!({
|
||||
"request": match config.request {
|
||||
DebugRequest::Launch(_) => "launch",
|
||||
DebugRequest::Attach(_) => "attach",
|
||||
DebugRequestType::Launch(_) => "launch",
|
||||
DebugRequestType::Attach(_) => "attach",
|
||||
},
|
||||
"process_id": if let DebugRequest::Attach(attach_config) = &config.request {
|
||||
"process_id": if let DebugRequestType::Attach(attach_config) = &config.request {
|
||||
attach_config.process_id
|
||||
} else {
|
||||
None
|
||||
},
|
||||
});
|
||||
let request = match config.request {
|
||||
DebugRequest::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
|
||||
DebugRequest::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
|
||||
DebugRequestType::Launch(_) => dap_types::StartDebuggingRequestArgumentsRequest::Launch,
|
||||
DebugRequestType::Attach(_) => dap_types::StartDebuggingRequestArgumentsRequest::Attach,
|
||||
};
|
||||
StartDebuggingRequestArguments {
|
||||
configuration: value,
|
||||
@@ -426,10 +357,11 @@ impl DebugAdapter for FakeAdapter {
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
Ok(DebugAdapterBinary {
|
||||
adapter_name: Self::ADAPTER_NAME.into(),
|
||||
command: "command".into(),
|
||||
arguments: vec![],
|
||||
arguments: None,
|
||||
connection: None,
|
||||
envs: HashMap::default(),
|
||||
envs: None,
|
||||
cwd: None,
|
||||
request_args: self.request_args(config),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
adapters::DebugAdapterBinary,
|
||||
adapters::{DebugAdapterBinary, DebugAdapterName},
|
||||
transport::{IoKind, LogKind, TransportDelegate},
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -88,6 +88,7 @@ impl DebugAdapterClient {
|
||||
) -> Result<Self> {
|
||||
let binary = match self.transport_delegate.transport() {
|
||||
crate::transport::Transport::Tcp(tcp_transport) => DebugAdapterBinary {
|
||||
adapter_name: binary.adapter_name,
|
||||
command: binary.command,
|
||||
arguments: binary.arguments,
|
||||
envs: binary.envs,
|
||||
@@ -218,6 +219,9 @@ impl DebugAdapterClient {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> DebugAdapterName {
|
||||
self.binary.adapter_name.clone()
|
||||
}
|
||||
pub fn binary(&self) -> &DebugAdapterBinary {
|
||||
&self.binary
|
||||
}
|
||||
@@ -318,6 +322,7 @@ mod tests {
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
adapter_name: "adapter".into(),
|
||||
command: "command".into(),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
@@ -388,6 +393,7 @@ mod tests {
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
adapter_name: "adapter".into(),
|
||||
command: "command".into(),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
@@ -441,6 +447,7 @@ mod tests {
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
adapter_name: "test-adapter".into(),
|
||||
command: "command".into(),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
|
||||
@@ -7,7 +7,7 @@ pub mod transport;
|
||||
|
||||
pub use dap_types::*;
|
||||
pub use registry::DapRegistry;
|
||||
pub use task::DebugRequest;
|
||||
pub use task::DebugRequestType;
|
||||
|
||||
pub type ScopeId = u64;
|
||||
pub type VariableReference = u64;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use gpui::{App, Global};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::adapters::{DebugAdapter, DebugAdapterName};
|
||||
@@ -12,20 +11,8 @@ struct DapRegistryState {
|
||||
#[derive(Clone, Default)]
|
||||
/// Stores available debug adapters.
|
||||
pub struct DapRegistry(Arc<RwLock<DapRegistryState>>);
|
||||
impl Global for DapRegistry {}
|
||||
|
||||
impl DapRegistry {
|
||||
pub fn global(cx: &mut App) -> &mut Self {
|
||||
let ret = cx.default_global::<Self>();
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if ret.adapter(crate::FakeAdapter::ADAPTER_NAME).is_none() {
|
||||
ret.add_adapter(Arc::new(crate::FakeAdapter::new()));
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn add_adapter(&self, adapter: Arc<dyn DebugAdapter>) {
|
||||
let name = adapter.name();
|
||||
let _previous_value = self.0.write().adapters.insert(name, adapter);
|
||||
@@ -34,12 +21,19 @@ impl DapRegistry {
|
||||
"Attempted to insert a new debug adapter when one is already registered"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn adapter(&self, name: &str) -> Option<Arc<dyn DebugAdapter>> {
|
||||
self.0.read().adapters.get(name).cloned()
|
||||
}
|
||||
|
||||
pub fn enumerate_adapters(&self) -> Vec<DebugAdapterName> {
|
||||
self.0.read().adapters.keys().cloned().collect()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake() -> Self {
|
||||
use crate::FakeAdapter;
|
||||
|
||||
let register = Self::default();
|
||||
register.add_adapter(Arc::new(FakeAdapter::new()));
|
||||
register
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::{ResultExt as _, TryFutureExt};
|
||||
use task::TCPHost;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
|
||||
|
||||
@@ -74,14 +74,16 @@ pub enum Transport {
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
async fn start(_: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if cfg!(any(test, feature = "test-support")) {
|
||||
return FakeTransport::start(cx)
|
||||
.await
|
||||
.map(|(transports, fake)| (transports, Self::Fake(fake)));
|
||||
}
|
||||
return FakeTransport::start(cx)
|
||||
.await
|
||||
.map(|(transports, fake)| (transports, Self::Fake(fake)));
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
|
||||
if binary.connection.is_some() {
|
||||
TcpTransport::start(binary, cx)
|
||||
.await
|
||||
@@ -126,7 +128,6 @@ pub(crate) struct TransportDelegate {
|
||||
pending_requests: Requests,
|
||||
transport: Transport,
|
||||
server_tx: Arc<Mutex<Option<Sender<Message>>>>,
|
||||
_tasks: Vec<gpui::Task<Option<()>>>,
|
||||
}
|
||||
|
||||
impl TransportDelegate {
|
||||
@@ -141,7 +142,6 @@ impl TransportDelegate {
|
||||
log_handlers: Default::default(),
|
||||
current_requests: Default::default(),
|
||||
pending_requests: Default::default(),
|
||||
_tasks: Default::default(),
|
||||
};
|
||||
let messages = this.start_handlers(transport_pipes, cx).await?;
|
||||
Ok((messages, this))
|
||||
@@ -168,43 +168,35 @@ impl TransportDelegate {
|
||||
|
||||
cx.update(|cx| {
|
||||
if let Some(stdout) = params.stdout.take() {
|
||||
self._tasks.push(
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_adapter_log(stdout, log_handler.clone()).log_err()),
|
||||
);
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_adapter_log(stdout, log_handler.clone()))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
self._tasks.push(
|
||||
cx.background_executor().spawn(
|
||||
Self::handle_output(
|
||||
params.output,
|
||||
client_tx,
|
||||
self.pending_requests.clone(),
|
||||
log_handler.clone(),
|
||||
)
|
||||
.log_err(),
|
||||
),
|
||||
);
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_output(
|
||||
params.output,
|
||||
client_tx,
|
||||
self.pending_requests.clone(),
|
||||
log_handler.clone(),
|
||||
))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
if let Some(stderr) = params.stderr.take() {
|
||||
self._tasks.push(
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_error(stderr, self.log_handlers.clone()).log_err()),
|
||||
);
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_error(stderr, self.log_handlers.clone()))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
self._tasks.push(
|
||||
cx.background_executor().spawn(
|
||||
Self::handle_input(
|
||||
params.input,
|
||||
client_rx,
|
||||
self.current_requests.clone(),
|
||||
self.pending_requests.clone(),
|
||||
log_handler.clone(),
|
||||
)
|
||||
.log_err(),
|
||||
),
|
||||
);
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_input(
|
||||
params.input,
|
||||
client_rx,
|
||||
self.current_requests.clone(),
|
||||
self.pending_requests.clone(),
|
||||
log_handler.clone(),
|
||||
))
|
||||
.detach_and_log_err(cx);
|
||||
})?;
|
||||
|
||||
{
|
||||
@@ -377,7 +369,6 @@ impl TransportDelegate {
|
||||
where
|
||||
Stderr: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
log::debug!("Handle error started");
|
||||
let mut buffer = String::new();
|
||||
|
||||
let mut reader = BufReader::new(stderr);
|
||||
@@ -529,21 +520,18 @@ pub struct TcpTransport {
|
||||
|
||||
impl TcpTransport {
|
||||
/// Get an open port to use with the tcp client when not supplied by debug config
|
||||
pub async fn port(host: &TcpArgumentsTemplate) -> Result<u16> {
|
||||
pub async fn port(host: &TCPHost) -> Result<u16> {
|
||||
if let Some(port) = host.port {
|
||||
Ok(port)
|
||||
} else {
|
||||
Self::unused_port(host.host()).await
|
||||
Ok(TcpListener::bind(SocketAddrV4::new(host.host(), 0))
|
||||
.await?
|
||||
.local_addr()?
|
||||
.port())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn unused_port(host: Ipv4Addr) -> Result<u16> {
|
||||
Ok(TcpListener::bind(SocketAddrV4::new(host, 0))
|
||||
.await?
|
||||
.local_addr()?
|
||||
.port())
|
||||
}
|
||||
|
||||
#[allow(dead_code, reason = "This is used in non test builds of Zed")]
|
||||
async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
|
||||
let Some(connection_args) = binary.connection.as_ref() else {
|
||||
return Err(anyhow!("No connection arguments provided"));
|
||||
@@ -558,8 +546,13 @@ impl TcpTransport {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
if let Some(args) = &binary.arguments {
|
||||
command.args(args);
|
||||
}
|
||||
|
||||
if let Some(envs) = &binary.envs {
|
||||
command.envs(envs);
|
||||
}
|
||||
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
@@ -642,8 +635,13 @@ impl StdioTransport {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
if let Some(args) = &binary.arguments {
|
||||
command.args(args);
|
||||
}
|
||||
|
||||
if let Some(envs) = &binary.envs {
|
||||
command.envs(envs);
|
||||
}
|
||||
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use dap::adapters::latest_github_release;
|
||||
use gpui::AsyncApp;
|
||||
use task::{DebugRequest, DebugTaskDefinition};
|
||||
use task::{DebugRequestType, DebugTaskDefinition};
|
||||
|
||||
use crate::*;
|
||||
|
||||
@@ -19,8 +19,8 @@ impl CodeLldbDebugAdapter {
|
||||
fn request_args(&self, config: &DebugTaskDefinition) -> dap::StartDebuggingRequestArguments {
|
||||
let mut configuration = json!({
|
||||
"request": match config.request {
|
||||
DebugRequest::Launch(_) => "launch",
|
||||
DebugRequest::Attach(_) => "attach",
|
||||
DebugRequestType::Launch(_) => "launch",
|
||||
DebugRequestType::Attach(_) => "attach",
|
||||
},
|
||||
});
|
||||
let map = configuration.as_object_mut().unwrap();
|
||||
@@ -28,10 +28,10 @@ impl CodeLldbDebugAdapter {
|
||||
map.insert("name".into(), Value::String(config.label.clone()));
|
||||
let request = config.request.to_dap();
|
||||
match &config.request {
|
||||
DebugRequest::Attach(attach) => {
|
||||
DebugRequestType::Attach(attach) => {
|
||||
map.insert("pid".into(), attach.process_id.into());
|
||||
}
|
||||
DebugRequest::Launch(launch) => {
|
||||
DebugRequestType::Launch(launch) => {
|
||||
map.insert("program".into(), launch.program.clone().into());
|
||||
|
||||
if !launch.args.is_empty() {
|
||||
@@ -140,13 +140,16 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
.ok_or_else(|| anyhow!("Adapter path is expected to be valid UTF-8"))?;
|
||||
Ok(DebugAdapterBinary {
|
||||
command,
|
||||
cwd: None,
|
||||
arguments: vec![
|
||||
cwd: Some(adapter_dir),
|
||||
arguments: Some(vec![
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
|
||||
],
|
||||
json!({"sourceLanguages": ["cpp", "rust"]})
|
||||
.to_string()
|
||||
.into(),
|
||||
]),
|
||||
request_args: self.request_args(config),
|
||||
envs: HashMap::default(),
|
||||
adapter_name: "test".into(),
|
||||
envs: None,
|
||||
connection: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use codelldb::CodeLldbDebugAdapter;
|
||||
use dap::{
|
||||
DapRegistry, DebugRequest,
|
||||
DapRegistry, DebugRequestType,
|
||||
adapters::{
|
||||
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
|
||||
GithubRepo,
|
||||
@@ -19,26 +19,23 @@ use dap::{
|
||||
};
|
||||
use gdb::GdbDebugAdapter;
|
||||
use go::GoDebugAdapter;
|
||||
use gpui::{App, BorrowAppContext};
|
||||
use javascript::JsDebugAdapter;
|
||||
use php::PhpDebugAdapter;
|
||||
use python::PythonDebugAdapter;
|
||||
use serde_json::{Value, json};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use task::TCPHost;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
|
||||
registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(PythonDebugAdapter));
|
||||
registry.add_adapter(Arc::from(PhpDebugAdapter));
|
||||
registry.add_adapter(Arc::from(JsDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GdbDebugAdapter));
|
||||
})
|
||||
pub fn init(registry: Arc<DapRegistry>) {
|
||||
registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(PythonDebugAdapter));
|
||||
registry.add_adapter(Arc::from(PhpDebugAdapter));
|
||||
registry.add_adapter(Arc::from(JsDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GdbDebugAdapter));
|
||||
}
|
||||
|
||||
pub(crate) async fn configure_tcp_connection(
|
||||
tcp_connection: TcpArgumentsTemplate,
|
||||
tcp_connection: TCPHost,
|
||||
) -> Result<(Ipv4Addr, u16, Option<u64>)> {
|
||||
let host = tcp_connection.host();
|
||||
let timeout = tcp_connection.timeout;
|
||||
@@ -56,7 +53,7 @@ trait ToDap {
|
||||
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest;
|
||||
}
|
||||
|
||||
impl ToDap for DebugRequest {
|
||||
impl ToDap for DebugRequestType {
|
||||
fn to_dap(&self) -> dap::StartDebuggingRequestArgumentsRequest {
|
||||
match self {
|
||||
Self::Launch(_) => dap::StartDebuggingRequestArgumentsRequest::Launch,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::{collections::HashMap, ffi::OsStr};
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use dap::StartDebuggingRequestArguments;
|
||||
use gpui::AsyncApp;
|
||||
use task::{DebugRequest, DebugTaskDefinition};
|
||||
use task::{DebugRequestType, DebugTaskDefinition};
|
||||
|
||||
use crate::*;
|
||||
|
||||
@@ -17,18 +17,18 @@ impl GdbDebugAdapter {
|
||||
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
|
||||
let mut args = json!({
|
||||
"request": match config.request {
|
||||
DebugRequest::Launch(_) => "launch",
|
||||
DebugRequest::Attach(_) => "attach",
|
||||
DebugRequestType::Launch(_) => "launch",
|
||||
DebugRequestType::Attach(_) => "attach",
|
||||
},
|
||||
});
|
||||
|
||||
let map = args.as_object_mut().unwrap();
|
||||
match &config.request {
|
||||
DebugRequest::Attach(attach) => {
|
||||
DebugRequestType::Attach(attach) => {
|
||||
map.insert("pid".into(), attach.process_id.into());
|
||||
}
|
||||
|
||||
DebugRequest::Launch(launch) => {
|
||||
DebugRequestType::Launch(launch) => {
|
||||
map.insert("program".into(), launch.program.clone().into());
|
||||
|
||||
if !launch.args.is_empty() {
|
||||
@@ -82,9 +82,10 @@ impl DebugAdapter for GdbDebugAdapter {
|
||||
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
adapter_name: Self::ADAPTER_NAME.into(),
|
||||
command: gdb_path,
|
||||
arguments: vec!["-i=dap".into()],
|
||||
envs: HashMap::default(),
|
||||
arguments: Some(vec!["-i=dap".into()]),
|
||||
envs: None,
|
||||
cwd: None,
|
||||
connection: None,
|
||||
request_args: self.request_args(config),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use dap::StartDebuggingRequestArguments;
|
||||
use gpui::AsyncApp;
|
||||
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
|
||||
use std::{ffi::OsStr, path::PathBuf};
|
||||
use task::DebugTaskDefinition;
|
||||
|
||||
use crate::*;
|
||||
@@ -12,12 +12,12 @@ impl GoDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "Delve";
|
||||
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
|
||||
let mut args = match &config.request {
|
||||
dap::DebugRequest::Attach(attach_config) => {
|
||||
dap::DebugRequestType::Attach(attach_config) => {
|
||||
json!({
|
||||
"processId": attach_config.process_id,
|
||||
})
|
||||
}
|
||||
dap::DebugRequest::Launch(launch_config) => json!({
|
||||
dap::DebugRequestType::Launch(launch_config) => json!({
|
||||
"program": launch_config.program,
|
||||
"cwd": launch_config.cwd,
|
||||
"args": launch_config.args
|
||||
@@ -92,14 +92,15 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
adapter_name: self.name(),
|
||||
command: delve_path,
|
||||
arguments: vec![
|
||||
arguments: Some(vec![
|
||||
"dap".into(),
|
||||
"--listen".into(),
|
||||
format!("{}:{}", host, port),
|
||||
],
|
||||
format!("{}:{}", host, port).into(),
|
||||
]),
|
||||
cwd: None,
|
||||
envs: HashMap::default(),
|
||||
envs: None,
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use adapters::latest_github_release;
|
||||
use dap::StartDebuggingRequestArguments;
|
||||
use gpui::AsyncApp;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use task::{DebugRequest, DebugTaskDefinition};
|
||||
use std::path::PathBuf;
|
||||
use task::{DebugRequestType, DebugTaskDefinition};
|
||||
|
||||
use crate::*;
|
||||
|
||||
@@ -18,16 +18,16 @@ impl JsDebugAdapter {
|
||||
let mut args = json!({
|
||||
"type": "pwa-node",
|
||||
"request": match config.request {
|
||||
DebugRequest::Launch(_) => "launch",
|
||||
DebugRequest::Attach(_) => "attach",
|
||||
DebugRequestType::Launch(_) => "launch",
|
||||
DebugRequestType::Attach(_) => "attach",
|
||||
},
|
||||
});
|
||||
let map = args.as_object_mut().unwrap();
|
||||
match &config.request {
|
||||
DebugRequest::Attach(attach) => {
|
||||
DebugRequestType::Attach(attach) => {
|
||||
map.insert("processId".into(), attach.process_id.into());
|
||||
}
|
||||
DebugRequest::Launch(launch) => {
|
||||
DebugRequestType::Launch(launch) => {
|
||||
map.insert("program".into(), launch.program.clone().into());
|
||||
|
||||
if !launch.args.is_empty() {
|
||||
@@ -106,22 +106,20 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
adapter_name: self.name(),
|
||||
command: delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
arguments: vec![
|
||||
adapter_path
|
||||
.join(Self::ADAPTER_PATH)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
port.to_string(),
|
||||
host.to_string(),
|
||||
],
|
||||
arguments: Some(vec![
|
||||
adapter_path.join(Self::ADAPTER_PATH).into(),
|
||||
port.to_string().into(),
|
||||
host.to_string().into(),
|
||||
]),
|
||||
cwd: None,
|
||||
envs: HashMap::default(),
|
||||
envs: None,
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use adapters::latest_github_release;
|
||||
use dap::adapters::TcpArguments;
|
||||
use gpui::AsyncApp;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use task::DebugTaskDefinition;
|
||||
|
||||
use crate::*;
|
||||
@@ -19,18 +19,20 @@ impl PhpDebugAdapter {
|
||||
config: &DebugTaskDefinition,
|
||||
) -> Result<dap::StartDebuggingRequestArguments> {
|
||||
match &config.request {
|
||||
dap::DebugRequest::Attach(_) => {
|
||||
dap::DebugRequestType::Attach(_) => {
|
||||
anyhow::bail!("php adapter does not support attaching")
|
||||
}
|
||||
dap::DebugRequest::Launch(launch_config) => Ok(dap::StartDebuggingRequestArguments {
|
||||
configuration: json!({
|
||||
"program": launch_config.program,
|
||||
"cwd": launch_config.cwd,
|
||||
"args": launch_config.args,
|
||||
"stopOnEntry": config.stop_on_entry.unwrap_or_default(),
|
||||
}),
|
||||
request: config.request.to_dap(),
|
||||
}),
|
||||
dap::DebugRequestType::Launch(launch_config) => {
|
||||
Ok(dap::StartDebuggingRequestArguments {
|
||||
configuration: json!({
|
||||
"program": launch_config.program,
|
||||
"cwd": launch_config.cwd,
|
||||
"args": launch_config.args,
|
||||
"stopOnEntry": config.stop_on_entry.unwrap_or_default(),
|
||||
}),
|
||||
request: config.request.to_dap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,26 +94,24 @@ impl DebugAdapter for PhpDebugAdapter {
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
adapter_name: self.name(),
|
||||
command: delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
arguments: vec![
|
||||
adapter_path
|
||||
.join(Self::ADAPTER_PATH)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
format!("--server={}", port),
|
||||
],
|
||||
arguments: Some(vec![
|
||||
adapter_path.join(Self::ADAPTER_PATH).into(),
|
||||
format!("--server={}", port).into(),
|
||||
]),
|
||||
connection: Some(TcpArguments {
|
||||
port,
|
||||
host,
|
||||
timeout,
|
||||
}),
|
||||
cwd: None,
|
||||
envs: HashMap::default(),
|
||||
envs: None,
|
||||
request_args: self.request_args(config)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::*;
|
||||
use dap::{DebugRequest, StartDebuggingRequestArguments};
|
||||
use dap::{DebugRequestType, StartDebuggingRequestArguments};
|
||||
use gpui::AsyncApp;
|
||||
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
|
||||
use std::{ffi::OsStr, path::PathBuf};
|
||||
use task::DebugTaskDefinition;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -16,18 +16,18 @@ impl PythonDebugAdapter {
|
||||
fn request_args(&self, config: &DebugTaskDefinition) -> StartDebuggingRequestArguments {
|
||||
let mut args = json!({
|
||||
"request": match config.request {
|
||||
DebugRequest::Launch(_) => "launch",
|
||||
DebugRequest::Attach(_) => "attach",
|
||||
DebugRequestType::Launch(_) => "launch",
|
||||
DebugRequestType::Attach(_) => "attach",
|
||||
},
|
||||
"subProcess": true,
|
||||
"redirectOutput": true,
|
||||
});
|
||||
let map = args.as_object_mut().unwrap();
|
||||
match &config.request {
|
||||
DebugRequest::Attach(attach) => {
|
||||
DebugRequestType::Attach(attach) => {
|
||||
map.insert("processId".into(), attach.process_id.into());
|
||||
}
|
||||
DebugRequest::Launch(launch) => {
|
||||
DebugRequestType::Launch(launch) => {
|
||||
map.insert("program".into(), launch.program.clone().into());
|
||||
map.insert("args".into(), launch.args.clone().into());
|
||||
|
||||
@@ -141,22 +141,20 @@ impl DebugAdapter for PythonDebugAdapter {
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
adapter_name: self.name(),
|
||||
command: python_path.ok_or(anyhow!("failed to find binary path for python"))?,
|
||||
arguments: vec![
|
||||
debugpy_dir
|
||||
.join(Self::ADAPTER_PATH)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
format!("--port={}", port),
|
||||
format!("--host={}", host),
|
||||
],
|
||||
arguments: Some(vec![
|
||||
debugpy_dir.join(Self::ADAPTER_PATH).into(),
|
||||
format!("--port={}", port).into(),
|
||||
format!("--host={}", host).into(),
|
||||
]),
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
timeout,
|
||||
}),
|
||||
cwd: None,
|
||||
envs: HashMap::default(),
|
||||
envs: None,
|
||||
request_args: self.request_args(config),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ workspace = true
|
||||
path = "src/debugger_tools.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
dap.workspace = true
|
||||
|
||||
@@ -41,7 +41,7 @@ struct DapLogView {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
pub struct LogStore {
|
||||
struct LogStore {
|
||||
projects: HashMap<WeakEntity<Project>, ProjectState>,
|
||||
debug_clients: HashMap<SessionId, DebugAdapterState>,
|
||||
rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
|
||||
@@ -101,7 +101,7 @@ impl DebugAdapterState {
|
||||
}
|
||||
|
||||
impl LogStore {
|
||||
pub fn new(cx: &Context<Self>) -> Self {
|
||||
fn new(cx: &Context<Self>) -> Self {
|
||||
let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some((client_id, io_kind, message)) = rpc_rx.next().await {
|
||||
@@ -566,13 +566,11 @@ impl DapLogView {
|
||||
.dap_store()
|
||||
.read(cx)
|
||||
.sessions()
|
||||
.filter_map(|session| {
|
||||
let session = session.read(cx);
|
||||
session.adapter_name();
|
||||
let client = session.adapter_client()?;
|
||||
.filter_map(|client| {
|
||||
let client = client.read(cx).adapter_client()?;
|
||||
Some(DapMenuItem {
|
||||
client_id: client.id(),
|
||||
client_name: session.adapter_name().to_string(),
|
||||
client_name: client.name().0.as_ref().into(),
|
||||
has_adapter_logs: client.has_adapter_logs(),
|
||||
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
})
|
||||
@@ -845,29 +843,3 @@ impl EventEmitter<Event> for LogStore {}
|
||||
impl EventEmitter<Event> for DapLogView {}
|
||||
impl EventEmitter<EditorEvent> for DapLogView {}
|
||||
impl EventEmitter<SearchEvent> for DapLogView {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LogStore {
|
||||
pub fn contained_session_ids(&self) -> Vec<SessionId> {
|
||||
self.debug_clients.keys().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
|
||||
self.debug_clients
|
||||
.get(&session_id)
|
||||
.expect("This session should exist if a test is calling")
|
||||
.rpc_messages
|
||||
.messages
|
||||
.clone()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
|
||||
self.debug_clients
|
||||
.get(&session_id)
|
||||
.expect("This session should exist if a test is calling")
|
||||
.log_messages
|
||||
.clone()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,6 @@ test-support = [
|
||||
"project/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"env_logger",
|
||||
"unindent",
|
||||
"debugger_tools"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -40,7 +37,6 @@ gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
project.workspace = true
|
||||
@@ -57,13 +53,9 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
env_logger = { workspace = true, optional = true }
|
||||
debugger_tools = { workspace = true, optional = true }
|
||||
unindent = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
debugger_tools = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use dap::DebugRequest;
|
||||
use dap::DebugRequestType;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::Subscription;
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
|
||||
use gpui::{Subscription, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -9,9 +9,7 @@ use sysinfo::System;
|
||||
use ui::{Context, Tooltip, prelude::*};
|
||||
use ui::{ListItem, ListItemSpacing};
|
||||
use util::debug_panic;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::debugger_panel::DebugPanel;
|
||||
use workspace::ModalView;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct Candidate {
|
||||
@@ -24,19 +22,19 @@ pub(crate) struct AttachModalDelegate {
|
||||
selected_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
placeholder_text: Arc<str>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<project::Project>,
|
||||
pub(crate) debug_config: task::DebugTaskDefinition,
|
||||
candidates: Arc<[Candidate]>,
|
||||
}
|
||||
|
||||
impl AttachModalDelegate {
|
||||
fn new(
|
||||
workspace: Entity<Workspace>,
|
||||
project: Entity<project::Project>,
|
||||
debug_config: task::DebugTaskDefinition,
|
||||
candidates: Arc<[Candidate]>,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace: workspace.downgrade(),
|
||||
project,
|
||||
debug_config,
|
||||
candidates,
|
||||
selected_index: 0,
|
||||
@@ -53,7 +51,7 @@ pub struct AttachModal {
|
||||
|
||||
impl AttachModal {
|
||||
pub fn new(
|
||||
workspace: Entity<Workspace>,
|
||||
project: Entity<project::Project>,
|
||||
debug_config: task::DebugTaskDefinition,
|
||||
modal: bool,
|
||||
window: &mut Window,
|
||||
@@ -77,11 +75,11 @@ impl AttachModal {
|
||||
.collect();
|
||||
processes.sort_by_key(|k| k.name.clone());
|
||||
let processes = processes.into_iter().collect();
|
||||
Self::with_processes(workspace, debug_config, processes, modal, window, cx)
|
||||
Self::with_processes(project, debug_config, processes, modal, window, cx)
|
||||
}
|
||||
|
||||
pub(super) fn with_processes(
|
||||
workspace: Entity<Workspace>,
|
||||
project: Entity<project::Project>,
|
||||
debug_config: task::DebugTaskDefinition,
|
||||
processes: Arc<[Candidate]>,
|
||||
modal: bool,
|
||||
@@ -90,7 +88,7 @@ impl AttachModal {
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| {
|
||||
Picker::uniform_list(
|
||||
AttachModalDelegate::new(workspace, debug_config, processes),
|
||||
AttachModalDelegate::new(project, debug_config, processes),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -204,7 +202,7 @@ impl PickerDelegate for AttachModalDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let candidate = self
|
||||
.matches
|
||||
.get(self.selected_index())
|
||||
@@ -218,26 +216,23 @@ impl PickerDelegate for AttachModalDelegate {
|
||||
};
|
||||
|
||||
match &mut self.debug_config.request {
|
||||
DebugRequest::Attach(config) => {
|
||||
DebugRequestType::Attach(config) => {
|
||||
config.process_id = Some(candidate.pid);
|
||||
}
|
||||
DebugRequest::Launch(_) => {
|
||||
DebugRequestType::Launch(_) => {
|
||||
debug_panic!("Debugger attach modal used on launch debug config");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let definition = self.debug_config.clone();
|
||||
let panel = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(panel) = panel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.start_session(definition, window, cx);
|
||||
});
|
||||
}
|
||||
let config = self.debug_config.clone();
|
||||
self.project
|
||||
.update(cx, |project, cx| {
|
||||
let ret = project.start_debug_session(config, cx);
|
||||
ret
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::{new_session_modal::NewSessionModal, session::DebugSession};
|
||||
use anyhow::{Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use dap::StartDebuggingRequestArguments;
|
||||
use dap::{
|
||||
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
|
||||
client::SessionId, debugger_settings::DebuggerSettings,
|
||||
@@ -18,7 +17,6 @@ use gpui::{
|
||||
actions, anchored, deferred,
|
||||
};
|
||||
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::{
|
||||
Project,
|
||||
debugger::{
|
||||
@@ -32,9 +30,10 @@ use settings::Settings;
|
||||
use std::any::TypeId;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use task::{DebugTaskDefinition, DebugTaskTemplate};
|
||||
use task::DebugTaskDefinition;
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
|
||||
use util::debug_panic;
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
@@ -64,7 +63,7 @@ pub struct DebugPanel {
|
||||
active_session: Option<Entity<DebugSession>>,
|
||||
/// This represents the last debug definition that was created in the new session modal
|
||||
pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
|
||||
project: Entity<Project>,
|
||||
project: WeakEntity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
@@ -98,10 +97,10 @@ impl DebugPanel {
|
||||
window,
|
||||
|panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
|
||||
panel.workspace.update(cx, |workspace, cx| {
|
||||
let workspace_handle = cx.entity().clone();
|
||||
let project = workspace.project().clone();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
crate::attach_modal::AttachModal::new(
|
||||
workspace_handle,
|
||||
project,
|
||||
event.debug_config.clone(),
|
||||
true,
|
||||
window,
|
||||
@@ -128,7 +127,7 @@ impl DebugPanel {
|
||||
_subscriptions,
|
||||
past_debug_definition: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
project,
|
||||
project: project.downgrade(),
|
||||
workspace: workspace.weak_handle(),
|
||||
context_menu: None,
|
||||
};
|
||||
@@ -220,7 +219,7 @@ impl DebugPanel {
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
cx.spawn(async move |cx| {
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
@@ -246,226 +245,114 @@ impl DebugPanel {
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
|
||||
|
||||
debug_panel
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_session(
|
||||
&mut self,
|
||||
definition: DebugTaskDefinition,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let task_contexts = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})
|
||||
.ok();
|
||||
let dap_store = self.project.read(cx).dap_store().clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task_context = if let Some(task) = task_contexts {
|
||||
task.await
|
||||
.active_worktree_context
|
||||
.map_or(task::TaskContext::default(), |context| context.1)
|
||||
} else {
|
||||
task::TaskContext::default()
|
||||
};
|
||||
|
||||
let (session, task) = dap_store.update(cx, |dap_store, cx| {
|
||||
let template = DebugTaskTemplate {
|
||||
locator: None,
|
||||
definition: definition.clone(),
|
||||
};
|
||||
let session = if let Some(debug_config) = template
|
||||
.to_zed_format()
|
||||
.resolve_task("debug_task", &task_context)
|
||||
.and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
|
||||
{
|
||||
dap_store.new_session(debug_config.definition, None, cx)
|
||||
} else {
|
||||
dap_store.new_session(definition.clone(), None, cx)
|
||||
};
|
||||
|
||||
(session.clone(), dap_store.boot_session(session, cx))
|
||||
})?;
|
||||
|
||||
match task.await {
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_error(&e, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
|
||||
session
|
||||
.update(cx, |session, cx| session.shutdown(cx))?
|
||||
.await;
|
||||
}
|
||||
Ok(_) => Self::register_session(this, session, cx).await?,
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
async fn register_session(
|
||||
this: WeakEntity<Self>,
|
||||
session: Entity<Session>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let adapter_name = session.update(cx, |session, _| session.adapter_name())?;
|
||||
this.update_in(cx, |_, window, cx| {
|
||||
cx.subscribe_in(
|
||||
&session,
|
||||
window,
|
||||
move |_, session, event: &SessionStateEvent, window, cx| match event {
|
||||
SessionStateEvent::Restart => {
|
||||
let mut curr_session = session.clone();
|
||||
while let Some(parent_session) = curr_session
|
||||
.read_with(cx, |session, _| session.parent_session().cloned())
|
||||
{
|
||||
curr_session = parent_session;
|
||||
}
|
||||
|
||||
let definition = curr_session.update(cx, |session, _| session.definition());
|
||||
let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
|
||||
|
||||
let definition = definition.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
task.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.start_session(definition, window, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
|
||||
let serialized_layout = persistence::get_serialized_pane_layout(adapter_name).await;
|
||||
|
||||
let workspace = this.update_in(cx, |this, window, cx| {
|
||||
this.sessions.retain(|session| {
|
||||
session
|
||||
.read(cx)
|
||||
.mode()
|
||||
.as_running()
|
||||
.map_or(false, |running_state| {
|
||||
!running_state.read(cx).session().read(cx).is_terminated()
|
||||
})
|
||||
});
|
||||
|
||||
let session_item = DebugSession::running(
|
||||
this.project.clone(),
|
||||
this.workspace.clone(),
|
||||
session,
|
||||
cx.weak_entity(),
|
||||
serialized_layout,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
|
||||
// We might want to make this an event subscription and only notify when a new thread is selected
|
||||
// This is used to filter the command menu correctly
|
||||
cx.observe(&running, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
this.sessions.push(session_item.clone());
|
||||
this.activate_session(session_item, window, cx);
|
||||
this.workspace.clone()
|
||||
})?;
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.focus_panel::<Self>(window, cx);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_child_session(
|
||||
&mut self,
|
||||
request: &StartDebuggingRequestArguments,
|
||||
parent_session: Entity<Session>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(worktree) = parent_session.read(cx).worktree() else {
|
||||
log::error!("Attempted to start a child session from non local debug session");
|
||||
return;
|
||||
};
|
||||
|
||||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let breakpoint_store = self.project.read(cx).breakpoint_store();
|
||||
let definition = parent_session.read(cx).definition().clone();
|
||||
let mut binary = parent_session.read(cx).binary().clone();
|
||||
binary.request_args = request.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
|
||||
let session =
|
||||
dap_store.new_session(definition.clone(), Some(parent_session.clone()), cx);
|
||||
|
||||
let task = session.update(cx, |session, cx| {
|
||||
session.boot(
|
||||
binary,
|
||||
worktree,
|
||||
breakpoint_store,
|
||||
dap_store_handle.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
(session, task)
|
||||
})?;
|
||||
|
||||
match task.await {
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_error(&e, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
|
||||
session
|
||||
.update(cx, |session, cx| session.shutdown(cx))?
|
||||
.await;
|
||||
}
|
||||
Ok(_) => Self::register_session(this, session, cx).await?,
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
|
||||
self.active_session.clone()
|
||||
}
|
||||
|
||||
pub fn debug_panel_items_by_client(
|
||||
&self,
|
||||
client_id: &SessionId,
|
||||
cx: &Context<Self>,
|
||||
) -> Vec<Entity<DebugSession>> {
|
||||
self.sessions
|
||||
.iter()
|
||||
.filter(|item| item.read(cx).session_id(cx) == *client_id)
|
||||
.map(|item| item.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn debug_panel_item_by_client(
|
||||
&self,
|
||||
client_id: SessionId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<DebugSession>> {
|
||||
self.sessions
|
||||
.iter()
|
||||
.find(|item| {
|
||||
let item = item.read(cx);
|
||||
|
||||
item.session_id(cx) == client_id
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn handle_dap_store_event(
|
||||
&mut self,
|
||||
_dap_store: &Entity<DapStore>,
|
||||
dap_store: &Entity<DapStore>,
|
||||
event: &dap_store::DapStoreEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
|
||||
let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
|
||||
return log::error!(
|
||||
"Couldn't get session with id: {session_id:?} from DebugClientStarted event"
|
||||
);
|
||||
};
|
||||
|
||||
let adapter_name = session.read(cx).adapter_name();
|
||||
|
||||
let session_id = *session_id;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let serialized_layout =
|
||||
persistence::get_serialized_pane_layout(adapter_name).await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let Some(project) = this.project.upgrade() else {
|
||||
return log::error!(
|
||||
"Debug Panel out lived it's weak reference to Project"
|
||||
);
|
||||
};
|
||||
|
||||
if this
|
||||
.sessions
|
||||
.iter()
|
||||
.any(|item| item.read(cx).session_id(cx) == session_id)
|
||||
{
|
||||
// We already have an item for this session.
|
||||
debug_panic!("We should never reuse session ids");
|
||||
return;
|
||||
}
|
||||
|
||||
this.sessions.retain(|session| {
|
||||
session
|
||||
.read(cx)
|
||||
.mode()
|
||||
.as_running()
|
||||
.map_or(false, |running_state| {
|
||||
!running_state.read(cx).session().read(cx).is_terminated()
|
||||
})
|
||||
});
|
||||
|
||||
let session_item = DebugSession::running(
|
||||
project,
|
||||
this.workspace.clone(),
|
||||
session,
|
||||
cx.weak_entity(),
|
||||
serialized_layout,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
|
||||
// We might want to make this an event subscription and only notify when a new thread is selected
|
||||
// This is used to filter the command menu correctly
|
||||
cx.observe(&running, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
this.sessions.push(session_item.clone());
|
||||
this.activate_session(session_item, window, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
dap_store::DapStoreEvent::RunInTerminal {
|
||||
title,
|
||||
cwd,
|
||||
@@ -487,12 +374,6 @@ impl DebugPanel {
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
dap_store::DapStoreEvent::SpawnChildSession {
|
||||
request,
|
||||
parent_session,
|
||||
} => {
|
||||
self.start_child_session(request, parent_session.clone(), window, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -527,7 +408,7 @@ impl DebugPanel {
|
||||
cwd,
|
||||
title,
|
||||
},
|
||||
task::RevealStrategy::Never,
|
||||
task::RevealStrategy::Always,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -587,6 +468,8 @@ impl DebugPanel {
|
||||
let session = this.dap_store().read(cx).session_by_id(session_id);
|
||||
session.map(|session| !session.read(cx).is_terminated())
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
@@ -1010,6 +893,7 @@ impl DebugPanel {
|
||||
|
||||
impl EventEmitter<PanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<project::Event> for DebugPanel {}
|
||||
|
||||
impl Focusable for DebugPanel {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
@@ -1155,15 +1039,3 @@ impl Render for DebugPanel {
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
struct DebuggerProvider(Entity<DebugPanel>);
|
||||
|
||||
impl workspace::DebuggerProvider for DebuggerProvider {
|
||||
fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App) {
|
||||
self.0.update(cx, |_, cx| {
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.start_session(definition, window, cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ mod new_session_modal;
|
||||
mod persistence;
|
||||
pub(crate) mod session;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
actions!(
|
||||
|
||||
@@ -4,14 +4,16 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use dap::{DapRegistry, DebugRequest};
|
||||
use anyhow::{Result, anyhow};
|
||||
use dap::DebugRequestType;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
|
||||
WeakEntity,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use task::{DebugTaskDefinition, DebugTaskTemplate, LaunchRequest};
|
||||
use task::{DebugTaskDefinition, LaunchConfig};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
|
||||
@@ -19,6 +21,7 @@ use ui::{
|
||||
LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
|
||||
ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
|
||||
@@ -34,9 +37,9 @@ pub(super) struct NewSessionModal {
|
||||
last_selected_profile_name: Option<SharedString>,
|
||||
}
|
||||
|
||||
fn suggested_label(request: &DebugRequest, debugger: &str) -> String {
|
||||
fn suggested_label(request: &DebugRequestType, debugger: &str) -> String {
|
||||
match request {
|
||||
DebugRequest::Launch(config) => {
|
||||
DebugRequestType::Launch(config) => {
|
||||
let last_path_component = Path::new(&config.program)
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy())
|
||||
@@ -44,7 +47,7 @@ fn suggested_label(request: &DebugRequest, debugger: &str) -> String {
|
||||
|
||||
format!("{} ({debugger})", last_path_component)
|
||||
}
|
||||
DebugRequest::Attach(config) => format!(
|
||||
DebugRequestType::Attach(config) => format!(
|
||||
"pid: {} ({debugger})",
|
||||
config.process_id.unwrap_or(u32::MAX)
|
||||
),
|
||||
@@ -68,7 +71,7 @@ impl NewSessionModal {
|
||||
.and_then(|def| def.stop_on_entry);
|
||||
|
||||
let launch_config = match past_debug_definition.map(|def| def.request) {
|
||||
Some(DebugRequest::Launch(launch_config)) => Some(launch_config),
|
||||
Some(DebugRequestType::Launch(launch_config)) => Some(launch_config),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -85,38 +88,39 @@ impl NewSessionModal {
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_config(&self, cx: &App, debugger: &str) -> DebugTaskDefinition {
|
||||
fn debug_config(&self, cx: &App) -> Option<DebugTaskDefinition> {
|
||||
let request = self.mode.debug_task(cx);
|
||||
DebugTaskDefinition {
|
||||
adapter: debugger.to_owned(),
|
||||
label: suggested_label(&request, debugger),
|
||||
Some(DebugTaskDefinition {
|
||||
adapter: self.debugger.clone()?.to_string(),
|
||||
label: suggested_label(&request, self.debugger.as_deref()?),
|
||||
request,
|
||||
initialize_args: self.initialize_args.clone(),
|
||||
tcp_connection: None,
|
||||
locator: None,
|
||||
stop_on_entry: match self.stop_on_entry {
|
||||
ToggleState::Selected => Some(true),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(debugger) = self.debugger.as_ref() else {
|
||||
// todo: show in UI.
|
||||
log::error!("No debugger selected");
|
||||
return;
|
||||
};
|
||||
let config = self.debug_config(cx, debugger);
|
||||
let debug_panel = self.debug_panel.clone();
|
||||
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) -> Result<()> {
|
||||
let workspace = self.workspace.clone();
|
||||
let config = self
|
||||
.debug_config(cx)
|
||||
.ok_or_else(|| anyhow!("Failed to create a debug config"))?;
|
||||
|
||||
let task_contexts = self
|
||||
.workspace
|
||||
let _ = self.debug_panel.update(cx, |panel, _| {
|
||||
panel.past_debug_definition = Some(config.clone());
|
||||
});
|
||||
|
||||
let task_contexts = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let task_context = if let Some(task) = task_contexts {
|
||||
task.await
|
||||
.active_worktree_context
|
||||
@@ -124,29 +128,39 @@ impl NewSessionModal {
|
||||
} else {
|
||||
task::TaskContext::default()
|
||||
};
|
||||
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
|
||||
|
||||
debug_panel.update_in(cx, |debug_panel, window, cx| {
|
||||
let template = DebugTaskTemplate {
|
||||
locator: None,
|
||||
definition: config.clone(),
|
||||
};
|
||||
if let Some(debug_config) = template
|
||||
.to_zed_format()
|
||||
.resolve_task("debug_task", &task_context)
|
||||
.and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
|
||||
let task = project.update(cx, |this, cx| {
|
||||
if let Some(debug_config) =
|
||||
config
|
||||
.clone()
|
||||
.to_zed_format()
|
||||
.ok()
|
||||
.and_then(|task_template| {
|
||||
task_template
|
||||
.resolve_task("debug_task", &task_context)
|
||||
.and_then(|resolved_task| {
|
||||
resolved_task.resolved_debug_adapter_config()
|
||||
})
|
||||
})
|
||||
{
|
||||
debug_panel.start_session(debug_config.definition, window, cx)
|
||||
this.start_debug_session(debug_config, cx)
|
||||
} else {
|
||||
debug_panel.start_session(config, window, cx)
|
||||
this.start_debug_session(config, cx)
|
||||
}
|
||||
})?;
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
let spawn_result = task.await;
|
||||
if spawn_result.is_ok() {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
spawn_result?;
|
||||
anyhow::Result::<_, anyhow::Error>::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_attach_picker(
|
||||
@@ -200,7 +214,12 @@ impl NewSessionModal {
|
||||
};
|
||||
|
||||
let available_adapters = workspace
|
||||
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
|
||||
.update(cx, |this, cx| {
|
||||
this.project()
|
||||
.read(cx)
|
||||
.debug_adapters()
|
||||
.enumerate_adapters()
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -232,20 +251,23 @@ impl NewSessionModal {
|
||||
this.debugger = Some(task.adapter.clone().into());
|
||||
this.initialize_args = task.initialize_args.clone();
|
||||
match &task.request {
|
||||
DebugRequest::Launch(launch_config) => {
|
||||
DebugRequestType::Launch(launch_config) => {
|
||||
this.mode = NewSessionMode::launch(
|
||||
Some(launch_config.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
DebugRequest::Attach(_) => {
|
||||
let Some(workspace) = this.workspace.upgrade() else {
|
||||
DebugRequestType::Attach(_) => {
|
||||
let Ok(project) = this
|
||||
.workspace
|
||||
.read_with(cx, |this, _| this.project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
this.mode = NewSessionMode::attach(
|
||||
this.debugger.clone(),
|
||||
workspace,
|
||||
project,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -263,7 +285,7 @@ impl NewSessionModal {
|
||||
}
|
||||
};
|
||||
|
||||
let available_adapters: Vec<DebugTaskTemplate> = workspace
|
||||
let available_adapters: Vec<DebugTaskDefinition> = workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.project()
|
||||
.read(cx)
|
||||
@@ -281,9 +303,9 @@ impl NewSessionModal {
|
||||
|
||||
for debug_definition in available_adapters {
|
||||
menu = menu.entry(
|
||||
debug_definition.definition.label.clone(),
|
||||
debug_definition.label.clone(),
|
||||
None,
|
||||
setter_for_name(debug_definition.definition),
|
||||
setter_for_name(debug_definition),
|
||||
);
|
||||
}
|
||||
menu
|
||||
@@ -300,7 +322,7 @@ struct LaunchMode {
|
||||
|
||||
impl LaunchMode {
|
||||
fn new(
|
||||
past_launch_config: Option<LaunchRequest>,
|
||||
past_launch_config: Option<LaunchConfig>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
@@ -326,9 +348,9 @@ impl LaunchMode {
|
||||
cx.new(|_| Self { program, cwd })
|
||||
}
|
||||
|
||||
fn debug_task(&self, cx: &App) -> task::LaunchRequest {
|
||||
fn debug_task(&self, cx: &App) -> task::LaunchConfig {
|
||||
let path = self.cwd.read(cx).text(cx);
|
||||
task::LaunchRequest {
|
||||
task::LaunchConfig {
|
||||
program: self.program.read(cx).text(cx),
|
||||
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
|
||||
args: Default::default(),
|
||||
@@ -345,20 +367,21 @@ struct AttachMode {
|
||||
impl AttachMode {
|
||||
fn new(
|
||||
debugger: Option<SharedString>,
|
||||
workspace: Entity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Entity<Self> {
|
||||
let debug_definition = DebugTaskDefinition {
|
||||
label: "Attach New Session Setup".into(),
|
||||
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
|
||||
request: dap::DebugRequestType::Attach(task::AttachConfig { process_id: None }),
|
||||
tcp_connection: None,
|
||||
adapter: debugger.clone().unwrap_or_default().into(),
|
||||
locator: None,
|
||||
initialize_args: None,
|
||||
stop_on_entry: Some(false),
|
||||
};
|
||||
let attach_picker = cx.new(|cx| {
|
||||
let modal = AttachModal::new(workspace, debug_definition.clone(), false, window, cx);
|
||||
let modal = AttachModal::new(project, debug_definition.clone(), false, window, cx);
|
||||
window.focus(&modal.focus_handle(cx));
|
||||
|
||||
modal
|
||||
@@ -368,8 +391,8 @@ impl AttachMode {
|
||||
attach_picker,
|
||||
})
|
||||
}
|
||||
fn debug_task(&self) -> task::AttachRequest {
|
||||
task::AttachRequest { process_id: None }
|
||||
fn debug_task(&self) -> task::AttachConfig {
|
||||
task::AttachConfig { process_id: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +406,7 @@ enum NewSessionMode {
|
||||
}
|
||||
|
||||
impl NewSessionMode {
|
||||
fn debug_task(&self, cx: &App) -> DebugRequest {
|
||||
fn debug_task(&self, cx: &App) -> DebugRequestType {
|
||||
match self {
|
||||
NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
|
||||
NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
|
||||
@@ -458,14 +481,14 @@ impl RenderOnce for NewSessionMode {
|
||||
impl NewSessionMode {
|
||||
fn attach(
|
||||
debugger: Option<SharedString>,
|
||||
workspace: Entity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Self {
|
||||
Self::Attach(AttachMode::new(debugger, workspace, window, cx))
|
||||
Self::Attach(AttachMode::new(debugger, project, window, cx))
|
||||
}
|
||||
fn launch(
|
||||
past_launch_config: Option<LaunchRequest>,
|
||||
past_launch_config: Option<LaunchConfig>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Self {
|
||||
@@ -557,12 +580,15 @@ impl Render for NewSessionModal {
|
||||
.toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let Some(workspace) = this.workspace.upgrade() else {
|
||||
let Ok(project) = this
|
||||
.workspace
|
||||
.read_with(cx, |this, _| this.project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
this.mode = NewSessionMode::attach(
|
||||
this.debugger.clone(),
|
||||
workspace,
|
||||
project,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -616,7 +642,7 @@ impl Render for NewSessionModal {
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.start_new_session(window, cx);
|
||||
this.start_new_session(window, cx).log_err();
|
||||
}))
|
||||
.disabled(self.debugger.is_none()),
|
||||
),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user