Compare commits

..

13 Commits

Author SHA1 Message Date
Michael Sloan
03ecb88fe3 Add .rules file 2025-04-18 19:07:08 -06:00
Antonio Scandurra
38fcadf948 Merge remote-tracking branch 'origin/main' into custom-tool-cards 2025-04-09 16:42:30 -06:00
Michael Sloan
ba767a1998 Fix directory context paths (#28459)
Release Notes:

- N/A
2025-04-09 21:40:46 +00:00
Antonio Scandurra
e5cbac1373 Checkpoint 2025-04-09 15:21:36 -06:00
renovate[bot]
23c3f5f410 Update Rust crate indexmap to v2.9.0 (#28455)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [indexmap](https://redirect.github.com/indexmap-rs/indexmap) |
workspace.dependencies | minor | `2.8.0` -> `2.9.0` |

---

### Release Notes

<details>
<summary>indexmap-rs/indexmap (indexmap)</summary>

###
[`v2.9.0`](https://redirect.github.com/indexmap-rs/indexmap/blob/HEAD/RELEASES.md#290-2025-04-04)

[Compare
Source](https://redirect.github.com/indexmap-rs/indexmap/compare/2.8.0...2.9.0)

- Added a `get_disjoint_mut` method to `IndexMap`, matching Rust 1.86's
    `HashMap` method.
- Added a `get_disjoint_indices_mut` method to `IndexMap` and
`map::Slice`,
    matching Rust 1.86's `get_disjoint_mut` method on slices.
- Deprecated the `borsh` feature in favor of their own `indexmap`
feature,
    solving a cyclic dependency that occured via `borsh-derive`.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-09 14:52:40 -06:00
Vitaly Slobodin
b3be294c90 lsp_store: Preserve environment variables from ExtensionLspAdapter (#28173)
## Description

In https://github.com/zed-industries/zed/pull/27213 the new feature for
setting env variables for LSPs was added but env vars passed from an
instance of `ExtensionLspAdapter` are lost now. This means if an
extension returns any env variable like this:

```rust
zed::Command {
  command: some_command,
  args: some_args,
  env: vec![("A", "value_for_a")],
}
```

The env variable `A` will never be used by `LspStore`. This commit
preserves env variables passed from an instance of
`ExtensionLspAdapter`.

After this change overwriting of env variables
happens in the following order:

```plaintext
shell <- variables from an extension <- variables from settings
```

## How to reproduce

Allow any extension to return a `zed::Command` with environment
variables to Zed. You can use [this
branch](https://github.com/zed-extensions/ruby/pull/48) for the Ruby
extension:

1. Check out the branch and install the dev version of the Ruby
extension.
2. Ensure you have the `solargraph` LSP configured and enabled for the
Ruby extension. This LSP is enabled by default in Zed and in the Ruby
extension.
3. Make sure you don’t have `solargraph` installed in your user gemset.
4. Open any Ruby project, such as [this
one](https://github.com/vitallium/stimulus-lsp-error-zed).
5. Open a Ruby file and wait for the error message about failing to
start `solargraph`. It should look like this or something similar:

```
[2025-04-05T23:17:26+02:00 ERROR project::lsp_store] server stderr: "/Users/vslobodin/.local/share/mise/installs/ruby/3.4.1/lib/ruby/site_ruby/3.4.0/rubygems.rb:262:in 'Gem.find_spec_for_exe': can't find gem solargraph (>= 0.a) with executable solargraph (Gem::GemNotFoundException)\n\tfrom /Users/vslobodin/.local/share/mise/installs/ruby/3.4.1/lib/ruby/site_ruby/3.4.0/rubygems.rb:281:in 'Gem.activate_bin_path'\n"
```

This error occurs because the Ruby extension passes the `GEM_PATH`
environment variable to specify the location of Ruby gems. Without it,
Zed tries to spawn the `solargraph` gem in the user's gemset scope. Ruby
fails to start it because the `solargraph` gem is not installed in the
user gemset but in the extension directory. By setting the `GEM_PATH`
environment variable, Ruby searches additional locations to start the
`solargraph` LSP.

I hope I've described it correctly. Please let me know if you need more
information. Thanks!

Release Notes:

- Fixed the issue where environment variables from `ExtensionLspAdapter`
were lost
2025-04-09 14:50:50 -06:00
Dino
af5318df98 Update default vim substitute command behavior and add support for 'g' flag (#28138)
This Pull Request updates the default behavior of the substitute (`s`)
command in vim mode to only replace the next match by default, instead
of all, and replace all matches only when the `g` flag is provided,
making it more similar to NeoVim's behavior.

In order to achieve this, the following changes were introduced:

- Update `BufferSearchBar::replace_next` to be a public method, so it
can be called from `Vim::replace_command` .
- Update the `Replacement::parse` to set the `should_replace_all` field
to `false` by default, and only set it to `true` if the `'g'` flag is
present in the query.
- Add support for when the `Replacement.should_replace_all` is set to
`false` in `Vim::replace_command`, so as to have it only replace the
next occurrence instead of all occurrences in the line.
- Introduce `BufferSearchBar::select_first_match` so as to activate the
first match on the line under the cursor.

Closes #24450 

Release Notes:

- Improved vim's substitute command so as to only replace the first
match by default, and replace all matches if the `'g'` flag is provided

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-04-09 14:34:51 -06:00
5brian
60c420a2da docs: Update vim features (#28360)
Follow up:
https://github.com/zed-industries/zed/pull/28044#issuecomment-2786769520

Adds
- Indent wise motions
- :ls
- :set

Release Notes:

- vim: Added documentation for indent-wise motions, `:ls`, and `:set`
2025-04-09 16:30:50 -04:00
5brian
ee6c33ffb3 Fix vim test keystroke (#28406)
I wrote the test wrongly in
https://github.com/zed-industries/zed/pull/28005:

It should be `$` instead of `shift-4`, so it was just yanking from the
middle of the line instead of the newline character. Fixed it and
regenerated it.

Release Notes:

- N/A
2025-04-09 14:29:03 -06:00
tidely
9ae4f4b158 gpui: Use BoolExt trait in more places (#28052)
Use the `BoolExt` trait which converts rust booleans to their objc
equivalent when applicable.


Release Notes:

- N/A
2025-04-09 14:28:15 -06:00
renovate[bot]
915a1cb116 Update actions/dependency-review-action digest to 67d4f4b (#28450)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/dependency-review-action](https://redirect.github.com/actions/dependency-review-action)
| action | digest | `3b139cf` -> `67d4f4b` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMzguMCIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-09 14:23:48 -06:00
renovate[bot]
aead0e11ff Update Rust crate mimalloc to v0.1.46 (#27964)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mimalloc](https://redirect.github.com/purpleprotocol/mimalloc_rust) |
dependencies | patch | `0.1.45` -> `0.1.46` |

---

### Release Notes

<details>
<summary>purpleprotocol/mimalloc_rust (mimalloc)</summary>

###
[`v0.1.46`](https://redirect.github.com/purpleprotocol/mimalloc_rust/releases/tag/v0.1.46):
Version 0.1.46

[Compare
Source](https://redirect.github.com/purpleprotocol/mimalloc_rust/compare/v0.1.45...v0.1.46)

##### Changes

-   Fixed musl builds.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMjcuMyIsInVwZGF0ZWRJblZlciI6IjM5LjIzOC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-09 14:23:19 -06:00
Antonio Scandurra
53375434cf Lay the groundwork to support rendering custom tool cards 2025-04-09 08:17:35 -06:00
52 changed files with 638 additions and 559 deletions

View File

@@ -225,7 +225,7 @@ jobs:
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 # v4
with:
license-check: false

110
.rules Normal file
View File

@@ -0,0 +1,110 @@
# 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.
# 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.

View File

@@ -1,13 +1,13 @@
[
{
"label": "Debug Zed (CodeLLDB)",
"adapter": "CodeLLDB",
"label": "Debug Zed with LLDB",
"adapter": "LLDB",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
},
{
"label": "Debug Zed (GDB)",
"label": "Debug Zed with GDB",
"adapter": "GDB",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",

14
Cargo.lock generated
View File

@@ -7080,9 +7080,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.8.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@@ -7937,9 +7937,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libmimalloc-sys"
version = "0.1.41"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
dependencies = [
"cc",
"libc",
@@ -8610,9 +8610,9 @@ dependencies = [
[[package]]
name = "mimalloc"
version = "0.1.45"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
dependencies = [
"libmimalloc-sys",
]
@@ -18106,7 +18106,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.183.0"
version = "0.182.0"
dependencies = [
"activity_indicator",
"agent",

View File

@@ -856,8 +856,8 @@ impl ActiveThread {
&tool_use.input,
self.thread
.read(cx)
.tool_result(&tool_use.id)
.map(|result| result.content.clone().into())
.output_for_tool(&tool_use.id)
.map(|output| output.clone().into())
.unwrap_or("".into()),
cx,
);
@@ -2202,18 +2202,23 @@ impl ActiveThread {
.buffer_font(cx),
)
.child(div().w_full().text_ui_sm(cx).children(
rendered_tool_use.as_ref().map(|rendered| {
MarkdownElement::new(
rendered.output.clone(),
tool_use_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
Some(card.clone().into_any_element())
} else {
rendered_tool_use.as_ref().map(|rendered| {
MarkdownElement::new(
rendered.output.clone(),
tool_use_markdown_style(window, cx),
)
.on_url_click({
let workspace = self.workspace.clone();
move |text, window, cx| {
open_markdown_link(text, workspace.clone(), window, cx);
}
})
.into_any_element()
})
}),
},
)),
),
ToolUseStatus::Running => container.child(
@@ -2255,10 +2260,11 @@ impl ActiveThread {
.color(Color::Muted)
.buffer_font(cx),
)
.child(
div()
.text_ui_sm(cx)
.children(rendered_tool_use.as_ref().map(|rendered| {
.child(div().text_ui_sm(cx).children(
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
Some(card.clone().into_any_element())
} else {
rendered_tool_use.as_ref().map(|rendered| {
MarkdownElement::new(
rendered.output.clone(),
tool_use_markdown_style(window, cx),
@@ -2269,8 +2275,10 @@ impl ActiveThread {
open_markdown_link(text, workspace.clone(), window, cx);
}
})
})),
),
.into_any_element()
})
},
)),
),
ToolUseStatus::Pending => container,
ToolUseStatus::NeedsConfirmation => container.child(
@@ -2840,10 +2848,10 @@ pub(crate) fn open_context(
}
}
AssistantContext::Directory(directory_context) => {
let path = directory_context.project_path.clone();
let project_path = directory_context.project_path(cx);
workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
if let Some(entry) = project.entry_for_path(&path, cx) {
if let Some(entry) = project.entry_for_path(&project_path, cx) {
cx.emit(project::Event::RevealInProjectPanel(entry.id));
}
})

View File

@@ -1,9 +1,9 @@
use std::{ops::Range, sync::Arc};
use std::{ops::Range, path::Path, sync::Arc};
use gpui::{App, Entity, SharedString};
use language::{Buffer, File};
use language_model::LanguageModelRequestMessage;
use project::ProjectPath;
use project::{ProjectPath, Worktree};
use serde::{Deserialize, Serialize};
use text::{Anchor, BufferId};
use ui::IconName;
@@ -69,10 +69,21 @@ pub struct FileContext {
#[derive(Debug, Clone)]
pub struct DirectoryContext {
pub id: ContextId,
pub project_path: ProjectPath,
pub worktree: Entity<Worktree>,
pub path: Arc<Path>,
/// Buffers of the files within the directory.
pub context_buffers: Vec<ContextBuffer>,
}
impl DirectoryContext {
pub fn project_path(&self, cx: &App) -> ProjectPath {
ProjectPath {
worktree_id: self.worktree.read(cx).id(),
path: self.path.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct SymbolContext {
pub id: ContextId,
@@ -86,12 +97,11 @@ pub struct FetchedUrlContext {
pub text: SharedString,
}
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
// explicitly or have a WeakModel<Thread> and remove during snapshot.
#[derive(Debug, Clone)]
pub struct ThreadContext {
pub id: ContextId,
// TODO: Entity<Thread> holds onto the thread even if the thread is deleted. Should probably be
// a WeakEntity and handle removal from the UI when it has dropped.
pub thread: Entity<Thread>,
pub text: SharedString,
}
@@ -105,12 +115,11 @@ impl ThreadContext {
}
}
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
// the context from the message editor in this case.
#[derive(Clone)]
pub struct ContextBuffer {
pub id: BufferId,
// TODO: Entity<Buffer> holds onto the thread even if the thread is deleted. Should probably be
// a WeakEntity and handle removal from the UI when it has dropped.
pub buffer: Entity<Buffer>,
pub file: Arc<dyn File>,
pub version: clock::Global,

View File

@@ -289,12 +289,14 @@ impl ContextPicker {
path_prefix,
} => {
let context_store = self.context_store.clone();
let worktree_id = project_path.worktree_id;
let path = project_path.path.clone();
ContextMenuItem::custom_entry(
move |_window, cx| {
render_file_context_entry(
ElementId::NamedInteger("ctx-recent".into(), ix),
worktree_id,
&path,
&path_prefix,
false,
@@ -466,7 +468,7 @@ fn recent_context_picker_entries(
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
.filter(|(path, _)| !current_files.contains(path))
.take(4)
.filter_map(|(project_path, _)| {
project

View File

@@ -189,6 +189,7 @@ impl PickerDelegate for FileContextPickerDelegate {
.toggle_state(selected)
.child(render_file_context_entry(
ElementId::NamedInteger("file-ctx-picker".into(), ix),
WorktreeId::from_usize(mat.worktree_id),
&mat.path,
&mat.path_prefix,
mat.is_dir,
@@ -328,19 +329,26 @@ pub fn extract_file_name_and_directory(
pub fn render_file_context_entry(
id: ElementId,
path: &Path,
worktree_id: WorktreeId,
path: &Arc<Path>,
path_prefix: &Arc<str>,
is_directory: bool,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix);
let added = context_store.upgrade().and_then(|context_store| {
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
if is_directory {
context_store.read(cx).includes_directory(path)
context_store.read(cx).includes_directory(&project_path)
} else {
context_store.read(cx).will_include_file_path(path, cx)
context_store
.read(cx)
.will_include_file_path(&project_path, cx)
}
});
@@ -380,8 +388,9 @@ pub fn render_file_context_entry(
)
.child(Label::new("Added").size(LabelSize::Small)),
),
FileInclusion::InDirectory(dir_name) => {
let dir_name = dir_name.to_string_lossy().into_owned();
FileInclusion::InDirectory(directory_project_path) => {
// TODO: Consider using worktree full_path to include worktree name.
let directory_path = directory_project_path.path.to_string_lossy().into_owned();
el.child(
h_flex()
@@ -395,7 +404,7 @@ pub fn render_file_context_entry(
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(Tooltip::text(format!("in {dir_name}")))
.tooltip(Tooltip::text(format!("in {directory_path}")))
}
})
}

View File

@@ -1,5 +1,5 @@
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow};
@@ -28,7 +28,7 @@ pub struct ContextStore {
// 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<PathBuf, ContextId>,
directories: HashMap<ProjectPath, ContextId>,
symbols: HashMap<ContextSymbolId, ContextId>,
symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
@@ -93,7 +93,7 @@ impl ContextStore {
let buffer_id = this.update(cx, |_, cx| buffer.read(cx).remote_id())?;
let already_included = this.update(cx, |this, cx| {
match this.will_include_buffer(buffer_id, &project_path.path) {
match this.will_include_buffer(buffer_id, &project_path) {
Some(FileInclusion::Direct(context_id)) => {
if remove_if_exists {
this.remove_context(context_id, cx);
@@ -159,7 +159,7 @@ impl ContextStore {
return Task::ready(Err(anyhow!("failed to read project")));
};
let already_included = match self.includes_directory(&project_path.path) {
let already_included = match self.includes_directory(&project_path) {
Some(FileInclusion::Direct(context_id)) => {
if remove_if_exists {
self.remove_context(context_id, cx);
@@ -223,14 +223,12 @@ impl ContextStore {
.collect::<Vec<_>>();
if context_buffers.is_empty() {
return Err(anyhow!(
"No text files found in {}",
&project_path.path.display()
));
let full_path = cx.update(|cx| worktree.read(cx).full_path(&project_path.path))?;
return Err(anyhow!("No text files found in {}", &full_path.display()));
}
this.update(cx, |this, cx| {
this.insert_directory(project_path, context_buffers, cx);
this.insert_directory(worktree, project_path, context_buffers, cx);
})?;
anyhow::Ok(())
@@ -239,17 +237,20 @@ impl ContextStore {
fn insert_directory(
&mut self,
worktree: Entity<Worktree>,
project_path: ProjectPath,
context_buffers: Vec<ContextBuffer>,
cx: &mut Context<Self>,
) {
let id = self.next_context_id.post_inc();
self.directories.insert(project_path.path.to_path_buf(), id);
let path = project_path.path.clone();
self.directories.insert(project_path, id);
self.context
.push(AssistantContext::Directory(DirectoryContext {
id,
project_path,
worktree,
path,
context_buffers,
}));
cx.notify();
@@ -478,23 +479,31 @@ impl ContextStore {
/// Returns whether the buffer is already included directly in the context, or if it will be
/// included in the context via a directory. Directory inclusion is based on paths rather than
/// buffer IDs as the directory will be re-scanned.
pub fn will_include_buffer(&self, buffer_id: BufferId, path: &Path) -> Option<FileInclusion> {
pub fn will_include_buffer(
&self,
buffer_id: BufferId,
project_path: &ProjectPath,
) -> Option<FileInclusion> {
if let Some(context_id) = self.files.get(&buffer_id) {
return Some(FileInclusion::Direct(*context_id));
}
self.will_include_file_path_via_directory(path)
self.will_include_file_path_via_directory(project_path)
}
/// Returns whether this file path is already included directly in the context, or if it will be
/// included in the context via a directory.
pub fn will_include_file_path(&self, path: &Path, cx: &App) -> Option<FileInclusion> {
pub fn will_include_file_path(
&self,
project_path: &ProjectPath,
cx: &App,
) -> Option<FileInclusion> {
if !self.files.is_empty() {
let found_file_context = self.context.iter().find(|context| match &context {
AssistantContext::File(file_context) => {
let buffer = file_context.context_buffer.buffer.read(cx);
if let Some(file_path) = buffer_path_log_err(buffer, cx) {
*file_path == *path
if let Some(context_path) = buffer.project_path(cx) {
&context_path == project_path
} else {
false
}
@@ -506,31 +515,40 @@ impl ContextStore {
}
}
self.will_include_file_path_via_directory(path)
self.will_include_file_path_via_directory(project_path)
}
fn will_include_file_path_via_directory(&self, path: &Path) -> Option<FileInclusion> {
fn will_include_file_path_via_directory(
&self,
project_path: &ProjectPath,
) -> Option<FileInclusion> {
if self.directories.is_empty() {
return None;
}
let mut buf = path.to_path_buf();
let mut path_buf = project_path.path.to_path_buf();
while buf.pop() {
if let Some(_) = self.directories.get(&buf) {
return Some(FileInclusion::InDirectory(buf));
while path_buf.pop() {
// TODO: This isn't very efficient. Consider using a better representation of the
// directories map.
let directory_project_path = ProjectPath {
worktree_id: project_path.worktree_id,
path: path_buf.clone().into(),
};
if let Some(_) = self.directories.get(&directory_project_path) {
return Some(FileInclusion::InDirectory(directory_project_path));
}
}
None
}
pub fn includes_directory(&self, path: &Path) -> Option<FileInclusion> {
if let Some(context_id) = self.directories.get(path) {
pub fn includes_directory(&self, project_path: &ProjectPath) -> Option<FileInclusion> {
if let Some(context_id) = self.directories.get(project_path) {
return Some(FileInclusion::Direct(*context_id));
}
self.will_include_file_path_via_directory(path)
self.will_include_file_path_via_directory(project_path)
}
pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
@@ -564,13 +582,13 @@ impl ContextStore {
}
}
pub fn file_paths(&self, cx: &App) -> HashSet<PathBuf> {
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
self.context
.iter()
.filter_map(|context| match context {
AssistantContext::File(file) => {
let buffer = file.context_buffer.buffer.read(cx);
buffer_path_log_err(buffer, cx).map(|p| p.to_path_buf())
buffer.project_path(cx)
}
AssistantContext::Directory(_)
| AssistantContext::Symbol(_)
@@ -587,7 +605,7 @@ impl ContextStore {
pub enum FileInclusion {
Direct(ContextId),
InDirectory(PathBuf),
InDirectory(ProjectPath),
}
// ContextBuffer without text.
@@ -654,19 +672,6 @@ fn collect_buffer_info_and_text(
Ok((buffer_info, text_task))
}
pub fn buffer_path_log_err(buffer: &Buffer, cx: &App) -> Option<Arc<Path>> {
if let Some(file) = buffer.file() {
let mut path = file.path().clone();
if path.as_os_str().is_empty() {
path = file.full_path(cx).into();
}
Some(path)
} else {
log::error!("Buffer that had a path unexpectedly no longer has a path.");
None
}
}
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
let path_extension = path.extension().and_then(|ext| ext.to_str());
let path_string = path.to_string_lossy();
@@ -742,13 +747,13 @@ pub fn refresh_context_store_text(
}
}
AssistantContext::Directory(directory_context) => {
let directory_path = directory_context.project_path(cx);
let should_refresh = changed_buffers.is_empty()
|| changed_buffers.iter().any(|buffer| {
let buffer = buffer.read(cx);
buffer_path_log_err(&buffer, cx).map_or(false, |path| {
path.starts_with(&directory_context.project_path.path)
})
let Some(buffer_path) = buffer.read(cx).project_path(cx) else {
return false;
};
buffer_path.starts_with(&directory_path)
});
if should_refresh {
@@ -835,14 +840,16 @@ fn refresh_directory_text(
let context_buffers = future::join_all(futures);
let id = directory_context.id;
let project_path = directory_context.project_path.clone();
let worktree = directory_context.worktree.clone();
let path = directory_context.path.clone();
Some(cx.spawn(async move |cx| {
let context_buffers = context_buffers.await;
context_store
.update(cx, |context_store, _| {
let new_directory_context = DirectoryContext {
id,
project_path,
worktree,
path,
context_buffers,
};
context_store.replace_context(AssistantContext::Directory(new_directory_context));

View File

@@ -1,3 +1,4 @@
use std::path::Path;
use std::rc::Rc;
use collections::HashSet;
@@ -9,6 +10,7 @@ use gpui::{
};
use itertools::Itertools;
use language::Buffer;
use project::ProjectItem;
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use workspace::{Workspace, notifications::NotifyResultExt};
@@ -93,26 +95,23 @@ impl ContextStrip {
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
let active_buffer = active_buffer_entity.read(cx);
let path = active_buffer.file()?.full_path(cx);
let project_path = active_buffer.project_path(cx)?;
if self
.context_store
.read(cx)
.will_include_buffer(active_buffer.remote_id(), &path)
.will_include_buffer(active_buffer.remote_id(), &project_path)
.is_some()
{
return None;
}
let name = match path.file_name() {
Some(name) => name.to_string_lossy().into_owned().into(),
None => path.to_string_lossy().into_owned().into(),
};
let file_name = active_buffer.file()?.file_name(cx);
let icon_path = FileIcons::get_icon(&path, cx);
let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
Some(SuggestedContext::File {
name,
name: file_name.to_string_lossy().into_owned().into(),
buffer: active_buffer_entity.downgrade(),
icon_path,
})

View File

@@ -6,7 +6,7 @@ use std::sync::Arc;
use agent_rules::load_worktree_rules_file;
use anyhow::{Context as _, Result, anyhow};
use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap};
use fs::Fs;
@@ -604,8 +604,12 @@ impl Thread {
self.tool_use.tool_results_for_message(id)
}
pub fn tool_result(&self, id: &LanguageModelToolUseId) -> Option<&LanguageModelToolResult> {
self.tool_use.tool_result(id)
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
Some(&self.tool_use.tool_result(id)?.content)
}
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&gpui::AnyView> {
self.tool_use.tool_result_card(id)
}
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
@@ -1450,8 +1454,11 @@ impl Thread {
) -> Task<()> {
let tool_name: Arc<str> = tool.name().into();
let run_tool = if self.tools.is_disabled(&tool.source(), &tool_name) {
Task::ready(Err(anyhow!("tool is disabled: {tool_name}")))
let tool_result = if self.tools.is_disabled(&tool.source(), &tool_name) {
ToolResult {
output: Task::ready(Err(anyhow!("tool is disabled: {tool_name}"))),
card: None,
}
} else {
tool.run(
input,
@@ -1462,9 +1469,15 @@ impl Thread {
)
};
// Store the card separately if it exists
if let Some(card) = tool_result.card.clone() {
self.tool_use
.insert_tool_result_card(tool_use_id.clone(), card);
}
cx.spawn({
async move |thread: WeakEntity<Thread>, cx| {
let output = run_tool.await;
let output = tool_result.output.await;
thread
.update(cx, |thread, cx| {

View File

@@ -54,6 +54,7 @@ pub struct ToolUseState {
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
tool_result_cards: HashMap<LanguageModelToolUseId, gpui::AnyView>,
}
pub const USING_TOOL_MARKER: &str = "<using_tool>";
@@ -66,6 +67,7 @@ impl ToolUseState {
tool_uses_by_user_message: HashMap::default(),
tool_results: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
tool_result_cards: HashMap::default(),
}
}
@@ -257,6 +259,18 @@ impl ToolUseState {
self.tool_results.get(tool_use_id)
}
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&gpui::AnyView> {
self.tool_result_cards.get(tool_use_id)
}
pub fn insert_tool_result_card(
&mut self,
tool_use_id: LanguageModelToolUseId,
card: gpui::AnyView,
) {
self.tool_result_cards.insert(tool_use_id, card);
}
pub fn request_tool_use(
&mut self,
assistant_message_id: MessageId,

View File

@@ -280,9 +280,10 @@ impl AddedContext {
}
AssistantContext::Directory(directory_context) => {
// TODO: handle worktree disambiguation. Maybe by storing an `Arc<dyn File>` to also
// handle renames?
let full_path = &directory_context.project_path.path;
let full_path = directory_context
.worktree
.read(cx)
.full_path(&directory_context.path);
let full_path_string: SharedString =
full_path.to_string_lossy().into_owned().into();
let name = full_path

View File

@@ -149,7 +149,7 @@ impl HeadlessAssistant {
.entry(pending_tool_use.name.clone())
.or_insert(0) += 1;
}
if let Some(tool_result) = thread.read(cx).tool_result(tool_use_id) {
if let Some(tool_result) = thread.read(cx).output_for_tool(tool_use_id) {
println!("Tool result: {:?}", tool_result);
}
if thread.read(cx).all_tools_finished() {

View File

@@ -8,7 +8,7 @@ use std::fmt::Formatter;
use std::sync::Arc;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use gpui::{AnyView, App, Entity, SharedString, Task};
use icons::IconName;
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -22,6 +22,22 @@ pub fn init(cx: &mut App) {
ToolRegistry::default_global(cx);
}
/// The result of running a tool, containing both the asynchronous output
/// and an optional card view that can be rendered immediately.
pub struct ToolResult {
/// The asynchronous task that will eventually resolve to the tool's output
pub output: Task<Result<String>>,
/// An optional view to present the output of the tool.
pub card: Option<AnyView>,
}
impl From<Task<Result<String>>> for ToolResult {
/// Convert from a task to a ToolResult with no card
fn from(output: Task<Result<String>>) -> Self {
Self { output, card: None }
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum ToolSource {
/// A native tool built-in to Zed.
@@ -66,7 +82,7 @@ pub trait Tool: 'static + Send + Sync {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>>;
) -> ToolResult;
}
impl Debug for dyn Tool {

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::io::BufReader;
use futures::{AsyncBufReadExt, AsyncReadExt};
use gpui::{App, AppContext, Entity, Task};
@@ -76,10 +76,10 @@ impl Tool for BashTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input: BashToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let project = project.read(cx);
@@ -90,13 +90,15 @@ impl Tool for BashTool {
let only_worktree = match worktrees.next() {
Some(worktree) => worktree,
None => return Task::ready(Err(anyhow!("No worktrees found in the project"))),
None => {
return Task::ready(Err(anyhow!("No worktrees found in the project"))).into();
}
};
if worktrees.next().is_some() {
return Task::ready(Err(anyhow!(
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly."
)));
))).into();
}
only_worktree.read(cx).abs_path()
@@ -108,7 +110,8 @@ impl Tool for BashTool {
{
return Task::ready(Err(anyhow!(
"The absolute path must be within one of the project's worktrees"
)));
)))
.into();
}
input_path.into()
@@ -117,13 +120,15 @@ impl Tool for BashTool {
return Task::ready(Err(anyhow!(
"`cd` directory {} not found in the project",
&input.cd
)));
)))
.into();
};
worktree.read(cx).abs_path()
};
cx.background_spawn(run_command_limited(working_dir, input.command))
.into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
use futures::future::join_all;
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -219,14 +219,14 @@ impl Tool for BatchTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<BatchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
if input.invocations.is_empty() {
return Task::ready(Err(anyhow!("No tool invocations provided")));
return Task::ready(Err(anyhow!("No tool invocations provided"))).into();
}
let run_tools_concurrently = input.run_tools_concurrently;
@@ -257,11 +257,11 @@ impl Tool for BatchTool {
let project = project.clone();
let action_log = action_log.clone();
let messages = messages.clone();
let task = cx
let tool_result = cx
.update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
tasks.push(task);
tasks.push(tool_result.output);
}
Ok((tasks, tool_names))
@@ -305,6 +305,6 @@ impl Tool for BatchTool {
}
Ok(formatted_results.trim().to_string())
})
}).into()
}
}

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use assistant_tool::{ActionLog, Tool, ToolResult};
use collections::IndexMap;
use gpui::{App, AsyncApp, Entity, Task};
use language::{OutlineItem, ParseStatus, Point};
@@ -129,10 +129,10 @@ impl Tool for CodeSymbolsTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let regex = match input.regex {
@@ -141,7 +141,7 @@ impl Tool for CodeSymbolsTool {
.build()
{
Ok(regex) => Some(regex),
Err(err) => return Task::ready(Err(anyhow!("Invalid regex: {err}"))),
Err(err) => return Task::ready(Err(anyhow!("Invalid regex: {err}"))).into(),
},
None => None,
};
@@ -149,7 +149,7 @@ impl Tool for CodeSymbolsTool {
cx.spawn(async move |cx| match input.path {
Some(path) => file_outline(project, path, action_log, regex, input.offset, cx).await,
None => project_symbols(project, regex, input.offset, cx).await,
})
}).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -77,10 +77,10 @@ impl Tool for CopyPathTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<CopyPathToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let copy_task = project.update(cx, |project, cx| {
match project
@@ -116,6 +116,6 @@ impl Tool for CopyPathTool {
err
)),
}
})
}).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -68,14 +68,14 @@ impl Tool for CreateDirectoryTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
Some(project_path) => project_path,
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))).into(),
};
let destination_path: Arc<str> = input.path.as_str().into();
@@ -88,6 +88,6 @@ impl Tool for CreateDirectoryTool {
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
Ok(format!("Created directory {destination_path}"))
})
}).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -73,14 +73,14 @@ impl Tool for CreateFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
Some(project_path) => project_path,
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))).into(),
};
let contents: Arc<str> = input.contents.as_str().into();
let destination_path: Arc<str> = input.path.as_str().into();
@@ -105,6 +105,6 @@ impl Tool for CreateFileTool {
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
Ok(format!("Created file {destination_path}"))
})
}).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -63,15 +63,15 @@ impl Tool for DeletePathTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
Ok(input) => input.path,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(project_path) = project.read(cx).find_project_path(&path_str, cx) else {
return Task::ready(Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
)));
))).into();
};
let Some(worktree) = project
@@ -80,7 +80,7 @@ impl Tool for DeletePathTool {
else {
return Task::ready(Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
)));
))).into();
};
let worktree_snapshot = worktree.read(cx).snapshot();
@@ -131,6 +131,6 @@ impl Tool for DeletePathTool {
"Couldn't delete {path_str} because that path isn't in this project."
)),
}
})
}).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -83,14 +83,14 @@ impl Tool for DiagnosticsTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
match serde_json::from_value::<DiagnosticsToolInput>(input)
.ok()
.and_then(|input| input.path)
{
Some(path) if !path.is_empty() => {
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
return Task::ready(Err(anyhow!("Could not find path {path} in project",))).into();
};
let buffer =
@@ -124,7 +124,7 @@ impl Tool for DiagnosticsTool {
} else {
Ok(output)
}
})
}).into()
}
_ => {
let project = project.read(cx);
@@ -155,9 +155,9 @@ impl Tool for DiagnosticsTool {
});
if has_diagnostics {
Task::ready(Ok(output))
Task::ready(Ok(output)).into()
} else {
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
Task::ready(Ok("No errors or warnings found in the project.".to_string())).into()
}
}
}

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::AsyncReadExt as _;
use gpui::{App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
@@ -146,10 +146,10 @@ impl Tool for FetchTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<FetchToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let text = cx.background_spawn({
@@ -165,6 +165,6 @@ impl Tool for FetchTool {
}
Ok(text)
})
}).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::{Context as _, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -159,10 +159,10 @@ impl Tool for FindReplaceFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
cx.spawn(async move |cx: &mut AsyncApp| {
@@ -253,6 +253,6 @@ impl Tool for FindReplaceFileTool {
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
})
}).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -77,10 +77,10 @@ impl Tool for ListDirectoryTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
// Sometimes models will return these even though we tell it to give a path and not a glob.
@@ -101,26 +101,26 @@ impl Tool for ListDirectoryTool {
.collect::<Vec<_>>()
.join("\n");
return Task::ready(Ok(output));
return Task::ready(Ok(output)).into();
}
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path {} not found in project", input.path)));
return Task::ready(Err(anyhow!("Path {} not found in project", input.path))).into();
};
let Some(worktree) = project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("Worktree not found")));
return Task::ready(Err(anyhow!("Worktree not found"))).into();
};
let worktree = worktree.read(cx);
let Some(entry) = worktree.entry_for_path(&project_path.path) else {
return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
};
if !entry.is_dir() {
return Task::ready(Err(anyhow!("{} is not a directory.", input.path)));
return Task::ready(Err(anyhow!("{} is not a directory.", input.path))).into();
}
let mut output = String::new();
@@ -133,8 +133,8 @@ impl Tool for ListDirectoryTool {
.unwrap();
}
if output.is_empty() {
return Task::ready(Ok(format!("{} is empty.", input.path)));
return Task::ready(Ok(format!("{} is empty.", input.path))).into();
}
Task::ready(Ok(output))
Task::ready(Ok(output)).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -90,10 +90,10 @@ impl Tool for MovePathTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<MovePathToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let rename_task = project.update(cx, |project, cx| {
match project
@@ -127,6 +127,6 @@ impl Tool for MovePathTool {
err
)),
}
})
}).into()
}
}

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult};
use chrono::{Local, Utc};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -60,10 +60,10 @@ impl Tool for NowTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input: NowToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let now = match input.timezone {
@@ -72,6 +72,6 @@ impl Tool for NowTool {
};
let text = format!("The current datetime is {now}.");
Task::ready(Ok(text))
Task::ready(Ok(text)).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::{Context as _, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -53,16 +53,16 @@ impl Tool for OpenTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input: OpenToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
cx.background_spawn(async move {
open::that(&input.path_or_url).context("Failed to open URL or file path")?;
Ok(format!("Successfully opened {}", input.path_or_url))
})
}).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -71,10 +71,10 @@ impl Tool for PathSearchTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {
Ok(input) => (input.offset, input.glob),
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let path_matcher = match PathMatcher::new([
@@ -82,7 +82,7 @@ impl Tool for PathSearchTool {
if glob.is_empty() { "*" } else { &glob },
]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))).into(),
};
let snapshots: Vec<Snapshot> = project
.read(cx)
@@ -136,6 +136,6 @@ impl Tool for PathSearchTool {
Ok(response)
}
})
}).into()
}
}

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -88,14 +88,14 @@ impl Tool for ReadFileTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path,)));
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path,))).into();
};
let file_path = input.path.clone();
@@ -146,6 +146,6 @@ impl Tool for ReadFileTool {
Ok(format!("This file was too big to read all at once. Here is an outline of its symbols:\n\n{outline}\n\nUsing the line numbers in this outline, you can call this tool again while specifying the start_line and end_line fields to see the implementations of symbols in the outline."))
}
}
})
}).into()
}
}

View File

@@ -1,6 +1,6 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::StreamExt;
use gpui::{App, Entity, Task};
use language::OffsetRangeExt;
@@ -92,13 +92,13 @@ impl Tool for RegexSearchTool {
project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> 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))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let query = match SearchQuery::regex(
@@ -106,17 +106,18 @@ impl Tool for RegexSearchTool {
false,
case_sensitive,
false,
false,
PathMatcher::default(),
PathMatcher::default(),
None,
) {
Ok(query) => query,
Err(error) => return Task::ready(Err(error)),
Err(error) => return Task::ready(Err(error)).into(),
};
let results = project.update(cx, |project, cx| project.search(query, cx));
cx.spawn(async move|cx| {
let output = cx.spawn(async move|cx| {
futures::pin_mut!(results);
let mut output = String::new();
@@ -200,6 +201,7 @@ impl Tool for RegexSearchTool {
} else {
Ok(format!("Found {matches_found} matches:\n{output}"))
}
})
});
output.into()
}
}

View File

@@ -1,5 +1,5 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::{Context as _, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AsyncApp, Entity, Task};
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -122,10 +122,10 @@ impl Tool for SymbolInfoTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
cx.spawn(async move |cx| {
@@ -205,7 +205,7 @@ impl Tool for SymbolInfoTool {
} else {
Ok(output)
}
})
}).into()
}
}

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool};
use anyhow::anyhow;
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -51,11 +51,11 @@ impl Tool for ThinkingTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
// This tool just "thinks out loud" and doesn't perform any actions.
Task::ready(match serde_json::from_value::<ThinkingToolInput>(input) {
Ok(_input) => Ok("Finished thinking.".to_string()),
Err(err) => Err(anyhow!(err)),
})
}).into()
}
}

View File

@@ -34,6 +34,7 @@ static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
false,
false,
false,
false,
Default::default(),
Default::default(),
None,

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::{Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolSource};
use anyhow::{anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
use gpui::{App, Entity, Task};
use icons::IconName;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -76,7 +76,7 @@ impl Tool for ContextServerTool {
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
cx: &mut App,
) -> Task<Result<String>> {
) -> ToolResult {
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
let tool_name = self.tool.name.clone();
let server_clone = server.clone();
@@ -115,9 +115,9 @@ impl Tool for ContextServerTool {
}
}
Ok(result)
})
}).into()
} else {
Task::ready(Err(anyhow!("Context server not found")))
Task::ready(Err(anyhow!("Context server not found"))).into()
}
}
}

View File

@@ -6365,20 +6365,9 @@ impl Editor {
breakpoint_display_points
}
fn edit_breakpoint_context_menu(
fn breakpoint_context_menu(
&self,
anchor: Anchor,
window: &Window,
cx: &mut Context<Self>,
) -> Entity<ui::ContextMenu> {
todo!()
}
fn basic_breakpoint_context_menu(
&self,
anchor: Anchor,
edit_menu_position: (Anchor, gpui::Point<Pixels>),
is_edit_menu: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ui::ContextMenu> {
@@ -6396,6 +6385,12 @@ impl Editor {
.breakpoint_at_row(row, window, cx)
.map(|(anchor, bp)| (anchor, Arc::from(bp)));
let log_breakpoint_msg = if breakpoint.as_ref().is_some_and(|bp| bp.1.message.is_some()) {
"Edit Log Breakpoint"
} else {
"Set Log Breakpoint"
};
let condition_breakpoint_msg = if breakpoint
.as_ref()
.is_some_and(|bp| bp.1.condition.is_some())
@@ -6423,13 +6418,9 @@ impl Editor {
let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
.map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
let breakpoint_is_at_row = breakpoint.is_some();
let log_editor = cx.new(|cx| {
let mut log_editor = Editor::single_line(window, cx);
if let Some(text) = &breakpoint.as_ref().and_then(|bp| bp.1.message.clone()) {
log_editor.insert(text.as_ref(), window, cx);
}
log_editor
let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
BreakpointState::Enabled => Some("Disable"),
BreakpointState::Disabled => Some("Enable"),
});
let (anchor, breakpoint) =
@@ -6453,73 +6444,8 @@ impl Editor {
})
.separator()
})
.entry("edit breakpoint", None, {
let weak_editor = weak_editor.clone();
move |window, cx| {
weak_editor
.update(cx, |editor, cx| {
let context_menu =
editor.edit_breakpoint_context_menu(anchor, window, cx);
let (source, clicked_point) = edit_menu_position;
editor.mouse_context_menu = MouseContextMenu::pinned_to_editor(
editor,
source,
clicked_point,
context_menu,
window,
cx,
);
})
.ok();
}
})
.when(breakpoint_is_at_row, |this| {
let weak_editor = weak_editor.clone();
let breakpoint = breakpoint.clone();
this.custom_row(move |window, cx| {
let breakpoint = weak_editor
.update(cx, |editor, cx| {
editor.breakpoint_at_row(row, window, cx).map(|bp| bp.1)
})
.ok()
.flatten()
.unwrap_or_else(|| breakpoint.as_ref().clone());
let is_enabled = match breakpoint.is_enabled() {
true => ToggleState::Selected,
false => ToggleState::Unselected,
};
ui::CheckboxWithLabel::new(
"enable-breakpoint",
Label::new("Enable"),
is_enabled,
{
let weak_editor = weak_editor.clone();
let breakpoint = breakpoint.clone();
move |_, _, cx| {
weak_editor
.update(cx, |this, cx| {
this.edit_breakpoint_at_anchor(
anchor,
breakpoint.clone(),
BreakpointEditAction::InvertState,
cx,
);
})
.log_err();
}
},
)
.checkbox_position(IconPosition::End)
.into_any_element()
})
})
.when(!breakpoint_is_at_row, |this| {
this.entry(set_breakpoint_msg, None, {
.when_some(toggle_state_msg, |this, msg| {
this.entry(msg, None, {
let weak_editor = weak_editor.clone();
let breakpoint = breakpoint.clone();
move |_window, cx| {
@@ -6528,7 +6454,7 @@ impl Editor {
this.edit_breakpoint_at_anchor(
anchor,
breakpoint.as_ref().clone(),
BreakpointEditAction::Toggle,
BreakpointEditAction::InvertState,
cx,
);
})
@@ -6536,72 +6462,69 @@ impl Editor {
}
})
})
.when(is_edit_menu, |this| {
this.custom_entry(
{
breakpoint_message_editor_element(
anchor,
breakpoint.clone(),
BreakpointMessageKind::Log,
log_editor.clone(),
weak_editor.clone(),
)
},
{
let breakpoint = breakpoint.clone();
let weak_editor = weak_editor.clone();
let log_editor = log_editor.clone();
move |_, cx| {
let log_message = log_editor.read(cx).text(cx);
weak_editor
.update(cx, |this, cx| {
this.edit_breakpoint_at_anchor(
anchor,
breakpoint.as_ref().clone(),
BreakpointEditAction::EditLogMessage(
log_message.into(),
),
cx,
);
})
.ok();
}
},
)
.entry(set_breakpoint_msg, None, {
let weak_editor = weak_editor.clone();
let breakpoint = breakpoint.clone();
move |_window, cx| {
weak_editor
.update(cx, |this, cx| {
this.edit_breakpoint_at_anchor(
anchor,
breakpoint.as_ref().clone(),
BreakpointEditAction::Toggle,
cx,
);
})
.log_err();
}
})
.entry(log_breakpoint_msg, None, {
let breakpoint = breakpoint.clone();
let weak_editor = weak_editor.clone();
move |window, cx| {
weak_editor
.update(cx, |this, cx| {
this.add_edit_breakpoint_block(
anchor,
breakpoint.as_ref(),
BreakpointPromptEditAction::Log,
window,
cx,
);
})
.log_err();
}
})
.entry(condition_breakpoint_msg, None, {
let breakpoint = breakpoint.clone();
let weak_editor = weak_editor.clone();
move |window, cx| {
weak_editor
.update(cx, |this, cx| {
this.add_edit_breakpoint_block(
anchor,
breakpoint.as_ref(),
BreakpointPromptEditAction::Condition,
window,
cx,
);
})
.log_err();
}
})
.entry(hit_condition_breakpoint_msg, None, move |window, cx| {
weak_editor
.update(cx, |this, cx| {
this.add_edit_breakpoint_block(
anchor,
breakpoint.as_ref(),
BreakpointPromptEditAction::HitCondition,
window,
cx,
);
})
.log_err();
})
// .entry(condition_breakpoint_msg, None, {
// let breakpoint = breakpoint.clone();
// let weak_editor = weak_editor.clone();
// move |window, cx| {
// weak_editor
// .update(cx, |this, cx| {
// this.add_edit_breakpoint_block(
// anchor,
// breakpoint.as_ref(),
// BreakpointPromptEditAction::Condition,
// window,
// cx,
// );
// })
// .log_err();
// }
// })
// .entry(hit_condition_breakpoint_msg, None, move |window, cx| {
// weak_editor
// .update(cx, |this, cx| {
// this.add_edit_breakpoint_block(
// anchor,
// breakpoint.as_ref(),
// BreakpointPromptEditAction::HitCondition,
// window,
// cx,
// );
// })
// .log_err();
// })
})
}
@@ -8822,48 +8745,6 @@ impl Editor {
}
}
fn render_breakpoint_menu_editor(
editor: &Entity<Editor>,
window: &mut Window,
cx: &App,
) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let theme = cx.theme();
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: AbsoluteLength::Rems(Rems(0.75)),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
background_color: Some(theme.colors().editor_background),
..Default::default()
};
let element = EditorElement::new(
editor,
EditorStyle {
background: theme.colors().editor_background,
local_player: theme.players().local(),
text: text_style,
..Default::default()
},
);
div()
.rounded_sm()
.when(
editor.focus_handle(cx).contains_focused(window, cx),
|this| this.border_color(theme.colors().border_focused),
)
.child(element)
.min_w(Pixels(300.0))
.size_full()
.bg(theme.colors().editor_background)
.h_4()
}
fn set_breakpoint_context_menu(
&mut self,
display_row: DisplayRow,
@@ -8881,13 +8762,7 @@ impl Editor {
.snapshot(cx)
.anchor_before(Point::new(display_row.0, 0u32));
let context_menu = self.basic_breakpoint_context_menu(
position.unwrap_or(source),
(source, clicked_point),
false,
window,
cx,
);
let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx);
self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
self,
@@ -18134,58 +18009,6 @@ impl Editor {
}
}
enum BreakpointMessageKind {
Log,
Conditional,
HitConditional,
}
fn breakpoint_message_editor_element(
anchor: Anchor,
breakpoint: Arc<Breakpoint>,
message_kind: BreakpointMessageKind,
message_editor: Entity<Editor>,
weak_editor: WeakEntity<Editor>,
) -> impl Fn(&mut Window, &mut App) -> AnyElement {
let log_editor = message_editor.clone();
let breakpoint = breakpoint.clone();
let weak_editor = weak_editor.clone();
move |window, cx| {
let label = Label::new("Log message");
let log_editor = log_editor.clone();
let breakpoint = breakpoint.clone();
let weak_editor = weak_editor.clone();
h_flex()
.gap_1()
.size_full()
.child(label)
.child(Editor::render_breakpoint_menu_editor(
&log_editor.clone(),
window,
cx,
))
.on_action(move |_: &menu::Confirm, _, cx| {
let log_message = log_editor.read(cx).text(cx);
weak_editor
.update(cx, |this, cx| {
this.edit_breakpoint_at_anchor(
anchor,
breakpoint.as_ref().clone(),
BreakpointEditAction::EditLogMessage(log_message.into()),
cx,
);
this.mouse_context_menu = None;
})
.ok();
})
.into_any_element()
}
}
// Consider user intent and default settings
fn choose_completion_range(
completion: &Completion,

View File

@@ -3890,15 +3890,7 @@ impl EditorElement {
)
})?;
element.prepaint_as_root(
position,
Size {
width: AvailableSpace::Definite(Pixels(100.)),
height: AvailableSpace::MinContent,
},
window,
cx,
);
element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx);
Some(element)
})
}

View File

@@ -1540,8 +1540,24 @@ impl SearchableItem for Editor {
let text = self.buffer.read(cx);
let text = text.snapshot(cx);
let mut edits = vec![];
let mut last_point: Option<Point> = None;
for m in matches {
let point = m.start.to_point(&text);
let text = text.text_for_range(m.clone()).collect::<Vec<_>>();
// Check if the row for the current match is different from the last
// match. If that's not the case and we're still replacing matches
// in the same row/line, skip this match if the `one_match_per_line`
// option is enabled.
if last_point.is_none() {
last_point = Some(point);
} else if last_point.is_some() && point.row != last_point.unwrap().row {
last_point = Some(point);
} else if query.one_match_per_line().is_some_and(|enabled| enabled) {
continue;
}
let text: Cow<_> = if text.len() == 1 {
text.first().cloned().unwrap().into()
} else {

View File

@@ -1,4 +1,4 @@
use super::{MacDisplay, NSRange, NSStringExt, ns_string, renderer};
use super::{BoolExt, MacDisplay, NSRange, NSStringExt, ns_string, renderer};
use crate::{
AnyWindowHandle, Bounds, DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor,
KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
@@ -1021,11 +1021,8 @@ impl PlatformWindow for MacWindow {
} else {
0
};
let opaque = if background_appearance == WindowBackgroundAppearance::Opaque {
YES
} else {
NO
};
let opaque = (background_appearance == WindowBackgroundAppearance::Opaque).to_objc();
unsafe {
this.native_window.setOpaque_(opaque);
// Shadows for transparent windows cause artifacts and performance issues
@@ -1981,14 +1978,11 @@ extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL {
let window_state = unsafe { get_window_state(this) };
let position = drag_event_position(&window_state, dragging_info);
if send_new_event(
send_new_event(
&window_state,
PlatformInput::FileDrop(FileDropEvent::Submit { position }),
) {
YES
} else {
NO
}
)
.to_objc()
}
fn external_paths_from_event(dragging_info: *mut Object) -> Option<ExternalPaths> {

View File

@@ -424,6 +424,8 @@ impl LocalLspStore {
let mut binary = binary_result?;
let mut shell_env = delegate.shell_env().await;
shell_env.extend(binary.env.unwrap_or_default());
if let Some(settings) = settings {
if let Some(arguments) = settings.arguments {
binary.arguments = arguments.into_iter().map(Into::into).collect();

View File

@@ -334,6 +334,10 @@ impl ProjectPath {
path: Path::new("").into(),
}
}
pub fn starts_with(&self, other: &ProjectPath) -> bool {
self.worktree_id == other.worktree_id && self.path.starts_with(&other.path)
}
}
#[derive(Debug, Default)]

View File

@@ -71,6 +71,7 @@ pub enum SearchQuery {
whole_word: bool,
case_sensitive: bool,
include_ignored: bool,
one_match_per_line: bool,
inner: SearchInputs,
},
}
@@ -116,6 +117,7 @@ impl SearchQuery {
whole_word: bool,
case_sensitive: bool,
include_ignored: bool,
one_match_per_line: bool,
files_to_include: PathMatcher,
files_to_exclude: PathMatcher,
buffers: Option<Vec<Entity<Buffer>>>,
@@ -156,6 +158,7 @@ impl SearchQuery {
case_sensitive,
include_ignored,
inner,
one_match_per_line,
})
}
@@ -166,6 +169,7 @@ impl SearchQuery {
message.whole_word,
message.case_sensitive,
message.include_ignored,
false,
deserialize_path_matches(&message.files_to_include)?,
deserialize_path_matches(&message.files_to_exclude)?,
None, // search opened only don't need search remote
@@ -459,6 +463,19 @@ impl SearchQuery {
Self::Regex { inner, .. } | Self::Text { inner, .. } => inner,
}
}
/// Whether this search should replace only one match per line, instead of
/// all matches.
/// Returns `None` for text searches, as only regex searches support this
/// option.
pub fn one_match_per_line(&self) -> Option<bool> {
match self {
Self::Regex {
one_match_per_line, ..
} => Some(*one_match_per_line),
Self::Text { .. } => None,
}
}
}
pub fn deserialize_path_matches(glob_set: &str) -> anyhow::Result<PathMatcher> {

View File

@@ -1231,6 +1231,8 @@ impl BufferSearchBar {
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
false,
self.search_options
.contains(SearchOptions::ONE_MATCH_PER_LINE),
Default::default(),
Default::default(),
None,

View File

@@ -1053,6 +1053,8 @@ impl ProjectSearchView {
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
self.search_options
.contains(SearchOptions::ONE_MATCH_PER_LINE),
included_files,
excluded_files,
open_buffers,

View File

@@ -48,6 +48,7 @@ bitflags! {
const CASE_SENSITIVE = 0b010;
const INCLUDE_IGNORED = 0b100;
const REGEX = 0b1000;
const ONE_MATCH_PER_LINE = 0b100000;
/// If set, reverse direction when finding the active match
const BACKWARDS = 0b10000;
}

View File

@@ -445,6 +445,8 @@ impl Vim {
}
let vim = cx.entity().clone();
pane.update(cx, |pane, cx| {
let mut options = SearchOptions::REGEX;
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
return;
};
@@ -453,7 +455,6 @@ impl Vim {
return None;
}
let mut options = SearchOptions::REGEX;
if replacement.is_case_sensitive {
options.set(SearchOptions::CASE_SENSITIVE, true)
}
@@ -468,6 +469,11 @@ impl Vim {
search_bar.is_contains_uppercase(&search),
);
}
if !replacement.should_replace_all {
options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
}
search_bar.set_replacement(Some(&replacement.replacement), cx);
Some(search_bar.search(&search, Some(options), window, cx))
});
@@ -476,29 +482,35 @@ impl Vim {
cx.spawn_in(window, async move |_, cx| {
search.await?;
search_bar.update_in(cx, |search_bar, window, cx| {
if replacement.should_replace_all {
search_bar.select_last_match(window, cx);
search_bar.replace_all(&Default::default(), window, cx);
cx.spawn(async move |_, cx| {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
editor
.update(cx, |editor, cx| editor.clear_search_within_ranges(cx))
.ok();
})
.detach();
vim.update(cx, |vim, cx| {
vim.move_cursor(
Motion::StartOfLine {
display_lines: false,
},
None,
window,
cx,
)
});
}
search_bar.select_last_match(window, cx);
search_bar.replace_all(&Default::default(), window, cx);
cx.spawn(async move |_, cx| {
cx.background_executor()
.timer(Duration::from_millis(200))
.await;
editor
.update(cx, |editor, cx| editor.clear_search_within_ranges(cx))
.ok();
})
.detach();
vim.update(cx, |vim, cx| {
vim.move_cursor(
Motion::StartOfLine {
display_lines: false,
},
None,
window,
cx,
)
});
// Disable the `ONE_MATCH_PER_LINE` search option when finished, as
// this is not properly supported outside of vim mode, and
// not disabling it makes the "Replace All Matches" button
// actually replace only the first match on each line.
options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
search_bar.set_search_options(options, cx);
})?;
anyhow::Ok(())
})
@@ -564,15 +576,16 @@ impl Replacement {
let mut replacement = Replacement {
search,
replacement,
should_replace_all: true,
should_replace_all: false,
is_case_sensitive: true,
};
for c in flags.chars() {
match c {
'g' | 'I' => {}
'g' => replacement.should_replace_all = true,
'c' | 'n' => replacement.should_replace_all = false,
'i' => replacement.is_case_sensitive = false,
'I' => replacement.is_case_sensitive = true,
_ => {}
}
}

View File

@@ -1155,8 +1155,7 @@ mod test {
fox ˇjumps over
the lazy dog"})
.await;
cx.simulate_shared_keystrokes("shift-v shift-4 shift-y")
.await;
cx.simulate_shared_keystrokes("shift-v $ shift-y").await;
cx.shared_state().await.assert_eq(indoc! {"
The quick brown
ˇfox jumps over

View File

@@ -36,7 +36,7 @@
{"ReadRegister":{"name":"\"","value":"fox jumps over\nthe lazy dog\n"}}
{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Key":"shift-4"}
{"Key":"$"}
{"Key":"shift-y"}
{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Normal"}}
{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}}

View File

@@ -1172,6 +1172,31 @@ impl Worktree {
pub fn is_single_file(&self) -> bool {
self.root_dir().is_none()
}
/// For visible worktrees, returns the path with the worktree name as the first component.
/// Otherwise, returns an absolute path.
pub fn full_path(&self, worktree_relative_path: &Path) -> PathBuf {
let mut full_path = PathBuf::new();
if self.is_visible() {
full_path.push(self.root_name());
} else {
let path = self.abs_path();
if self.is_local() && path.starts_with(home_dir().as_path()) {
full_path.push("~");
full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
} else {
full_path.push(path)
}
}
if worktree_relative_path.components().next().is_some() {
full_path.push(&worktree_relative_path);
}
full_path
}
}
impl LocalWorktree {
@@ -3229,27 +3254,7 @@ impl language::File for File {
}
fn full_path(&self, cx: &App) -> PathBuf {
let mut full_path = PathBuf::new();
let worktree = self.worktree.read(cx);
if worktree.is_visible() {
full_path.push(worktree.root_name());
} else {
let path = worktree.abs_path();
if worktree.is_local() && path.starts_with(home_dir().as_path()) {
full_path.push("~");
full_path.push(path.strip_prefix(home_dir().as_path()).unwrap());
} else {
full_path.push(path)
}
}
if self.path.components().next().is_some() {
full_path.push(&self.path);
}
full_path
self.worktree.read(cx).full_path(&self.path)
}
/// Returns the last component of this handle's absolute path. If this handle refers to the root

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.183.0"
version = "0.182.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -166,6 +166,7 @@ Zed's vim mode includes some features that are usually provided by very popular
- You can add key bindings to your keymap to navigate "camelCase" names. [Head down to the Optional key bindings](#optional-key-bindings) section to learn how.
- You can use `gr` to do [ReplaceWithRegister](https://github.com/vim-scripts/ReplaceWithRegister).
- You can use `cx` for [vim-exchange](https://github.com/tommcdo/vim-exchange) functionality. Note that it does not have a default binding in visual mode, but you can add one to your keymap (refer to the [optional key bindings](#optional-key-bindings) section).
- You can navigate to indent depths relative to your cursor with the [indent wise](https://github.com/jeetsukumaran/vim-indentwise) plugin `[-`, `]-`, `[+`, `]+`, `[=`, `]=`.
## Command palette
@@ -199,6 +200,7 @@ This table shows commands for managing windows, tabs, and panes. As commands don
| `:tabn[ext]` | Go to the next tab |
| `:tabp[rev]` | Go to previous tab |
| `:tabc[lose]` | Close the current tab |
| `:ls` | Show all buffers |
> **Note:** The `!` character is used to force the command to execute without saving changes or prompting before overwriting a file.
@@ -267,6 +269,16 @@ These commands help you edit text.
| `:s[ort] [i]` | Sort the current selection (with i, case-insensitively) |
| `:y[ank]` | Yank (copy) the current selection or line |
### Set
These commands modify editor options locally for the current buffer.
| Command | Description |
| ------------------------------- | --------------------------------------------------------------------------------------------- |
| `:se[t] [no]wrap` | Lines longer than the width of the window will wrap and displaying continues on the next line |
| `:se[t] [no]nu[mber]` | Print the line number in front of each line |
| `:se[t] [no]r[elative]nu[mber]` | Changes the displayed number to be relative to the cursor |
### Command mnemonics
As any Zed command is available, you may find that it's helpful to remember mnemonics that run the correct command. For example:
@@ -498,6 +510,7 @@ Here's an example of these settings changed:
```json
{
"vim": {
"default_mode": "insert",
"use_system_clipboard": "never",
"use_multiline_find": true,
"use_smartcase_find": true,