Compare commits

..

13 Commits

Author SHA1 Message Date
Joseph T. Lyons
1d0b4df24f zed 0.187.3 2025-05-16 16:26:22 +02:00
Remco Smits
2c861184fa debugger: Prevent pane context menu from showing on secondary mouse click in list entries (#30781)
This PR prevents the debug panel pane context menu from showing when you
click your secondary mouse button in **stackframe**, **breakpoint** and
**module** list entries.

Release Notes:

- N/A
2025-05-16 16:24:41 +02:00
Anthony Eid
7c0c5bd516 debugger: Fix inline values panic when selecting stack frames (#30821)
Release Notes:

- debugger beta: Fix panic that could occur when selecting a stack frame
- debugger beta: Fix inline values not showing in stack trace view

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-16 16:24:41 +02:00
Joseph T. Lyons
8f9b217f79 zed 0.187.2 2025-05-15 12:44:38 +02:00
Cole Miller
de30643e74 debugger: Make the stack frame list and module list keyboard-navigable (#30682)
- Switch stack frame list and module list to `UniformList` to access
scrolling behavior
- Implement `menu::` navigation actions

Release Notes:

- Debugger Beta: Added support for menu navigation actions (`ctrl-n`,
`ctrl-p`, etc.) in the stack frame list and module list.
2025-05-15 11:53:55 +02:00
gcp-cherry-pick-bot[bot]
235fd06adc workspace: Remove default keybindings for close active dock (cherry-pick #30691) (#30736)
Cherry-picked workspace: Remove default keybindings for close active
dock (#30691)

Release Notes:

- N/A

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-15 11:53:00 +02:00
gcp-cherry-pick-bot[bot]
c408200f9b Fix rejecting overwritten files if the agent previously edited them (cherry-pick #30744) (#30745)
Cherry-picked Fix rejecting overwritten files if the agent previously
edited them (#30744)

Release Notes:

- Fixed rejecting overwritten files if the agent had previously edited
them.

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-05-15 11:48:18 +02:00
Joseph T. Lyons
53faf0d78d zed 0.187.1 2025-05-14 16:15:22 +02:00
Nate Butler
f2050dfe2b debugger: Tidy up dropdown menus (#30679)
Before
![CleanShot 2025-05-14 at 13 22
44@2x](https://github.com/user-attachments/assets/c6c06c5c-571d-4913-a691-161f44bba27c)

After
![CleanShot 2025-05-14 at 13 22
17@2x](https://github.com/user-attachments/assets/0a25a053-81a3-4b96-8963-4b770b1e5b45)

Release Notes:

- N/A
2025-05-14 16:13:53 +02:00
Danilo Leal
4cfc49e2d4 agent: Fix Markdown codeblock header buttons (#30645)
Closes https://github.com/zed-industries/zed/issues/30592

Release Notes:

- agent: Fixed Markdown codeblock header buttons being pushed by long
paths/file names.
2025-05-14 16:00:01 +02:00
Oleksiy Syvokon
72426a9608 agent: Fix tool use in Gemini (#30689)
Thread doesn't run pending tools when `stop_reason` is not `ToolUse`.
Perhaps we should change that so that it always runs pending tools if
there are some, but for now this change just fixes setting `stop_reason`
for Google models.

Release Notes:

- N/A
2025-05-14 15:59:18 +02:00
Oleksiy Syvokon
1c638a1309 agent: Don't duplicate recommended models in all models list (#30692)
Release Notes:

- N/A
2025-05-14 15:58:58 +02:00
Joseph T. Lyons
249597a4a8 v0.187.x preview 2025-05-14 12:18:41 +02:00
545 changed files with 7131 additions and 13077 deletions

View File

@@ -13,6 +13,12 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-args=-all_load"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-args=-all_load"]
[target.'cfg(target_os = "windows")']
rustflags = [
"--cfg",

View File

@@ -30,7 +30,3 @@ ffdda588b41f7d9d270ffe76cab116f828ad545e
# 2024-07-05 Improved formatting of default keymaps (single line per bind)
# https://github.com/zed-industries/zed/pull/13887
813cc3f5e537372fc86720b5e71b6e1c815440ab
# 2024-07-24 docs: Format docs
# https://github.com/zed-industries/zed/pull/15352
3a44a59f8ec114ac1ba22f7da1652717ef7e4e5c

View File

@@ -29,8 +29,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -29,8 +29,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -28,8 +28,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -28,8 +28,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -49,8 +49,8 @@ body:
attributes:
label: Zed Version and System Specs
description: |
Open Zed, from the command palette select "zed: copy system specs into clipboard"
Open Zed, from the command palette select "zed: Copy System Specs Into Clipboard"
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true

View File

@@ -26,9 +26,9 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true
- type: textarea

View File

@@ -19,8 +19,6 @@ amtoaer <amtoaer@gmail.com>
amtoaer <amtoaer@gmail.com> <amtoaer@outlook.com>
Andrei Zvonimir Crnković <andrei@0x7f.dev>
Andrei Zvonimir Crnković <andrei@0x7f.dev> <andreicek@0x7f.dev>
Angelk90 <angelo.k90@hotmail.it>
Angelk90 <angelo.k90@hotmail.it> <20476002+Angelk90@users.noreply.github.com>
Antonio Scandurra <me@as-cii.com>
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Ben Kunkle <ben@zed.dev>
@@ -40,8 +38,6 @@ Dairon Medina <dairon.medina@gmail.com>
Danilo Leal <danilo@zed.dev>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Edwin Aronsson <75266237+4teapo@users.noreply.github.com>
Elvis Pranskevichus <elvis@geldata.com>
Elvis Pranskevichus <elvis@geldata.com> <elvis@magic.io>
Evren Sen <nervenes@icloud.com>
Evren Sen <nervenes@icloud.com> <146845123+evrensen467@users.noreply.github.com>
Evren Sen <nervenes@icloud.com> <146845123+evrsen@users.noreply.github.com>
@@ -73,8 +69,6 @@ Lilith Iris <itslirissama@gmail.com> <83819417+Irilith@users.noreply.github.com>
LoganDark <contact@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
Marko Kungla <marko.kungla@gmail.com>
Marko Kungla <marko.kungla@gmail.com> <marko@mkungla.dev>
Marshall Bowers <git@maxdeviant.com>
Marshall Bowers <git@maxdeviant.com> <elliott.codes@gmail.com>
Marshall Bowers <git@maxdeviant.com> <marshall@zed.dev>
@@ -90,7 +84,6 @@ Michael Sloan <michael@zed.dev> <mgsloan@google.com>
Mikayla Maki <mikayla@zed.dev>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
Morgan Krey <morgan@zed.dev>
Muhammad Talal Anwar <mail@talal.io>
Muhammad Talal Anwar <mail@talal.io> <talalanwar@outlook.com>
Nate Butler <iamnbutler@gmail.com>
@@ -123,18 +116,11 @@ Shish <webmaster@shishnet.org>
Shish <webmaster@shishnet.org> <shish@shishnet.org>
Smit Barmase <0xtimsb@gmail.com>
Smit Barmase <0xtimsb@gmail.com> <smit@zed.dev>
Thomas <github.thomaub@gmail.com>
Thomas <github.thomaub@gmail.com> <thomas.aubry94@gmail.com>
Thomas <github.thomaub@gmail.com> <thomas.aubry@paylead.fr>
Thomas Heartman <thomasheartman+github@gmail.com>
Thomas Heartman <thomasheartman+github@gmail.com> <thomas@getunleash.io>
Thomas Mickley-Doyle <tmickleydoyle@gmail.com>
Thomas Mickley-Doyle <tmickleydoyle@gmail.com> <thomas@zed.dev>
Thorben Kröger <dev@thorben.net>
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
Thorsten Ball <mrnugget@gmail.com>
Thorsten Ball <mrnugget@gmail.com> <me@thorstenball.com>
Thorsten Ball <mrnugget@gmail.com> <thorsten@zed.dev>
Thorsten Ball <thorsten@zed.dev>
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>
Tristan Hume <tris.hume@gmail.com>
Tristan Hume <tris.hume@gmail.com> <tristan@anthropic.com>
Uladzislau Kaminski <i@uladkaminski.com>

2
.rules
View File

@@ -115,7 +115,7 @@ Other entities can then register a callback to handle these events by doing `cx.
GPUI has had some changes to its APIs. Always write code using the new APIs:
* `spawn` methods now take async closures (`AsyncFn`), and so should be called like `cx.spawn(async move |cx| ...)`.
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which no longer exist and should NEVER be used.
* Use `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.

1494
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -74,12 +74,11 @@ members = [
"crates/inline_completion",
"crates/inline_completion_button",
"crates/install_cli",
"crates/jj",
"crates/jj_ui",
"crates/journal",
"crates/language",
"crates/language_extension",
"crates/language_model",
"crates/language_model_selector",
"crates/language_models",
"crates/language_selector",
"crates/language_tools",
@@ -280,12 +279,11 @@ indexed_docs = { path = "crates/indexed_docs" }
inline_completion = { path = "crates/inline_completion" }
inline_completion_button = { path = "crates/inline_completion_button" }
install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
jj_ui = { path = "crates/jj_ui" }
journal = { path = "crates/journal" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_model_selector = { path = "crates/language_model_selector" }
language_models = { path = "crates/language_models" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
@@ -428,7 +426,6 @@ convert_case = "0.8.0"
core-foundation = "0.10.0"
core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a016ba710191b9fdded28c8b042af4b617f7" }
dashmap = "6.0"
@@ -461,7 +458,6 @@ indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
jj-lib = { git = "https://github.com/jj-vcs/jj", rev = "e18eb8e05efaa153fad5ef46576af145bba1807f" }
jsonschema = "0.30.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -469,12 +465,12 @@ jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,r
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
linkme = "0.3.31"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
markup5ever_rcdom = "0.3.0"
metal = "0.29"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -553,9 +549,9 @@ syn = { version = "1.0.72", features = ["full", "extra-traits"] }
sys-locale = "0.3.1"
sysinfo = "0.31.0"
take-until = "0.2.0"
tempfile = "3.20.0"
tempfile = "3.9.0"
thiserror = "2.0.12"
tiktoken-rs = "0.7.0"
tiktoken-rs = "0.6.0"
time = { version = "0.3", features = [
"macros",
"parsing",
@@ -599,7 +595,6 @@ url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.3"
wasi-preview1-component-adapter-provider = "29"
wasm-encoder = "0.221"
wasmparser = "0.221"
wasmtime = { version = "29", default-features = false, features = [
@@ -608,8 +603,6 @@ wasmtime = { version = "29", default-features = false, features = [
"runtime",
"cranelift",
"component-model",
"incremental-cache",
"parallel-compilation",
] }
wasmtime-wasi = "29"
which = "6.0.0"
@@ -795,9 +788,6 @@ let_underscore_future = "allow"
# running afoul of the borrow checker.
too_many_arguments = "allow"
# We often have large enum variants yet we rarely actually bother with splitting them up.
large_enum_variant = "allow"
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",
@@ -805,6 +795,7 @@ ignored = [
"prost_build",
"serde",
"component",
"linkme",
"documented",
"workspace-hack",
]

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.87-bookworm as builder
FROM rust:1.86-bookworm as builder
WORKDIR app
COPY . .

View File

@@ -8,6 +8,10 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of
### Installation
<a href="https://repology.org/project/zed-editor/versions">
<img src="https://repology.org/badge/vertical-allrepos/zed-editor.svg?minversion=0.143.5" alt="Packaging status" align="right">
</a>
On macOS and Linux you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager).
Other platforms are not yet available:

View File

@@ -512,8 +512,6 @@
"alt-ctrl-o": "projects::OpenRecent",
"alt-shift-open": "projects::OpenRemote",
"alt-ctrl-shift-o": "projects::OpenRemote",
// Change to open path modal for existing remote connection by setting the parameter
// "alt-ctrl-shift-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
"alt-ctrl-shift-b": "branches::OpenRecent",
"alt-shift-enter": "toast::RunAction",
"ctrl-~": "workspace::NewTerminal",
@@ -768,7 +766,7 @@
"alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"
@@ -930,7 +928,6 @@
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],

View File

@@ -588,7 +588,6 @@
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
"alt-cmd-o": "projects::OpenRecent",
"ctrl-cmd-o": "projects::OpenRemote",
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true }],
"alt-cmd-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"cmd-s": "workspace::Save",
@@ -826,7 +825,7 @@
"alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
"cmd-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"
@@ -1012,7 +1011,7 @@
"alt-right": ["terminal::SendText", "\u001bf"],
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
"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"],

View File

@@ -72,9 +72,7 @@
"alt-left": "editor::SelectToPreviousWordStart",
"alt-right": "editor::SelectToNextWordEnd",
"pagedown": "editor::SelectPageDown",
"ctrl-v": "editor::SelectPageDown",
"pageup": "editor::SelectPageUp",
"alt-v": "editor::SelectPageUp",
"ctrl-f": "editor::SelectRight",
"ctrl-b": "editor::SelectLeft",
"ctrl-n": "editor::SelectDown",

View File

@@ -51,7 +51,9 @@
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
"ctrl-delete": "editor::DeleteToNextWordEnd",
"f3": "editor::FindNextMatch",
"shift-f3": "editor::FindPreviousMatch"
}
},
{

View File

@@ -72,9 +72,7 @@
"alt-left": "editor::SelectToPreviousWordStart",
"alt-right": "editor::SelectToNextWordEnd",
"pagedown": "editor::SelectPageDown",
"ctrl-v": "editor::SelectPageDown",
"pageup": "editor::SelectPageUp",
"alt-v": "editor::SelectPageUp",
"ctrl-f": "editor::SelectRight",
"ctrl-b": "editor::SelectLeft",
"ctrl-n": "editor::SelectDown",

View File

@@ -53,7 +53,9 @@
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cmd-g": "editor::FindNextMatch",
"cmd-shift-g": "editor::FindPreviousMatch"
}
},
{

View File

@@ -152,7 +152,6 @@
"g end": ["vim::EndOfLine", { "display_lines": true }],
"g 0": ["vim::StartOfLine", { "display_lines": true }],
"g home": ["vim::StartOfLine", { "display_lines": true }],
"g shift-m": ["vim::MiddleOfLine", { "display_lines": true }],
"g ^": ["vim::FirstNonWhitespace", { "display_lines": true }],
"g v": "vim::RestoreVisualSelection",
"g ]": "editor::GoToDiagnostic",

View File

@@ -335,9 +335,7 @@
// Whether to show onboarding banners in the titlebar.
"show_onboarding_banner": true,
// Whether to show user picture in the titlebar.
"show_user_picture": true,
// Whether to show the sign in button in the titlebar.
"show_sign_in": true
"show_user_picture": true
},
// Scrollbar related settings
"scrollbar": {
@@ -758,8 +756,6 @@
"stream_edits": false,
// When enabled, agent edits will be displayed in single-file editors for review
"single_file_review": true,
// When enabled, show voting thumbs for feedback on agent edits.
"enable_feedback": true,
"default_profile": "write",
"profiles": {
"write": {
@@ -1715,8 +1711,6 @@
// }
// ]
"ssh_connections": [],
// Whether to read ~/.ssh/config for ssh connection sources.
"read_ssh_config": true,
// Configures context servers for use by the agent.
"context_servers": {},
"debugger": {

View File

@@ -47,11 +47,12 @@ heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
inventory.workspace = true
itertools.workspace = true
jsonschema.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
linkme.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true

View File

@@ -185,14 +185,12 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = TextSize::Small.rems(cx);
let mut text_style = window.text_style();
let line_height = buffer_font_size * 1.75;
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(ui_font_size.into()),
line_height: Some(line_height.into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
@@ -333,6 +331,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
}
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
const MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK: usize = 10;
fn render_markdown_code_block(
message_id: MessageId,
@@ -345,20 +344,17 @@ fn render_markdown_code_block(
_window: &Window,
cx: &App,
) -> Div {
let label_size = rems(0.8125);
let label = match kind {
CodeBlockKind::Indented => None,
CodeBlockKind::Fenced => Some(
h_flex()
.px_1()
.gap_1()
.child(
Icon::new(IconName::Code)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(div().text_size(label_size).child("Plain Text"))
.child(Label::new("untitled").size(LabelSize::Small))
.into_any_element(),
),
CodeBlockKind::FencedLang(raw_language_name) => Some(render_code_language(
@@ -395,7 +391,7 @@ fn render_markdown_code_block(
.id(("code-block-header-label", ix))
.ml_1()
.gap_1()
.child(div().text_size(label_size).child(file_name))
.child(Label::new(file_name).size(LabelSize::Small))
.child(Label::new(path).color(Color::Muted).size(LabelSize::Small))
.tooltip(move |window, cx| {
Tooltip::with_meta(
@@ -408,10 +404,9 @@ fn render_markdown_code_block(
})
.into_any_element()
} else {
div()
Label::new(path_range.path.to_string_lossy().to_string())
.size(LabelSize::Small)
.ml_1()
.text_size(label_size)
.child(path_range.path.to_string_lossy().to_string())
.into_any_element()
};
@@ -459,13 +454,19 @@ fn render_markdown_code_block(
.copied_code_block_ids
.contains(&(message_id, ix));
let is_expanded = active_thread.read(cx).is_codeblock_expanded(message_id, ix);
let can_expand = metadata.line_count >= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
let is_expanded = if can_expand {
active_thread.read(cx).is_codeblock_expanded(message_id, ix)
} else {
false
};
let codeblock_header_bg = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
let control_buttons = h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
@@ -516,48 +517,44 @@ fn render_markdown_code_block(
}
}),
)
.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
IconName::ChevronDown
},
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
);
});
let codeblock_header = h_flex()
.relative()
.p_1()
.gap_1()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(codeblock_header_bg)
.map(|this| {
if !is_expanded {
this.rounded_md()
} else {
this.rounded_t_md()
.border_b_1()
.border_color(cx.theme().colors().border.opacity(0.6))
}
})
.rounded_t_md()
.children(label)
.child(control_buttons);
@@ -565,12 +562,12 @@ fn render_markdown_code_block(
.group(CODEBLOCK_CONTAINER_GROUP)
.my_2()
.overflow_hidden()
.rounded_md()
.rounded_lg()
.border_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(cx.theme().colors().editor_background)
.child(codeblock_header)
.when(!is_expanded, |this| this.h(rems_from_px(31.)))
.when(can_expand && !is_expanded, |this| this.max_h_80())
}
fn open_path(
@@ -631,13 +628,10 @@ fn render_code_language(
.map(|language| language.name().into())
.unwrap_or(name_fallback);
let label_size = rems(0.8125);
h_flex()
.px_1()
.gap_1p5()
.children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)))
.child(div().text_size(label_size).child(language_label))
.gap_1()
.children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::Small)))
.child(Label::new(language_label).size(LabelSize::Small))
.into_any_element()
}
@@ -1018,7 +1012,6 @@ impl ActiveThread {
self.push_message(message_id, &message_segments, window, cx);
}
self.scroll_to_bottom(cx);
self.save_thread(cx);
cx.notify();
}
@@ -1032,7 +1025,6 @@ impl ActiveThread {
self.edited_message(message_id, &message_segments, window, cx);
}
self.scroll_to_bottom(cx);
self.save_thread(cx);
cx.notify();
}
@@ -1546,15 +1538,11 @@ impl ActiveThread {
let project = self.thread.read(cx).project().clone();
let prompt_store = self.thread_store.read(cx).prompt_store().clone();
let git_store = project.read(cx).git_store().clone();
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
let load_context_task =
crate::context::load_context(new_context, &project, &prompt_store, cx);
self._load_edited_message_context_task =
Some(cx.spawn_in(window, async move |this, cx| {
let (context, checkpoint) =
futures::future::join(load_context_task, checkpoint).await;
let context = load_context_task.await;
let _ = this
.update_in(cx, |this, window, cx| {
this.thread.update(cx, |thread, cx| {
@@ -1563,7 +1551,6 @@ impl ActiveThread {
Role::User,
vec![MessageSegment::Text(edited_text)],
Some(context.loaded_context),
checkpoint.ok(),
cx,
);
for message_id in this.messages_after(message_id) {
@@ -1733,11 +1720,10 @@ impl ActiveThread {
.on_action(cx.listener(Self::confirm_editing_message))
.capture_action(cx.listener(Self::paste))
.min_h_6()
.w_full()
.flex_grow()
.w_full()
.gap_2()
.child(state.context_strip.clone())
.child(div().pt(px(-3.)).px_neg_0p5().child(EditorElement::new(
.child(EditorElement::new(
&state.editor,
EditorStyle {
background: colors.editor_background,
@@ -1746,7 +1732,8 @@ impl ActiveThread {
syntax: cx.theme().syntax().clone(),
..Default::default()
},
)))
))
.child(state.context_strip.clone())
}
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
@@ -1874,8 +1861,7 @@ impl ActiveThread {
.child(open_as_markdown),
)
.into_any_element(),
None if AssistantSettings::get_global(cx).enable_feedback =>
feedback_container
None => feedback_container
.child(
div().visible_on_hover("feedback_container").child(
Label::new(
@@ -1918,9 +1904,6 @@ impl ActiveThread {
.child(open_as_markdown),
)
.into_any_element(),
None => feedback_container
.child(h_flex().child(open_as_markdown))
.into_any_element(),
};
let message_is_empty = message.should_display_content();
@@ -1934,6 +1917,16 @@ impl ActiveThread {
v_flex()
.w_full()
.gap_1()
.when(!message_is_empty, |parent| {
parent.child(div().min_h_6().child(self.render_message_content(
message_id,
rendered_message,
has_tool_uses,
workspace.clone(),
window,
cx,
)))
})
.when(!added_context.is_empty(), |parent| {
parent.child(h_flex().flex_wrap().gap_1().children(
added_context.into_iter().map(|added_context| {
@@ -1952,16 +1945,6 @@ impl ActiveThread {
}),
))
})
.when(!message_is_empty, |parent| {
parent.child(div().pt_0p5().min_h_6().child(self.render_message_content(
message_id,
rendered_message,
has_tool_uses,
workspace.clone(),
window,
cx,
)))
})
.into_any_element()
}
});
@@ -1987,7 +1970,6 @@ impl ActiveThread {
h_flex()
.p_2p5()
.gap_1()
.items_end()
.children(message_content)
.when_some(editing_message_state, |this, state| {
let focus_handle = state.editor.focus_handle(cx).clone();
@@ -2001,7 +1983,6 @@ impl ActiveThread {
)
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -2019,12 +2000,11 @@ impl ActiveThread {
.child(
IconButton::new(
"confirm-edit-message",
IconName::Return,
IconName::Check,
)
.disabled(state.editor.read(cx).is_empty(cx))
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.icon_color(Color::Success)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -2044,6 +2024,9 @@ impl ActiveThread {
)
}),
)
.when(editing_message_state.is_none(), |this| {
this.tooltip(Tooltip::text("Click To Edit"))
})
.on_click(cx.listener({
let message_segments = message.segments.clone();
move |this, _, window, cx| {
@@ -2084,16 +2067,6 @@ impl ActiveThread {
let panel_background = cx.theme().colors().panel_background;
let backdrop = div()
.id("backdrop")
.stop_mouse_events_except_scroll()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8)
.on_click(cx.listener(Self::handle_cancel_click));
v_flex()
.w_full()
.map(|parent| {
@@ -2263,7 +2236,15 @@ impl ActiveThread {
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(backdrop)
parent.relative().child(
div()
.stop_mouse_events_except_scroll()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
})
.into_any()
}
@@ -2373,17 +2354,39 @@ impl ActiveThread {
}),
transform: Some(Arc::new({
let active_thread = cx.entity();
let editor_bg = cx.theme().colors().editor_background;
move |el, range, metadata, _, cx| {
let can_expand = metadata.line_count
>= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
if !can_expand {
return el;
}
move |element, range, _, _, cx| {
let is_expanded = active_thread
.read(cx)
.is_codeblock_expanded(message_id, range.start);
if is_expanded {
return element;
return el;
}
element
el.child(
div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_1_4()
.rounded_b_lg()
.bg(linear_gradient(
0.,
linear_color_stop(editor_bg, 0.),
linear_color_stop(
editor_bg.opacity(0.),
1.,
),
)),
)
}
})),
},
@@ -3385,21 +3388,16 @@ impl ActiveThread {
self.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(true)
.unwrap_or(false)
}
pub fn toggle_codeblock_expanded(&mut self, message_id: MessageId, ix: usize) {
let is_expanded = self
.expanded_code_blocks
.entry((message_id, ix))
.or_insert(true);
.or_insert(false);
*is_expanded = !*is_expanded;
}
pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
self.list_state.reset(self.messages.len());
cx.notify();
}
}
pub enum ActiveThreadEvent {
@@ -3413,7 +3411,6 @@ impl Render for ActiveThread {
v_flex()
.size_full()
.relative()
.bg(cx.theme().colors().panel_background)
.on_mouse_move(cx.listener(|this, _, _, cx| {
this.show_scrollbar = true;
this.hide_scrollbar_later(cx);

View File

@@ -85,7 +85,6 @@ actions!(
KeepAll,
Follow,
ResetTrialUpsell,
ResetTrialEndUpsell,
]
);
@@ -217,6 +216,7 @@ fn register_slash_commands(cx: &mut App) {
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
slash_command_registry.register_command(assistant_slash_commands::TerminalSlashCommand, true);
slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
slash_command_registry
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);

View File

@@ -18,8 +18,8 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use settings::{Settings, update_settings_file};
use ui::{
Disclosure, ElevationIndex, Indicator, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip,
prelude::*,
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
Switch, SwitchColor, Tooltip, prelude::*,
};
use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
@@ -142,7 +142,7 @@ impl AgentConfiguration {
.expanded_provider_configurations
.get(&provider.id())
.copied()
.unwrap_or(false);
.unwrap_or(true);
v_flex()
.pt_3()
@@ -201,12 +201,12 @@ impl AgentConfiguration {
.on_click(cx.listener({
let provider_id = provider.id().clone();
move |this, _event, _window, _cx| {
let is_expanded = this
let is_open = this
.expanded_provider_configurations
.entry(provider_id.clone())
.or_insert(false);
.or_insert(true);
*is_expanded = !*is_expanded;
*is_open = !*is_open;
}
})),
),
@@ -214,9 +214,9 @@ impl AgentConfiguration {
)
.when(is_expanded, |parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(Label::new(format!(
None => parent.child(div().child(Label::new(format!(
"No configuration view for {provider_name}",
))),
)))),
})
}
@@ -230,8 +230,7 @@ impl AgentConfiguration {
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_4()
.border_b_1()
.border_color(cx.theme().colors().border)
.flex_1()
.child(
v_flex()
.gap_0p5()
@@ -332,8 +331,7 @@ impl AgentConfiguration {
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.flex_1()
.child(Headline::new("General Settings"))
.child(self.render_command_permission(cx))
.child(self.render_single_file_review(cx))
@@ -346,17 +344,18 @@ impl AgentConfiguration {
) -> impl IntoElement {
let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone();
const SUBHEADING: &str = "Connect to context servers via the Model Context Protocol either via Zed extensions or directly.";
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.border_b_1()
.border_color(cx.theme().colors().border)
.flex_1()
.child(
v_flex()
.gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(Label::new("Connect to context servers via the Model Context Protocol either via Zed extensions or directly.").color(Color::Muted)),
.child(Label::new(SUBHEADING).color(Color::Muted)),
)
.children(
context_server_ids.into_iter().map(|context_server_id| {
@@ -631,7 +630,9 @@ impl Render for AgentConfiguration {
.size_full()
.overflow_y_scroll()
.child(self.render_general_settings_section(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_context_servers_section(window, cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_provider_configuration_section(cx)),
)
.child(

View File

@@ -30,6 +30,7 @@ pub(crate) struct ConfigureContextServerModal {
context_server_store: Entity<ContextServerStore>,
}
#[allow(clippy::large_enum_variant)]
enum Configuration {
NotAvailable,
Required(ConfigurationRequiredState),

View File

@@ -3,10 +3,10 @@ use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use crate::Thread;
use assistant_context_editor::language_model_selector::{
use language_model::{ConfiguredModel, LanguageModelRegistry};
use language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use language_model::{ConfiguredModel, LanguageModelRegistry};
use settings::update_settings_file;
use std::sync::Arc;
use ui::{PopoverMenuHandle, Tooltip, prelude::*};

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use db::kvp::KEY_VALUE_STORE;
use markdown::Markdown;
use serde::{Deserialize, Serialize};
@@ -17,7 +17,6 @@ use assistant_settings::{AssistantDockPosition, AssistantSettings};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use client::{UserStore, zed_urls};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
@@ -31,6 +30,7 @@ use language::LanguageRegistry;
use language_model::{
LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::ToggleModelSelector;
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use proto::Plan;
@@ -66,8 +66,8 @@ use crate::ui::AgentOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, TextThreadStore, ThreadEvent,
ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
OpenHistory, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
ToggleNavigationMenu, ToggleOptionsMenu,
};
const AGENT_PANEL_KEY: &str = "agent_panel";
@@ -157,10 +157,7 @@ pub fn init(cx: &mut App) {
window.refresh();
})
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
TrialUpsell::set_dismissed(false, cx);
})
.register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
TrialEndUpsell::set_dismissed(false, cx);
set_trial_upsell_dismissed(false, cx);
});
},
)
@@ -570,15 +567,6 @@ impl AgentPanel {
menu = menu.header("Recently Opened");
for entry in recently_opened.iter() {
if let RecentEntry::Context(context) = entry {
if context.read(cx).path().is_none() {
log::error!(
"bug: text thread in recent history list was never saved"
);
continue;
}
}
let summary = entry.summary(cx);
menu = menu.entry_with_end_slot_on_hover(
@@ -1212,7 +1200,12 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace.upgrade() else {
let Some(workspace) = self
.workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace dropped"))
.log_err()
else {
return;
};
@@ -1297,26 +1290,14 @@ impl AgentPanel {
let new_is_history = matches!(new_view, ActiveView::History);
match &self.active_view {
ActiveView::Thread { thread, .. } => {
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
if let Some(thread) = thread.upgrade() {
if thread.read(cx).is_empty() {
let id = thread.read(cx).id().clone();
self.history_store.update(cx, |store, cx| {
store.remove_recently_opened_thread(id, cx);
});
store.remove_recently_opened_thread(id, cx);
}
}
}
ActiveView::PromptEditor { context_editor, .. } => {
let context = context_editor.read(cx).context();
// When switching away from an unsaved text thread, delete its entry.
if context.read(cx).path().is_none() {
let context = context.clone();
self.history_store.update(cx, |store, cx| {
store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
});
}
}
}),
_ => {}
}
@@ -1930,23 +1911,12 @@ impl AgentPanel {
}
}
fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
if TrialEndUpsell::dismissed() {
return false;
}
let plan = self.user_store.read(cx).current_plan();
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
matches!(plan, Some(Plan::Free)) && has_previous_trial
}
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
if !matches!(self.active_view, ActiveView::Thread { .. }) {
return false;
}
if self.hide_trial_upsell || TrialUpsell::dismissed() {
if self.hide_trial_upsell || dismissed_trial_upsell() {
return false;
}
@@ -1992,115 +1962,125 @@ impl AgentPanel {
move |toggle_state, _window, cx| {
let toggle_state_bool = toggle_state.selected();
TrialUpsell::set_dismissed(toggle_state_bool, cx);
set_trial_upsell_dismissed(toggle_state_bool, cx);
},
);
let contents = div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(
Label::new("Try Zed Pro for free for 14 days - no credit card required.")
.size(LabelSize::Small),
)
.child(
Label::new(
"Use your own API keys or enable usage-based billing once you hit the cap.",
)
.color(Color::Muted),
)
.child(
h_flex()
Some(
div().p_2().child(
v_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.elevation_2(cx)
.rounded(px(8.))
.bg(cx.theme().colors().background.alpha(0.5))
.p(px(3.))
.child(
h_flex()
div()
.gap_2()
.flex()
.flex_col()
.size_full()
.border_1()
.rounded(px(5.))
.border_color(cx.theme().colors().text.alpha(0.1))
.overflow_hidden()
.relative()
.bg(cx.theme().colors().panel_background)
.px_4()
.py_3()
.child(
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |this, cx| {
this.hide_trial_upsell = true;
cx.notify();
});
}
}),
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
)
)
.child(
Button::new("cta-button", "Start Trial")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
),
),
);
Some(self.render_upsell_container(cx, contents))
}
fn render_trial_end_upsell(
&self,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if !self.should_render_trial_end_upsell(cx) {
return None;
}
Some(
self.render_upsell_container(
cx,
div()
.size_full()
.gap_2()
.flex()
.flex_col()
.child(
Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
)
.child(
Label::new("You've been automatically reset to the free plan.")
.size(LabelSize::Small),
)
.child(
h_flex()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(div())
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32)))
)
)
// .child(
// div()
// .absolute()
// .top_0()
// .right(px(360.))
// .size(px(401.))
// .overflow_hidden()
// .bg(cx.theme().colors().panel_background)
// )
.child(
div()
.absolute()
.top_0()
.right_0()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.bg(linear_gradient(
75.,
linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
linear_color_stop(cx.theme().colors().panel_background, 0.45),
))
)
.child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
.child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small))
.child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
.child(
h_flex()
.gap_2()
.w_full()
.px_neg_1()
.justify_between()
.items_center()
.child(h_flex().items_center().gap_1().child(checkbox))
.child(
Button::new("dismiss-button", "Stay on Free")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(cx, |_this, cx| {
TrialEndUpsell::set_dismissed(true, cx);
cx.notify();
});
}
}),
)
.child(
Button::new("cta-button", "Upgrade to Zed Pro")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::account_url(cx))
}),
h_flex()
.gap_2()
.child(
Button::new("dismiss-button", "Not Now")
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let agent_panel = cx.entity();
move |_, _, cx| {
agent_panel.update(
cx,
|this, cx| {
let hidden =
this.hide_trial_upsell;
println!("hidden: {}", hidden);
this.hide_trial_upsell = true;
let new_hidden =
this.hide_trial_upsell;
println!(
"new_hidden: {}",
new_hidden
);
cx.notify();
},
);
}
}),
)
.child(
Button::new("cta-button", "Start Trial")
.style(ButtonStyle::Transparent)
.on_click(|_, _, cx| {
cx.open_url(&zed_urls::account_url(cx))
}),
),
),
),
),
@@ -2108,91 +2088,6 @@ impl AgentPanel {
)
}
fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
div().p_2().child(
v_flex()
.w_full()
.elevation_2(cx)
.rounded(px(8.))
.bg(cx.theme().colors().background.alpha(0.5))
.p(px(3.))
.child(
div()
.gap_2()
.flex()
.flex_col()
.size_full()
.border_1()
.rounded(px(5.))
.border_color(cx.theme().colors().text.alpha(0.1))
.overflow_hidden()
.relative()
.bg(cx.theme().colors().panel_background)
.px_4()
.py_3()
.child(
div()
.absolute()
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.child(
Vector::new(
VectorName::Grid,
rems_from_px(441.),
rems_from_px(167.),
)
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
),
)
.child(
div()
.absolute()
.top(px(-8.0))
.right_0()
.w(px(400.))
.h(px(92.))
.child(
Vector::new(
VectorName::AiGrid,
rems_from_px(400.),
rems_from_px(92.),
)
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
),
)
// .child(
// div()
// .absolute()
// .top_0()
// .right(px(360.))
// .size(px(401.))
// .overflow_hidden()
// .bg(cx.theme().colors().panel_background)
// )
.child(
div()
.absolute()
.top_0()
.right_0()
.w(px(660.))
.h(px(401.))
.overflow_hidden()
.bg(linear_gradient(
75.,
linear_color_stop(
cx.theme().colors().panel_background.alpha(0.01),
1.0,
),
linear_color_stop(cx.theme().colors().panel_background, 0.45),
)),
)
.child(content),
),
)
}
fn render_active_thread_or_empty_state(
&self,
window: &mut Window,
@@ -2240,7 +2135,6 @@ impl AgentPanel {
v_flex()
.size_full()
.bg(cx.theme().colors().panel_background)
.when(recent_history.is_empty(), |this| {
let configuration_error_ref = &configuration_error;
this.child(
@@ -2911,7 +2805,6 @@ impl Render for AgentPanel {
.on_action(cx.listener(Self::toggle_zoom))
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
.children(self.render_trial_end_upsell(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
.relative()
@@ -3099,14 +2992,25 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
}
}
struct TrialUpsell;
const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
impl Dismissable for TrialUpsell {
const KEY: &'static str = "dismissed-trial-upsell";
fn dismissed_trial_upsell() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
.log_err()
.map_or(false, |s| s.is_some())
}
struct TrialEndUpsell;
impl Dismissable for TrialEndUpsell {
const KEY: &'static str = "dismissed-trial-end-upsell";
fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE
.write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
.await
} else {
db::kvp::KEY_VALUE_STORE
.delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
.await
}
})
}

View File

@@ -1,7 +1,7 @@
use crate::context::ContextLoadResult;
use crate::inline_prompt_editor::CodegenStatus;
use crate::{context::load_context, context_store::ContextStore};
use anyhow::{Context as _, Result};
use anyhow::Result;
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::HashSet;
@@ -419,16 +419,16 @@ impl CodegenAlternative {
if start_buffer.remote_id() == end_buffer.remote_id() {
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
} else {
anyhow::bail!("invalid transformation range");
return Err(anyhow::anyhow!("invalid transformation range"));
}
} else {
anyhow::bail!("invalid transformation range");
return Err(anyhow::anyhow!("invalid transformation range"));
};
let prompt = self
.builder
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
.context("generating content prompt")?;
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
let context_task = self.context_store.as_ref().map(|context_store| {
if let Some(project) = self.project.upgrade() {

View File

@@ -942,8 +942,8 @@ impl MentionLink {
format!("[@{}]({}:{})", title, Self::THREAD, id)
}
ThreadContextEntry::Context { path, title } => {
let filename = path.file_name().unwrap_or_default().to_string_lossy();
let escaped_filename = urlencoding::encode(&filename);
let filename = path.file_name().unwrap_or_default();
let escaped_filename = urlencoding::encode(&filename.to_string_lossy()).to_string();
format!(
"[@{}]({}:{}{})",
title,

View File

@@ -2,7 +2,7 @@ use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_context_editor::AssistantContext;
use collections::{HashSet, IndexSet};
use futures::{self, FutureExt};
@@ -142,12 +142,17 @@ impl ContextStore {
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Result<Option<AgentContextHandle>> {
let project = self.project.upgrade().context("failed to read project")?;
let entry_id = project
let Some(project) = self.project.upgrade() else {
return Err(anyhow!("failed to read project"));
};
let Some(entry_id) = project
.read(cx)
.entry_for_path(project_path, cx)
.map(|entry| entry.id)
.context("no entry found for directory context")?;
else {
return Err(anyhow!("no entry found for directory context"));
};
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Directory(DirectoryContextHandle {

View File

@@ -84,12 +84,6 @@ impl ContextStrip {
}
}
/// Whether or not the context strip has items to display
pub fn has_context_items(&self, cx: &App) -> bool {
self.context_store.read(cx).context().next().is_some()
|| self.suggested_context(cx).is_some()
}
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
if let Some(workspace) = self.workspace.upgrade() {
let project = workspace.read(cx).project().read(cx);
@@ -110,14 +104,14 @@ impl ContextStrip {
}
}
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
fn suggested_context(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
match self.suggest_context_kind {
SuggestContextKind::File => self.suggested_file(cx),
SuggestContextKind::Thread => self.suggested_thread(cx),
}
}
fn suggested_file(&self, cx: &App) -> Option<SuggestedContext> {
fn suggested_file(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
let workspace = self.workspace.upgrade()?;
let active_item = workspace.read(cx).active_item(cx)?;
@@ -144,7 +138,7 @@ impl ContextStrip {
})
}
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
fn suggested_thread(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
if !self.context_picker.read(cx).allow_threads() {
return None;
}

View File

@@ -1,6 +1,6 @@
use std::{collections::VecDeque, path::Path, sync::Arc};
use anyhow::Context as _;
use anyhow::{Context as _, anyhow};
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
use chrono::{DateTime, Utc};
use futures::future::{TryFutureExt as _, join_all};
@@ -130,10 +130,7 @@ impl HistoryStore {
.boxed()
})
.unwrap_or_else(|_| {
async {
anyhow::bail!("no thread store");
}
.boxed()
async { Err(anyhow!("no thread store")) }.boxed()
}),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
@@ -143,10 +140,7 @@ impl HistoryStore {
.boxed()
})
.unwrap_or_else(|_| {
async {
anyhow::bail!("no context store");
}
.boxed()
async { Err(anyhow!("no context store")) }.boxed()
}),
});
let entries = join_all(entries)

View File

@@ -9,10 +9,8 @@ use crate::terminal_codegen::TerminalCodegen;
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use client::ErrorExt;
use collections::VecDeque;
use db::kvp::Dismissable;
use editor::display_map::EditorMargins;
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
@@ -25,6 +23,7 @@ use gpui::{
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::ToggleModelSelector;
use parking_lot::Mutex;
use settings::Settings;
use std::cmp;
@@ -34,6 +33,7 @@ use ui::utils::WithRemSize;
use ui::{
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
};
use util::ResultExt;
use workspace::Workspace;
pub struct PromptEditor<T> {
@@ -451,7 +451,7 @@ impl<T: 'static> PromptEditor<T> {
editor.move_to_end(&Default::default(), window, cx)
});
}
} else if self.context_strip.read(cx).has_context_items(cx) {
} else {
self.context_strip.focus_handle(cx).focus(window);
}
}
@@ -722,7 +722,7 @@ impl<T: 'static> PromptEditor<T> {
.child(CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again"),
if RateLimitNotice::dismissed() {
if dismissed_rate_limit_notice() {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
@@ -734,7 +734,7 @@ impl<T: 'static> PromptEditor<T> {
ui::ToggleState::Selected => true,
};
RateLimitNotice::set_dismissed(is_dismissed, cx);
set_rate_limit_notice_dismissed(is_dismissed, cx)
},
))
.child(
@@ -974,7 +974,7 @@ impl PromptEditor<BufferCodegen> {
CodegenStatus::Error(error) => {
if cx.has_flag::<ZedProFeatureFlag>()
&& error.error_code() == proto::ErrorCode::RateLimitExceeded
&& !RateLimitNotice::dismissed()
&& !dismissed_rate_limit_notice()
{
self.show_rate_limit_notice = true;
cx.notify();
@@ -1180,10 +1180,27 @@ impl PromptEditor<TerminalCodegen> {
}
}
struct RateLimitNotice;
const DISMISSED_RATE_LIMIT_NOTICE_KEY: &str = "dismissed-rate-limit-notice";
impl Dismissable for RateLimitNotice {
const KEY: &'static str = "dismissed-rate-limit-notice";
fn dismissed_rate_limit_notice() -> bool {
db::kvp::KEY_VALUE_STORE
.read_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY)
.log_err()
.map_or(false, |s| s.is_some())
}
fn set_rate_limit_notice_dismissed(is_dismissed: bool, cx: &mut App) {
db::write_and_log(cx, move || async move {
if is_dismissed {
db::kvp::KEY_VALUE_STORE
.write_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into(), "1".into())
.await
} else {
db::kvp::KEY_VALUE_STORE
.delete_kvp(DISMISSED_RATE_LIMIT_NOTICE_KEY.into())
.await
}
})
}
pub enum CodegenStatus {

View File

@@ -8,7 +8,6 @@ use crate::ui::{
AnimatedLabel, MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use assistant_context_editor::language_model_selector::ToggleModelSelector;
use assistant_settings::{AssistantSettings, CompletionMode};
use buffer_diff::BufferDiff;
use client::UserStore;
@@ -31,6 +30,7 @@ use language_model::{
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::ToggleModelSelector;
use multi_buffer;
use project::Project;
use prompt_store::PromptStore;
@@ -401,7 +401,7 @@ impl MessageEditor {
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
if self.context_picker_menu_handle.is_deployed() {
cx.propagate();
} else if self.context_strip.read(cx).has_context_items(cx) {
} else {
self.context_strip.focus_handle(cx).focus(window);
}
}

View File

@@ -191,7 +191,7 @@ impl TerminalInlineAssistant {
};
self.prompt_history.retain(|prompt| *prompt != user_prompt);
self.prompt_history.push_back(user_prompt);
self.prompt_history.push_back(user_prompt.clone());
if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
self.prompt_history.pop_front();
}

View File

@@ -24,7 +24,7 @@ use language_model::{
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, SelectedModel,
StopReason, TokenUsage, WrappedTextContent,
StopReason, TokenUsage,
};
use postage::stream::Stream as _;
use project::Project;
@@ -214,7 +214,7 @@ pub struct GitState {
pub diff: Option<String>,
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct ThreadCheckpoint {
message_id: MessageId,
git_checkpoint: GitStoreCheckpoint,
@@ -881,10 +881,7 @@ impl Thread {
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
match &self.tool_use.tool_result(id)?.content {
LanguageModelToolResultContent::Text(text)
| LanguageModelToolResultContent::WrappedText(WrappedTextContent { text, .. }) => {
Some(text)
}
LanguageModelToolResultContent::Text(str) => Some(str),
LanguageModelToolResultContent::Image(_) => {
// TODO: We should display image
None
@@ -999,7 +996,6 @@ impl Thread {
new_role: Role,
new_segments: Vec<MessageSegment>,
loaded_context: Option<LoadedContext>,
checkpoint: Option<GitStoreCheckpoint>,
cx: &mut Context<Self>,
) -> bool {
let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
@@ -1010,15 +1006,6 @@ impl Thread {
if let Some(context) = loaded_context {
message.loaded_context = context;
}
if let Some(git_checkpoint) = checkpoint {
self.checkpoints_by_message.insert(
id,
ThreadCheckpoint {
message_id: id,
git_checkpoint,
},
);
}
self.touch_updated_at();
cx.emit(ThreadEvent::MessageEdited(id));
true
@@ -1630,7 +1617,7 @@ impl Thread {
CompletionRequestStatus::Failed {
code, message, request_id
} => {
anyhow::bail!("completion request failed. request_id: {request_id}, code: {code}, message: {message}");
return Err(anyhow!("completion request failed. request_id: {request_id}, code: {code}, message: {message}"));
}
CompletionRequestStatus::UsageUpdated {
amount, limit
@@ -2518,12 +2505,8 @@ impl Thread {
writeln!(markdown, "**\n")?;
match &tool_result.content {
LanguageModelToolResultContent::Text(text)
| LanguageModelToolResultContent::WrappedText(WrappedTextContent {
text,
..
}) => {
writeln!(markdown, "{text}")?;
LanguageModelToolResultContent::Text(str) => {
writeln!(markdown, "{}", str)?;
}
LanguageModelToolResultContent::Image(image) => {
writeln!(markdown, "![Image](data:base64,{})", image.source)?;
@@ -2600,7 +2583,7 @@ impl Thread {
.read(cx)
.current_user()
.map(|user| user.github_login.clone());
let client = self.project.read(cx).client();
let client = self.project.read(cx).client().clone();
let serialize_task = self.serialize(cx);
cx.background_executor()

View File

@@ -260,7 +260,10 @@ impl ThreadHistory {
}
});
self.search_state = SearchState::Searching { query, _task: task };
self.search_state = SearchState::Searching {
query: query.clone(),
_task: task,
};
cx.notify();
}

View File

@@ -419,7 +419,7 @@ impl ThreadStore {
let thread = database
.try_find_thread(id.clone())
.await?
.with_context(|| format!("no thread found with ID: {id:?}"))?;
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
let thread = this.update_in(cx, |this, window, cx| {
cx.new(|cx| {
@@ -699,14 +699,20 @@ impl SerializedThread {
SerializedThread::VERSION => Ok(serde_json::from_value::<SerializedThread>(
saved_thread_json,
)?),
_ => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
_ => Err(anyhow!(
"unrecognized serialized thread version: {}",
version
)),
},
None => {
let saved_thread =
serde_json::from_value::<LegacySerializedThread>(saved_thread_json)?;
Ok(saved_thread.upgrade())
}
version => anyhow::bail!("unrecognized serialized thread version: {version:?}"),
version => Err(anyhow!(
"unrecognized serialized thread version: {:?}",
version
)),
}
}
}

View File

@@ -425,17 +425,16 @@ impl ToolUseState {
let content = match tool_result {
ToolResultContent::Text(text) => {
let text = if text.len() < tool_output_limit {
text
} else {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
LanguageModelToolResultContent::Text(
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
};
LanguageModelToolResultContent::Text(text.into())
.into(),
)
}
ToolResultContent::Image(language_model_image) => {
if language_model_image.estimate_tokens() < tool_output_limit {

View File

@@ -1,8 +1,8 @@
use std::sync::OnceLock;
use collections::HashMap;
use component::ComponentId;
use gpui::{App, Entity, WeakEntity};
use linkme::distributed_slice;
use std::sync::OnceLock;
use ui::{AnyElement, Component, ComponentScope, Window};
use workspace::Workspace;
@@ -12,15 +12,9 @@ use crate::ActiveThread;
pub type PreviewFn =
fn(WeakEntity<Workspace>, Entity<ActiveThread>, &mut Window, &mut App) -> Option<AnyElement>;
pub struct AgentPreviewFn(fn() -> (ComponentId, PreviewFn));
impl AgentPreviewFn {
pub const fn new(f: fn() -> (ComponentId, PreviewFn)) -> Self {
Self(f)
}
}
inventory::collect!(AgentPreviewFn);
/// Distributed slice for preview registration functions
#[distributed_slice]
pub static __ALL_AGENT_PREVIEWS: [fn() -> (ComponentId, PreviewFn)] = [..];
/// Trait that must be implemented by components that provide agent previews.
pub trait AgentPreview: Component + Sized {
@@ -42,14 +36,16 @@ pub trait AgentPreview: Component + Sized {
#[macro_export]
macro_rules! register_agent_preview {
($type:ty) => {
inventory::submit! {
$crate::ui::preview::AgentPreviewFn::new(|| {
(
<$type as component::Component>::id(),
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
)
})
}
#[linkme::distributed_slice($crate::ui::preview::__ALL_AGENT_PREVIEWS)]
static __REGISTER_AGENT_PREVIEW: fn() -> (
component::ComponentId,
$crate::ui::preview::PreviewFn,
) = || {
(
<$type as component::Component>::id(),
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
)
};
};
}
@@ -60,8 +56,8 @@ static AGENT_PREVIEW_REGISTRY: OnceLock<HashMap<ComponentId, PreviewFn>> = OnceL
fn get_or_init_registry() -> &'static HashMap<ComponentId, PreviewFn> {
AGENT_PREVIEW_REGISTRY.get_or_init(|| {
let mut map = HashMap::default();
for register_fn in inventory::iter::<AgentPreviewFn>() {
let (id, preview_fn) = (register_fn.0)();
for register_fn in __ALL_AGENT_PREVIEWS.iter() {
let (id, preview_fn) = register_fn();
map.insert(id, preview_fn);
}
map

View File

@@ -90,7 +90,7 @@ impl Model {
} else if id.starts_with("claude-3-haiku") {
Ok(Self::Claude3Haiku)
} else {
anyhow::bail!("invalid model id {id}");
Err(anyhow!("invalid model id"))
}
}
@@ -385,10 +385,10 @@ impl RateLimitInfo {
}
}
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> Result<&'a str, anyhow::Error> {
Ok(headers
.get(key)
.with_context(|| format!("missing header `{key}`"))?
.ok_or_else(|| anyhow!("missing header `{key}`"))?
.to_str()?)
}

View File

@@ -163,10 +163,8 @@ impl AskPassSession {
#[cfg(unix)]
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
let zed_path = std::env::current_exe()
.context("Failed to determine current executable path for use in askpass")?
.context("Failed to figure out current executable path for use in askpass")?
.to_string_lossy()
// see https://github.com/rust-lang/rust/issues/69343
.trim_end_matches(" (deleted)")
.to_string();
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have

View File

@@ -1,6 +1,6 @@
// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build.
use anyhow::anyhow;
use anyhow::Context as _;
use gpui::{App, AssetSource, Result, SharedString};
use rust_embed::RustEmbed;
@@ -21,7 +21,7 @@ impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
Self::get(path)
.map(|f| Some(f.data))
.with_context(|| format!("loading asset at path {path:?}"))
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
@@ -39,7 +39,7 @@ impl AssetSource for Assets {
impl Assets {
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
pub fn load_fonts(&self, cx: &App) -> anyhow::Result<()> {
pub fn load_fonts(&self, cx: &App) -> gpui::Result<()> {
let font_paths = self.list("fonts")?;
let mut embedded_fonts = Vec::new();
for font_path in font_paths {

View File

@@ -22,7 +22,6 @@ clock.workspace = true
collections.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
@@ -30,16 +29,15 @@ gpui.workspace = true
indexed_docs.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
log.workspace = true
multi_buffer.workspace = true
open_ai.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
regex.workspace = true
rope.workspace = true
rpc.workspace = true

View File

@@ -2,7 +2,6 @@ mod context;
mod context_editor;
mod context_history;
mod context_store;
pub mod language_model_selector;
mod slash_command;
mod slash_command_picker;

View File

@@ -1,7 +1,7 @@
#[cfg(test)]
mod context_tests;
use anyhow::{Context as _, Result, bail};
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_settings::AssistantSettings;
use assistant_slash_command::{
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
@@ -3011,7 +3011,7 @@ impl SavedContext {
let saved_context_json = serde_json::from_str::<serde_json::Value>(json)?;
match saved_context_json
.get("version")
.context("version not found")?
.ok_or_else(|| anyhow!("version not found"))?
{
serde_json::Value::String(version) => match version.as_str() {
SavedContext::VERSION => {
@@ -3032,9 +3032,9 @@ impl SavedContext {
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
Ok(saved_context.upgrade())
}
_ => anyhow::bail!("unrecognized saved context version: {version:?}"),
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
},
_ => anyhow::bail!("version not found on saved context"),
_ => Err(anyhow!("version not found on saved context")),
}
}

View File

@@ -1,6 +1,3 @@
use crate::language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use anyhow::Result;
use assistant_settings::AssistantSettings;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
@@ -39,6 +36,9 @@ use language_model::{
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
Role,
};
use language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
};
use multi_buffer::MultiBufferRow;
use picker::Picker;
use project::{Project, Worktree};
@@ -3044,7 +3044,7 @@ fn invoked_slash_command_fold_placeholder(
.gap_2()
.bg(cx.theme().colors().surface_background)
.rounded_sm()
.child(Label::new(format!("/{}", command.name)))
.child(Label::new(format!("/{}", command.name.clone())))
.map(|parent| match &command.status {
InvokedSlashCommandStatus::Running(_) => {
parent.child(Icon::new(IconName::ArrowCircle).with_animation(

View File

@@ -2,7 +2,7 @@ use crate::{
AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
SavedContextMetadata,
};
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
use client::{Client, TypedEnvelope, proto, telemetry::Telemetry};
use clock::ReplicaId;
@@ -164,18 +164,16 @@ impl ContextStore {
) -> Result<proto::OpenContextResponse> {
let context_id = ContextId::from_proto(envelope.payload.context_id);
let operations = this.update(&mut cx, |this, cx| {
anyhow::ensure!(
!this.project.read(cx).is_via_collab(),
"only the host contexts can be opened"
);
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("only the host contexts can be opened"));
}
let context = this
.loaded_context_for_id(&context_id, cx)
.context("context not found")?;
anyhow::ensure!(
context.read(cx).replica_id() == ReplicaId::default(),
"context must be opened via the host"
);
if context.read(cx).replica_id() != ReplicaId::default() {
return Err(anyhow!("context must be opened via the host"));
}
anyhow::Ok(
context
@@ -195,10 +193,9 @@ impl ContextStore {
mut cx: AsyncApp,
) -> Result<proto::CreateContextResponse> {
let (context_id, operations) = this.update(&mut cx, |this, cx| {
anyhow::ensure!(
!this.project.read(cx).is_via_collab(),
"can only create contexts as the host"
);
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("can only create contexts as the host"));
}
let context = this.create(cx);
let context_id = context.read(cx).id().clone();
@@ -240,10 +237,9 @@ impl ContextStore {
mut cx: AsyncApp,
) -> Result<proto::SynchronizeContextsResponse> {
this.update(&mut cx, |this, cx| {
anyhow::ensure!(
!this.project.read(cx).is_via_collab(),
"only the host can synchronize contexts"
);
if this.project.read(cx).is_via_collab() {
return Err(anyhow!("only the host can synchronize contexts"));
}
let mut local_versions = Vec::new();
for remote_version_proto in envelope.payload.contexts {
@@ -374,7 +370,7 @@ impl ContextStore {
) -> Task<Result<Entity<AssistantContext>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
return Task::ready(Err(anyhow::anyhow!("project was not remote")));
return Task::ready(Err(anyhow!("project was not remote")));
};
let replica_id = project.replica_id();
@@ -537,7 +533,7 @@ impl ContextStore {
) -> Task<Result<Entity<AssistantContext>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
return Task::ready(Err(anyhow::anyhow!("project was not remote")));
return Task::ready(Err(anyhow!("project was not remote")));
};
if let Some(context) = self.loaded_context_for_id(&context_id, cx) {

View File

@@ -23,7 +23,6 @@ log.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
deepseek = { workspace = true, features = ["schemars"] }
mistral = { workspace = true, features = ["schemars"] }
schemars.workspace = true
serde.workspace = true
settings.workspace = true

View File

@@ -10,7 +10,6 @@ use deepseek::Model as DeepseekModel;
use gpui::{App, Pixels, SharedString};
use language_model::{CloudModel, LanguageModel};
use lmstudio::Model as LmStudioModel;
use mistral::Model as MistralModel;
use ollama::Model as OllamaModel;
use schemars::{JsonSchema, schema::Schema};
use serde::{Deserialize, Serialize};
@@ -72,11 +71,6 @@ pub enum AssistantProviderContentV1 {
default_model: Option<DeepseekModel>,
api_url: Option<String>,
},
#[serde(rename = "mistral")]
Mistral {
default_model: Option<MistralModel>,
api_url: Option<String>,
},
}
#[derive(Default, Clone, Debug)]
@@ -100,7 +94,6 @@ pub struct AssistantSettings {
pub single_file_review: bool,
pub model_parameters: Vec<LanguageModelParameters>,
pub preferred_completion_mode: CompletionMode,
pub enable_feedback: bool,
}
impl AssistantSettings {
@@ -255,12 +248,6 @@ impl AssistantSettingsContent {
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Mistral { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "mistral".into(),
model: model.id().to_string(),
})
}
}),
inline_assistant_model: None,
commit_message_model: None,
@@ -274,7 +261,6 @@ impl AssistantSettingsContent {
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
},
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
},
@@ -305,7 +291,6 @@ impl AssistantSettingsContent {
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
},
None => AssistantSettingsContentV2::default(),
}
@@ -588,7 +573,6 @@ impl Default for VersionedAssistantSettingsContent {
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
})
}
}
@@ -663,10 +647,6 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: normal
preferred_completion_mode: Option<CompletionMode>,
/// Whether to show thumb buttons for feedback in the agent panel.
///
/// Default: true
enable_feedback: Option<bool>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
@@ -704,7 +684,7 @@ impl JsonSchema for LanguageModelProviderSetting {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"amazon-bedrock".into(),
"bedrock".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),
@@ -712,7 +692,6 @@ impl JsonSchema for LanguageModelProviderSetting {
"zed.dev".into(),
"copilot_chat".into(),
"deepseek".into(),
"mistral".into(),
]),
..Default::default()
}
@@ -874,7 +853,6 @@ impl Settings for AssistantSettings {
&mut settings.preferred_completion_mode,
value.preferred_completion_mode,
);
merge(&mut settings.enable_feedback, value.enable_feedback);
settings
.model_parameters
@@ -1011,7 +989,6 @@ mod tests {
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
enable_feedback: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
},

View File

@@ -9,7 +9,6 @@ use anyhow::Result;
use futures::StreamExt;
use futures::stream::{self, BoxStream};
use gpui::{App, SharedString, Task, WeakEntity, Window};
use language::HighlightId;
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
use serde::{Deserialize, Serialize};
@@ -17,7 +16,6 @@ use std::{
ops::Range,
sync::{Arc, atomic::AtomicBool},
};
use ui::ActiveTheme;
use workspace::{Workspace, ui::IconName};
pub fn init(cx: &mut App) {
@@ -327,18 +325,6 @@ impl SlashCommandLine {
}
}
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;

View File

@@ -35,6 +35,7 @@ rope.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
terminal_view.workspace = true
text.workspace = true
toml.workspace = true
ui.workspace = true

View File

@@ -12,6 +12,11 @@ mod selection_command;
mod streaming_example_command;
mod symbols_command;
mod tab_command;
mod terminal_command;
use gpui::App;
use language::{CodeLabel, HighlightId};
use ui::ActiveTheme as _;
pub use crate::cargo_workspace_command::*;
pub use crate::context_server_command::*;
@@ -27,5 +32,16 @@ pub use crate::selection_command::*;
pub use crate::streaming_example_command::*;
pub use crate::symbols_command::*;
pub use crate::tab_command::*;
pub use crate::terminal_command::*;
use assistant_slash_command::create_label_for_command;
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str(command_name, None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
}

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_slash_command::{
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
SlashCommandOutputSection, SlashCommandResult,
@@ -84,7 +84,9 @@ impl SlashCommand for ContextServerSlashCommand {
if let Some(server) = self.store.read(cx).get_running_server(&server_id) {
cx.foreground_executor().spawn(async move {
let protocol = server.client().context("Context server not initialized")?;
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
let completion_result = protocol
.completion(
@@ -137,16 +139,21 @@ impl SlashCommand for ContextServerSlashCommand {
let store = self.store.read(cx);
if let Some(server) = store.get_running_server(&server_id) {
cx.foreground_executor().spawn(async move {
let protocol = server.client().context("Context server not initialized")?;
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
anyhow::ensure!(
result
.messages
.iter()
.all(|msg| matches!(msg.role, context_server::types::Role::User)),
"Prompt contains non-user roles, which is not supported"
);
// Check that there are only user roles
if result
.messages
.iter()
.any(|msg| !matches!(msg.role, context_server::types::Role::User))
{
return Err(anyhow!(
"Prompt contains non-user roles, which is not supported"
));
}
// Extract text from user messages into a single prompt string
let mut prompt = result
@@ -185,7 +192,9 @@ impl SlashCommand for ContextServerSlashCommand {
}
fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String, String)> {
anyhow::ensure!(!arguments.is_empty(), "No arguments given");
if arguments.is_empty() {
return Err(anyhow!("No arguments given"));
}
match &prompt.arguments {
Some(args) if args.len() == 1 => {
@@ -193,16 +202,16 @@ fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String,
let arg_value = arguments.join(" ");
Ok((arg_name, arg_value))
}
Some(_) => anyhow::bail!("Prompt must have exactly one argument"),
None => anyhow::bail!("Prompt has no arguments"),
Some(_) => Err(anyhow!("Prompt must have exactly one argument")),
None => Err(anyhow!("Prompt has no arguments")),
}
}
fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<String, String>> {
match &prompt.arguments {
Some(args) if args.len() > 1 => {
anyhow::bail!("Prompt has more than one argument, which is not supported");
}
Some(args) if args.len() > 1 => Err(anyhow!(
"Prompt has more than one argument, which is not supported"
)),
Some(args) if args.len() == 1 => {
if !arguments.is_empty() {
let mut map = HashMap::default();
@@ -211,15 +220,15 @@ fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<Str
} else if arguments.is_empty() && args[0].required == Some(false) {
Ok(HashMap::default())
} else {
anyhow::bail!("Prompt expects argument but none given");
Err(anyhow!("Prompt expects argument but none given"))
}
}
Some(_) | None => {
anyhow::ensure!(
arguments.is_empty(),
"Prompt expects no arguments but some were given"
);
Ok(HashMap::default())
if arguments.is_empty() {
Ok(HashMap::default())
} else {
Err(anyhow!("Prompt expects no arguments but some were given"))
}
}
}
}

View File

@@ -118,7 +118,10 @@ impl SlashCommand for DeltaSlashCommand {
}
}
anyhow::ensure!(changes_detected, "no new changes detected");
if !changes_detected {
return Err(anyhow!("no new changes detected"));
}
Ok(output.to_event_stream())
})
}

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
@@ -189,7 +189,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
window.spawn(cx, async move |_| {
task.await?
.map(|output| output.to_event_stream())
.context("No diagnostics found")
.ok_or_else(|| anyhow!("No diagnostics found"))
})
}
}

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Result, anyhow, bail};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
@@ -52,16 +52,15 @@ impl DocsSlashCommand {
.is_none()
{
let index_provider_deps = maybe!({
let workspace = workspace.clone().ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace
.as_ref()
.context("no workspace")?
.upgrade()
.context("workspace dropped")?;
.ok_or_else(|| anyhow!("workspace was dropped"))?;
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let cargo_workspace_root = Self::path_to_cargo_toml(project, cx)
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
.context("no Cargo workspace root found")?;
.ok_or_else(|| anyhow!("no Cargo workspace root found"))?;
anyhow::Ok((fs, cargo_workspace_root))
});
@@ -79,11 +78,10 @@ impl DocsSlashCommand {
.is_none()
{
let http_client = maybe!({
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace
.as_ref()
.context("no workspace")?
.upgrade()
.context("workspace was dropped")?;
.ok_or_else(|| anyhow!("workspace was dropped"))?;
let project = workspace.read(cx).project().clone();
anyhow::Ok(project.read(cx).client().http_client())
});
@@ -176,7 +174,7 @@ impl SlashCommand for DocsSlashCommand {
let args = DocsSlashCommandArgs::parse(arguments);
let store = args
.provider()
.context("no docs provider specified")
.ok_or_else(|| anyhow!("no docs provider specified"))
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
cx.background_spawn(async move {
fn build_completions(items: Vec<String>) -> Vec<ArgumentCompletion> {
@@ -289,7 +287,7 @@ impl SlashCommand for DocsSlashCommand {
let task = cx.background_spawn({
let store = args
.provider()
.context("no docs provider specified")
.ok_or_else(|| anyhow!("no docs provider specified"))
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
async move {
let (provider, key) = match args.clone() {

View File

@@ -3,7 +3,7 @@ use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Context, Result, anyhow, bail};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,

View File

@@ -230,10 +230,7 @@ fn collect_files(
})
.collect::<anyhow::Result<Vec<custom_path_matcher::PathMatcher>>>()
else {
return futures::stream::once(async {
anyhow::bail!("invalid path");
})
.boxed();
return futures::stream::once(async { Err(anyhow!("invalid path")) }).boxed();
};
let project_handle = project.downgrade();

View File

@@ -1,7 +1,6 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use crate::{TerminalView, terminal_panel::TerminalPanel};
use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
@@ -9,10 +8,11 @@ use assistant_slash_command::{
};
use gpui::{App, Entity, Task, WeakEntity};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use ui::prelude::*;
use workspace::{Workspace, dock::Panel};
use assistant_slash_command::create_label_for_command;
use super::create_label_for_command;
pub struct TerminalSlashCommand;

View File

@@ -1,5 +1,5 @@
use crate::ActionLog;
use anyhow::{Context as _, Result};
use anyhow::{Result, anyhow};
use gpui::{AsyncApp, Entity};
use language::{OutlineItem, ParseStatus};
use project::Project;
@@ -22,7 +22,7 @@ pub async fn file_outline(
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&path, cx)
.with_context(|| format!("Path {path} not found in project"))
.ok_or_else(|| anyhow!("Path {path} not found in project"))
})??;
project
@@ -41,9 +41,9 @@ pub async fn file_outline(
}
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let outline = snapshot
.outline(None)
.context("No outline information available for this file at path {path}")?;
let Some(outline) = snapshot.outline(None) else {
return Err(anyhow!("No outline information available for this file."));
};
render_outline(
outline

View File

@@ -27,10 +27,12 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
const UNSUPPORTED_KEYS: [&str; 4] = ["if", "then", "else", "$ref"];
for key in UNSUPPORTED_KEYS {
anyhow::ensure!(
!obj.contains_key(key),
"Schema cannot be made compatible because it contains \"{key}\""
);
if obj.contains_key(key) {
return Err(anyhow::anyhow!(
"Schema cannot be made compatible because it contains \"{}\" ",
key
));
}
}
const KEYS_TO_REMOVE: [&str; 5] = [

View File

@@ -35,13 +35,13 @@ indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
linkme.workspace = true
log.workspace = true
markdown.workspace = true
open.workspace = true
paths.workspace = true
portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
regex.workspace = true
rust-embed.workspace = true
schemars.workspace = true

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, AppContext, Entity, Task};
@@ -107,13 +107,17 @@ impl Tool for CopyPathTool {
});
cx.background_spawn(async move {
let _ = copy_task.await.with_context(|| {
format!(
"Copying {} to {}",
input.source_path, input.destination_path
)
})?;
Ok(format!("Copied {} to {}", input.source_path, input.destination_path).into())
match copy_task.await {
Ok(_) => Ok(
format!("Copied {} to {}", input.source_path, input.destination_path).into(),
),
Err(err) => Err(anyhow!(
"Failed to copy {} to {}: {}",
input.source_path,
input.destination_path,
err
)),
}
})
.into()
}

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
@@ -86,7 +86,7 @@ impl Tool for CreateDirectoryTool {
project.create_entry(project_path.clone(), true, cx)
})?
.await
.with_context(|| format!("Creating directory {destination_path}"))?;
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
Ok(format!("Created directory {destination_path}").into())
})

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
@@ -122,17 +122,19 @@ impl Tool for DeletePathTool {
}
}
let deletion_task = project
.update(cx, |project, cx| {
project.delete_file(project_path, false, cx)
})?
.with_context(|| {
format!("Couldn't delete {path_str} because that path isn't in this project.")
})?;
deletion_task
.await
.with_context(|| format!("Deleting {path_str}"))?;
Ok(format!("Deleted {path_str}").into())
let delete = project.update(cx, |project, cx| {
project.delete_file(project_path, false, cx)
})?;
match delete {
Some(deletion_task) => match deletion_task.await {
Ok(()) => Ok(format!("Deleted {path_str}").into()),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
},
None => Err(anyhow!(
"Couldn't delete {path_str} because that path isn't in this project."
)),
}
})
.into()
}

View File

@@ -24,7 +24,6 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll};
use streaming_diff::{CharOperation, StreamingDiff};
use util::debug_panic;
#[derive(Serialize)]
struct CreateFilePromptTemplate {
@@ -544,11 +543,6 @@ impl EditAgent {
if last_message.content.is_empty() {
conversation.messages.pop();
}
} else {
debug_panic!(
"Last message must be an Assistant tool calling! Got {:?}",
last_message.content
);
}
}

View File

@@ -3,9 +3,9 @@ use crate::{
ReadFileToolInput,
edit_file_tool::{EditFileMode, EditFileToolInput},
grep_tool::GrepToolInput,
list_directory_tool::ListDirectoryToolInput,
};
use Role::*;
use anyhow::anyhow;
use assistant_tool::ToolRegistry;
use client::{Client, UserStore};
use collections::HashMap;
@@ -15,10 +15,9 @@ use gpui::{AppContext, TestAppContext};
use indoc::{formatdoc, indoc};
use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId,
};
use project::Project;
use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext};
use rand::prelude::*;
use reqwest_client::ReqwestClient;
use serde_json::json;
@@ -26,7 +25,6 @@ use std::{
cmp::Reverse,
fmt::{self, Display},
io::Write as _,
str::FromStr,
sync::mpsc,
};
use util::path;
@@ -41,8 +39,8 @@ fn eval_extract_handle_command_output() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -82,9 +80,11 @@ fn eval_extract_handle_command_output() {
)],
),
],
Some(input_file_content.into()),
EvalAssertion::assert_eq(output_file_content),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::assert_eq(output_file_content),
},
);
}
@@ -98,8 +98,8 @@ fn eval_delete_run_git_blame() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -136,9 +136,11 @@ fn eval_delete_run_git_blame() {
)],
),
],
Some(input_file_content.into()),
EvalAssertion::assert_eq(output_file_content),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::assert_eq(output_file_content),
},
);
}
@@ -151,8 +153,8 @@ fn eval_translate_doc_comments() {
eval(
200,
1.,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -189,9 +191,11 @@ fn eval_translate_doc_comments() {
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff("Doc comments were translated to Italian"),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff("Doc comments were translated to Italian"),
},
);
}
@@ -205,8 +209,8 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
@@ -302,12 +306,14 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- The compile_parser_to_wasm method has been changed to use wasi-sdk
- ureq is used to download the SDK for current platform and architecture
"}),
),
},
);
}
@@ -318,10 +324,10 @@ fn eval_disable_cursor_blinking() {
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
let edit_description = "Comment out the call to `BlinkManager::enable`";
eval(
100,
200,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(User, [text("Let's research how to cursor blinking works.")]),
message(
Assistant,
@@ -375,13 +381,15 @@ fn eval_disable_cursor_blinking() {
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- Calls to BlinkManager in `observe_window_activation` were commented out
- The call to `blink_manager.enable` above the call to show_cursor_names was commented out
- All the edits have valid indentation
"}),
),
},
);
}
@@ -394,8 +402,8 @@ fn eval_from_pixels_constructor() {
eval(
100,
0.95,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(indoc! {"
@@ -567,12 +575,14 @@ fn eval_from_pixels_constructor() {
)],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
),
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(indoc! {"
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
},
);
}
@@ -580,13 +590,12 @@ fn eval_from_pixels_constructor() {
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_zode() {
let input_file_path = "root/zode.py";
let input_content = None;
let edit_description = "Create the main Zode CLI script";
eval(
200,
1.,
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
message(
Assistant,
@@ -644,8 +653,10 @@ fn eval_zode() {
],
),
],
input_content,
EvalAssertion::new(async move |sample, _, _cx| {
input_path: input_file_path.into(),
input_content: None,
edit_description: edit_description.into(),
assertion: EvalAssertion::new(async move |sample, _, _cx| {
let invalid_starts = [' ', '`', '\n'];
let mut message = String::new();
for start in invalid_starts {
@@ -669,7 +680,7 @@ fn eval_zode() {
})
}
}),
),
},
);
}
@@ -682,8 +693,8 @@ fn eval_add_overwrite_test() {
eval(
200,
0.5, // TODO: make this eval better
EvalInput::from_conversation(
vec![
EvalInput {
conversation: vec![
message(
User,
[text(indoc! {"
@@ -887,93 +898,13 @@ fn eval_add_overwrite_test() {
],
),
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff(
"A new test for overwritten files was created, without changing any previous test",
),
),
);
}
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_create_empty_file() {
// Check that Edit Agent can create a file without writing its
// thoughts into it. This issue is not specific to empty files, but
// it's easier to reproduce with them.
//
//
// Model | Pass rate
// ============================================
//
// --------------------------------------------
// Prompt version: 2025-05-21
// --------------------------------------------
//
// claude-3.7-sonnet | 1.00
// gemini-2.5-pro-preview-03-25 | 1.00
// gemini-2.5-flash-preview-04-17 | 1.00
// gpt-4.1 | 1.00
//
//
// TODO: gpt-4.1-mini errored 38 times:
// "data did not match any variant of untagged enum ResponseStreamResult"
//
let input_file_content = None;
let expected_output_content = String::new();
eval(
100,
0.99,
EvalInput::from_conversation(
vec![
message(User, [text("Create a second empty todo file ")]),
message(
Assistant,
[
text(formatdoc! {"
I'll help you create a second empty todo file.
First, let me examine the project structure to see if there's already a todo file, which will help me determine the appropriate name and location for the second one.
"}),
tool_use(
"toolu_01GAF8TtsgpjKxCr8fgQLDgR",
"list_directory",
ListDirectoryToolInput {
path: "root".to_string(),
},
),
],
),
message(
User,
[tool_result(
"toolu_01GAF8TtsgpjKxCr8fgQLDgR",
"list_directory",
"root/TODO\nroot/TODO2\nroot/new.txt\n",
)],
),
message(
Assistant,
[
text(formatdoc! {"
I can see there's already a `TODO` file in the `root` directory. Let me create a second empty todo file called `TODO3` in the same directory:
"}),
tool_use(
"toolu_01Tb3iQ9griqSYMmVuykQPWU",
"edit_file",
EditFileToolInput {
display_description: "Create empty TODO3 file".to_string(),
mode: EditFileMode::Create,
path: "root/TODO3".into(),
},
),
],
),
],
input_file_content,
// Bad behavior is to write something like
// "I'll create an empty TODO3 file as requested."
EvalAssertion::assert_eq(expected_output_content),
),
},
);
}
@@ -1032,46 +963,12 @@ fn tool_result(
#[derive(Clone)]
struct EvalInput {
conversation: Vec<LanguageModelRequestMessage>,
edit_file_input: EditFileToolInput,
input_path: PathBuf,
input_content: Option<String>,
edit_description: String,
assertion: EvalAssertion,
}
impl EvalInput {
fn from_conversation(
conversation: Vec<LanguageModelRequestMessage>,
input_content: Option<String>,
assertion: EvalAssertion,
) -> Self {
let msg = conversation.last().expect("Conversation must not be empty");
if msg.role != Role::Assistant {
panic!("Conversation must end with an assistant message");
}
let tool_use = msg
.content
.iter()
.flat_map(|content| match content {
MessageContent::ToolUse(tool_use) if tool_use.name == "edit_file".into() => {
Some(tool_use)
}
_ => None,
})
.next()
.expect("Conversation must end with an edit_file tool use")
.clone();
let edit_file_input: EditFileToolInput =
serde_json::from_value(tool_use.input.clone()).unwrap();
EvalInput {
conversation,
edit_file_input,
input_content,
assertion,
}
}
}
#[derive(Clone)]
struct EvalSample {
text: String,
@@ -1179,7 +1076,10 @@ impl EvalAssertion {
}
}
anyhow::bail!("No score found in response. Raw output: {output}");
Err(anyhow!(
"No score found in response. Raw output: {}",
output
))
})
}
@@ -1316,7 +1216,7 @@ fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usiz
passed_count as f64 / evaluated_count as f64
};
print!(
"\r\x1b[KEvaluated {}/{} ({:.2}% passed)",
"\r\x1b[KEvaluated {}/{} ({:.2}%)",
evaluated_count,
iterations,
passed_ratio * 100.0
@@ -1355,21 +1255,13 @@ impl EditAgentTest {
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let agent_model = SelectedModel::from_str(
&std::env::var("ZED_AGENT_MODEL")
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
)
.unwrap();
let judge_model = SelectedModel::from_str(
&std::env::var("ZED_JUDGE_MODEL")
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
)
.unwrap();
let (agent_model, judge_model) = cx
.update(|cx| {
cx.spawn(async move |cx| {
let agent_model = Self::load_model(&agent_model, cx).await;
let judge_model = Self::load_model(&judge_model, cx).await;
let agent_model =
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
let judge_model =
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
(agent_model.unwrap(), judge_model.unwrap())
})
})
@@ -1384,17 +1276,15 @@ impl EditAgentTest {
}
async fn load_model(
selected_model: &SelectedModel,
provider: &str,
id: &str,
cx: &mut AsyncApp,
) -> Result<Arc<dyn LanguageModel>> {
let (provider, model) = cx.update(|cx| {
let models = LanguageModelRegistry::read_global(cx);
let model = models
.available_models(cx)
.find(|model| {
model.provider_id() == selected_model.provider
&& model.id() == selected_model.model
})
.find(|model| model.provider_id().0 == provider && model.id().0 == id)
.unwrap();
let provider = models.provider(&model.provider_id()).unwrap();
(provider, model)
@@ -1407,7 +1297,7 @@ impl EditAgentTest {
let path = self
.project
.read_with(cx, |project, cx| {
project.find_project_path(eval.edit_file_input.path, cx)
project.find_project_path(eval.input_path, cx)
})
.unwrap();
let buffer = self
@@ -1415,68 +1305,31 @@ impl EditAgentTest {
.update(cx, |project, cx| project.open_buffer(path, cx))
.await
.unwrap();
let tools = cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
})
.collect::<Vec<_>>()
});
let tool_names = tools
.iter()
.map(|tool| tool.name.clone())
.collect::<Vec<_>>();
let worktrees = vec![WorktreeContext {
root_name: "root".to_string(),
rules_file: None,
}];
let prompt_builder = PromptBuilder::new(None)?;
let project_context = ProjectContext::new(worktrees, Vec::default());
let system_prompt = prompt_builder.generate_assistant_system_prompt(
&project_context,
&ModelContext {
available_tools: tool_names,
},
)?;
let has_system_prompt = eval
.conversation
.first()
.map_or(false, |msg| msg.role == Role::System);
let messages = if has_system_prompt {
eval.conversation
} else {
[LanguageModelRequestMessage {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
}]
.into_iter()
.chain(eval.conversation)
.collect::<Vec<_>>()
};
let conversation = LanguageModelRequest {
messages,
tools,
messages: eval.conversation,
tools: cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
})
.collect()
}),
..Default::default()
};
let edit_output = if matches!(eval.edit_file_input.mode, EditFileMode::Edit) {
if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
}
let edit_output = if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
let (edit_output, _) = self.agent.edit(
buffer.clone(),
eval.edit_file_input.display_description,
eval.edit_description,
&conversation,
&mut cx.to_async(),
);
@@ -1484,7 +1337,7 @@ impl EditAgentTest {
} else {
let (edit_output, _) = self.agent.overwrite(
buffer.clone(),
eval.edit_file_input.display_description,
eval.edit_description,
&conversation,
&mut cx.to_async(),
);

View File

@@ -98,21 +98,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.with_context(|| format!("parsing sha from {line}"))?;
.ok_or_else(|| anyhow!("failed to parse sha"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing original line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing final line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing line count from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -80,7 +80,7 @@ async fn run_git_blame(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("starting git blame process")?;
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
let stdin = child
.stdin
@@ -92,7 +92,10 @@ async fn run_git_blame(
}
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
let output = child
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -100,7 +103,7 @@ async fn run_git_blame(
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
anyhow::bail!("git blame process failed: {stderr}");
return Err(anyhow!("git blame process failed: {}", stderr));
}
Ok(String::from_utf8(output.stdout)?)
@@ -141,21 +144,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.with_context(|| format!("parsing sha from {line}"))?;
.ok_or_else(|| anyhow!("failed to parse sha"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing original line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing final line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing line count from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -1249,7 +1249,7 @@ pub struct ActiveDiagnosticGroup {
}
#[derive(Debug, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum ActiveDiagnostic {
None,
All,
@@ -5272,7 +5272,7 @@ impl Editor {
task.await?;
}
anyhow::Ok(())
Ok::<_, anyhow::Error>(())
})
.detach_and_log_err(cx);
}
@@ -10369,8 +10369,8 @@ impl Editor {
.map(|line| {
line.strip_prefix(&line_prefix)
.or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start()))
.with_context(|| {
format!("line did not start with prefix {line_prefix:?}: {line:?}")
.ok_or_else(|| {
anyhow!("line did not start with prefix {line_prefix:?}: {line:?}")
})
})
.collect::<Result<Vec<_>, _>>()
@@ -16944,7 +16944,7 @@ impl Editor {
Err(err) => {
let message = format!("Failed to copy permalink: {err}");
anyhow::Result::<()>::Err(err).log_err();
Err::<(), anyhow::Error>(err).log_err();
if let Some(workspace) = workspace {
workspace
@@ -16999,7 +16999,7 @@ impl Editor {
Err(err) => {
let message = format!("Failed to open permalink: {err}");
anyhow::Result::<()>::Err(err).log_err();
Err::<(), anyhow::Error>(err).log_err();
if let Some(workspace) = workspace {
workspace

View File

@@ -80,7 +80,7 @@ async fn run_git_blame(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("starting git blame process")?;
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
let stdin = child
.stdin
@@ -92,7 +92,10 @@ async fn run_git_blame(
}
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
let output = child
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
handle_command_output(output)
}
@@ -104,7 +107,7 @@ fn handle_command_output(output: std::process::Output) -> Result<String> {
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
anyhow::bail!("git blame process failed: {stderr}");
return Err(anyhow!("git blame process failed: {}", stderr));
}
Ok(String::from_utf8(output.stdout)?)
@@ -145,21 +148,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.with_context(|| format!("parsing sha from {line}"))?;
.ok_or_else(|| anyhow!("failed to parse sha"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing original line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing final line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing line count from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -80,7 +80,7 @@ async fn run_git_blame(
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("starting git blame process")?;
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;
let stdin = child
.stdin
@@ -92,7 +92,10 @@ async fn run_git_blame(
}
stdin.flush().await?;
let output = child.output().await.context("reading git blame output")?;
let output = child
.output()
.await
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -100,7 +103,7 @@ async fn run_git_blame(
if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) {
return Ok(String::new());
}
anyhow::bail!("git blame process failed: {stderr}");
return Err(anyhow!("git blame process failed: {}", stderr));
}
Ok(String::from_utf8(output.stdout)?)
@@ -141,21 +144,21 @@ impl BlameEntry {
let sha = parts
.next()
.and_then(|line| line.parse::<Oid>().ok())
.with_context(|| format!("parsing sha from {line}"))?;
.ok_or_else(|| anyhow!("failed to parse sha"))?;
let original_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing original line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse original line number"))?;
let final_line_number = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing final line number from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let line_count = parts
.next()
.and_then(|line| line.parse::<u32>().ok())
.with_context(|| format!("parsing line count from {line}"))?;
.ok_or_else(|| anyhow!("Failed to parse final line number"))?;
let start_line = final_line_number.saturating_sub(1);
let end_line = start_line + line_count;

View File

@@ -20,7 +20,7 @@ use std::{
#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))]
use anyhow::Error;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context, Result, anyhow};
use etcetera::BaseStrategy as _;
use fs4::fs_std::FileExt;
use indoc::indoc;
@@ -875,13 +875,16 @@ impl Loader {
FileExt::unlock(lock_file)?;
fs::remove_file(lock_path)?;
anyhow::ensure!(
output.status.success(),
"Parser compilation failed.\nStdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
Ok(())
if output.status.success() {
Ok(())
} else {
Err(anyhow!(
"Parser compilation failed.\nStdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
))
}
}
#[cfg(unix)]
@@ -938,13 +941,17 @@ impl Loader {
.map(|f| format!(" `{f}`"))
.collect::<Vec<_>>()
.join("\n");
anyhow::bail!(format!(indoc! {"
Missing required functions in the external scanner, parsing won't work without these!
{missing}
return Err(anyhow!(format!(
indoc! {"
Missing required functions in the external scanner, parsing won't work without these!
You can read more about this at https://tree-sitter.github.io/tree-sitter/creating-parsers/4-external-scanners
"}));
{}
You can read more about this at https://tree-sitter.github.io/tree-sitter/creating-parsers/4-external-scanners
"},
missing,
)));
}
}
}
@@ -1001,9 +1008,9 @@ impl Loader {
{
EmccSource::Podman
} else {
anyhow::bail!(
return Err(anyhow!(
"You must have either emcc, docker, or podman on your PATH to run this command"
);
));
};
let mut command = match source {
@@ -1096,11 +1103,12 @@ impl Loader {
.spawn()
.with_context(|| "Failed to run emcc command")?
.wait()?;
anyhow::ensure!(status.success(), "emcc command failed");
let source_path = src_path.join(output_name);
fs::rename(&source_path, &output_path).with_context(|| {
format!("failed to rename wasm output file from {source_path:?} to {output_path:?}")
})?;
if !status.success() {
return Err(anyhow!("emcc command failed"));
}
fs::rename(src_path.join(output_name), output_path)
.context("failed to rename wasm output file")?;
Ok(())
}
@@ -1177,8 +1185,11 @@ impl Loader {
.map(|path| {
let path = parser_path.join(path);
// prevent p being above/outside of parser_path
anyhow::ensure!(path.starts_with(parser_path), "External file path {path:?} is outside of parser directory {parser_path:?}");
Ok(path)
if path.starts_with(parser_path) {
Ok(path)
} else {
Err(anyhow!("External file path {path:?} is outside of parser directory {parser_path:?}"))
}
})
.collect::<Result<Vec<_>>>()
}).transpose()?,
@@ -1313,8 +1324,11 @@ impl Loader {
let name = GRAMMAR_NAME_REGEX
.captures(&first_three_lines)
.and_then(|c| c.get(1))
.with_context(|| {
format!("Failed to parse the language name from grammar.json at {grammar_path:?}")
.ok_or_else(|| {
anyhow!(
"Failed to parse the language name from grammar.json at {}",
grammar_path.display()
)
})?;
Ok(name.as_str().to_string())
@@ -1333,7 +1347,7 @@ impl Loader {
{
Ok(config.0)
} else {
anyhow::bail!("Unknown scope '{scope}'")
Err(anyhow!("Unknown scope '{scope}'"))
}
} else if let Some((lang, _)) = self
.language_configuration_for_file_name(path)
@@ -1357,7 +1371,7 @@ impl Loader {
} else if let Some(lang) = self.language_configuration_for_first_line_regex(path)? {
Ok(lang.0)
} else {
anyhow::bail!("No language found");
Err(anyhow!("No language found"))
}
}

View File

@@ -3,7 +3,7 @@ use crate::{
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
schema::json_schema_for,
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
ToolUseStatus,
@@ -22,7 +22,7 @@ use language::{
};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use project::{Project, ProjectPath};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -38,7 +38,7 @@ use workspace::Workspace;
pub struct EditFileTool;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be
/// shown in the UI and also passed to another model to perform the edit.
@@ -86,7 +86,7 @@ pub struct EditFileToolInput {
pub mode: EditFileMode,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum EditFileMode {
Edit,
@@ -171,9 +171,12 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let project_path = match resolve_path(&input, project.clone(), cx) {
Ok(path) => path,
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.display()
)))
.into();
};
let card = window.and_then(|window| {
@@ -196,6 +199,20 @@ impl Tool for EditFileTool {
})?
.await?;
let exists = buffer.read_with(cx, |buffer, _| {
buffer
.file()
.as_ref()
.map_or(false, |file| file.disk_state().exists())
})?;
let create_or_overwrite = match input.mode {
EditFileMode::Create | EditFileMode::Overwrite => true,
_ => false,
};
if !create_or_overwrite && !exists {
return Err(anyhow!("{} not found", input.path.display()));
}
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
.background_spawn({
@@ -204,15 +221,15 @@ impl Tool for EditFileTool {
})
.await;
let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) {
edit_agent.edit(
let (output, mut events) = if create_or_overwrite {
edit_agent.overwrite(
buffer.clone(),
input.display_description.clone(),
&request,
cx,
)
} else {
edit_agent.overwrite(
edit_agent.edit(
buffer.clone(),
input.display_description.clone(),
&request,
@@ -279,15 +296,15 @@ impl Tool for EditFileTool {
let input_path = input.path.display();
if diff.is_empty() {
anyhow::ensure!(
!hallucinated_old_text,
formatdoc! {"
Some edits were produced but none of them could be applied.
Read the relevant sections of {input_path} again so that
I can perform the requested edits.
"}
);
Ok("No edits were made.".to_string().into())
if hallucinated_old_text {
Err(anyhow!(formatdoc! {"
Some edits were produced but none of them could be applied.
Read the relevant sections of {input_path} again so that
I can perform the requested edits.
"}))
} else {
Ok("No edits were made.".to_string().into())
}
} else {
Ok(ToolResultOutput {
content: ToolResultContent::Text(format!(
@@ -332,71 +349,6 @@ impl Tool for EditFileTool {
}
}
/// Validate that the file path is valid, meaning:
///
/// - For `edit` and `overwrite`, the path must point to an existing file.
/// - For `create`, the file must not already exist, but it's parent dir must exist.
fn resolve_path(
input: &EditFileToolInput,
project: Entity<Project>,
cx: &mut App,
) -> Result<ProjectPath> {
let project = project.read(cx);
match input.mode {
EditFileMode::Edit | EditFileMode::Overwrite => {
let path = project
.find_project_path(&input.path, cx)
.context("Can't edit file: path not found")?;
let entry = project
.entry_for_path(&path, cx)
.context("Can't edit file: path not found")?;
anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory");
Ok(path)
}
EditFileMode::Create => {
if let Some(path) = project.find_project_path(&input.path, cx) {
anyhow::ensure!(
project.entry_for_path(&path, cx).is_none(),
"Can't create file: file already exists"
);
}
let parent_path = input
.path
.parent()
.context("Can't create file: incorrect path")?;
let parent_project_path = project.find_project_path(&parent_path, cx);
let parent_entry = parent_project_path
.as_ref()
.and_then(|path| project.entry_for_path(&path, cx))
.context("Can't create file: parent directory doesn't exist")?;
anyhow::ensure!(
parent_entry.is_dir(),
"Can't create file: parent is not a directory"
);
let file_name = input
.path
.file_name()
.context("Can't create file: invalid filename")?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
path: Arc::from(parent.path.join(file_name)),
..parent
});
new_file_path.context("Can't create file")
}
}
}
pub struct EditFileToolCard {
path: PathBuf,
editor: Entity<Editor>,
@@ -448,7 +400,7 @@ impl EditFileToolCard {
diff_task: None,
preview_expanded: true,
error_expanded: None,
full_height_expanded: true,
full_height_expanded: false,
total_lines: None,
}
}
@@ -917,7 +869,6 @@ async fn build_buffer_diff(
#[cfg(test)]
mod tests {
use super::*;
use client::TelemetrySettings;
use fs::FakeFs;
use gpui::TestAppContext;
use language_model::fake_provider::FakeLanguageModel;
@@ -957,102 +908,10 @@ mod tests {
.await;
assert_eq!(
result.unwrap_err().to_string(),
"Can't edit file: path not found"
"root/nonexistent_file.txt not found"
);
}
#[gpui::test]
async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) {
let mode = &EditFileMode::Create;
let result = test_resolve_path(mode, "root/new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
let result = test_resolve_path(mode, "dir/new.txt", cx);
assert_resolved_path_eq(result.await, "dir/new.txt");
let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't create file: file already exists"
);
let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't create file: parent directory doesn't exist"
);
}
#[gpui::test]
async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) {
let mode = &EditFileMode::Edit;
let path_with_root = "root/dir/subdir/existing.txt";
let path_without_root = "dir/subdir/existing.txt";
let result = test_resolve_path(mode, path_with_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, path_without_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't edit file: path not found"
);
let result = test_resolve_path(mode, "root/dir", cx);
assert_eq!(
result.await.unwrap_err().to_string(),
"Can't edit file: path is a directory"
);
}
async fn test_resolve_path(
mode: &EditFileMode,
path: &str,
cx: &mut TestAppContext,
) -> anyhow::Result<ProjectPath> {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"dir": {
"subdir": {
"existing.txt": "hello"
}
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let input = EditFileToolInput {
display_description: "Some edit".into(),
path: path.into(),
mode: mode.clone(),
};
let result = cx.update(|cx| resolve_path(&input, project, cx));
result
}
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
let actual = path
.expect("Should return valid path")
.path
.to_str()
.unwrap()
.replace("\\", "/"); // Naive Windows paths normalization
assert_eq!(actual, expected);
}
#[test]
fn still_streaming_ui_text_with_path() {
let input = json!({
@@ -1125,7 +984,6 @@ mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
TelemetrySettings::register(cx);
Project::init_settings(cx);
});
}

View File

@@ -109,7 +109,7 @@ impl Tool for GrepTool {
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();
return Task::ready(Err(anyhow!("Failed to parse input: {}", error))).into();
}
};
@@ -122,7 +122,7 @@ impl Tool for GrepTool {
) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid include glob pattern: {error}"))).into();
return Task::ready(Err(anyhow!("invalid include glob pattern: {}", error))).into();
}
};

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
@@ -117,10 +117,17 @@ impl Tool for MovePathTool {
});
cx.background_spawn(async move {
let _ = rename_task.await.with_context(|| {
format!("Moving {} to {}", input.source_path, input.destination_path)
})?;
Ok(format!("Moved {} to {}", input.source_path, input.destination_path).into())
match rename_task.await {
Ok(_) => {
Ok(format!("Moved {} to {}", input.source_path, input.destination_path).into())
}
Err(err) => Err(anyhow!(
"Failed to move {} to {}: {}",
input.source_path,
input.destination_path,
err
)),
}
})
.into()
}

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{ToolResultContent, outline};
use gpui::{AnyWindowHandle, App, Entity, Task};
@@ -129,7 +129,7 @@ impl Tool for ReadFileTool {
let language_model_image = cx
.update(|cx| LanguageModelImage::from_image(image, cx))?
.await
.context("processing image")?;
.ok_or_else(|| anyhow!("Failed to process image"))?;
Ok(ToolResultOutput {
content: ToolResultContent::Image(language_model_image),
@@ -152,7 +152,7 @@ impl Tool for ReadFileTool {
.as_ref()
.map_or(true, |file| !file.disk_state().exists())
})? {
anyhow::bail!("{file_path} not found");
return Err(anyhow!("{} not found", file_path));
}
project.update(cx, |project, cx| {

View File

@@ -1,13 +1,12 @@
You are an expert engineer and your task is to write a new file from scratch.
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
The text you output will be saved verbatim as the content of the file.
Tool calls have been disabled. You MUST start your response directly with the file's new content.
<file_path>
<file_to_edit>
{{path}}
</file_path>
</file_to_edit>
<edit_description>
{{edit_description}}
</edit_description>
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
The text you output will be saved verbatim as the content of the file.

View File

@@ -382,11 +382,13 @@ fn working_dir(
match worktrees.next() {
Some(worktree) => {
anyhow::ensure!(
worktrees.next().is_none(),
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
);
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
if worktrees.next().is_none() {
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
} else {
Err(anyhow!(
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
))
}
}
None => Ok(None),
}
@@ -407,7 +409,9 @@ fn working_dir(
}
}
anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees.");
Err(anyhow!(
"`cd` directory {cd:?} was not in any of the project's worktrees."
))
}
}

View File

@@ -1,6 +1,6 @@
use std::{io::Cursor, sync::Arc};
use anyhow::{Context as _, Result};
use anyhow::Result;
use collections::HashMap;
use gpui::{App, AssetSource, Global};
use rodio::{
@@ -44,8 +44,8 @@ impl SoundRegistry {
let bytes = self
.assets
.load(&path)?
.map(anyhow::Ok)
.with_context(|| format!("No asset available for path {path}"))??
.map(Ok)
.unwrap_or_else(|| Err(anyhow::anyhow!("No such asset available")))?
.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use client::{Client, TelemetrySettings};
use db::RELEASE_CHANNEL;
use db::kvp::KEY_VALUE_STORE;
@@ -358,7 +358,7 @@ impl AutoUpdater {
cx.default_global::<GlobalAutoUpdate>()
.0
.clone()
.context("auto-update not initialized")
.ok_or_else(|| anyhow!("auto-update not initialized"))
})??;
let release = Self::get_release(
@@ -402,7 +402,7 @@ impl AutoUpdater {
cx.default_global::<GlobalAutoUpdate>()
.0
.clone()
.context("auto-update not initialized")
.ok_or_else(|| anyhow!("auto-update not initialized"))
})??;
let release = Self::get_release(
@@ -456,11 +456,12 @@ impl AutoUpdater {
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
anyhow::ensure!(
response.status().is_success(),
"failed to fetch release: {:?}",
String::from_utf8_lossy(&body),
);
if !response.status().is_success() {
return Err(anyhow!(
"failed to fetch release: {:?}",
String::from_utf8_lossy(&body),
));
}
serde_json::from_slice(body.as_slice()).with_context(|| {
format!(
@@ -520,10 +521,10 @@ impl AutoUpdater {
let installer_dir = InstallerDir::new().await?;
let filename = match OS {
"macos" => anyhow::Ok("Zed.dmg"),
"macos" => Ok("Zed.dmg"),
"linux" => Ok("zed.tar.gz"),
"windows" => Ok("ZedUpdateInstaller.exe"),
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
_ => Err(anyhow!("not supported: {:?}", OS)),
}?;
#[cfg(not(target_os = "windows"))]
@@ -544,7 +545,7 @@ impl AutoUpdater {
"macos" => install_release_macos(&installer_dir, downloaded_asset, &cx).await,
"linux" => install_release_linux(&installer_dir, downloaded_asset, &cx).await,
"windows" => install_release_windows(downloaded_asset).await,
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
_ => Err(anyhow!("not supported: {:?}", OS)),
}?;
this.update(&mut cx, |this, cx| {
@@ -600,11 +601,12 @@ async fn download_remote_server_binary(
let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
let mut response = client.get(&release.url, request_body, true).await?;
anyhow::ensure!(
response.status().is_success(),
"failed to download remote server release: {:?}",
response.status()
);
if !response.status().is_success() {
return Err(anyhow!(
"failed to download remote server release: {:?}",
response.status()
));
}
smol::io::copy(response.body_mut(), &mut temp_file).await?;
smol::fs::rename(&temp, &target_path).await?;
@@ -751,7 +753,7 @@ async fn install_release_macos(
let running_app_path = cx.update(|cx| cx.app_path())??;
let running_app_filename = running_app_path
.file_name()
.with_context(|| format!("invalid running app path {running_app_path:?}"))?;
.ok_or_else(|| anyhow!("invalid running app path"))?;
let mount_path = temp_dir.path().join("Zed");
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();

View File

@@ -22,7 +22,7 @@ mod windows_impl {
use super::dialog::create_dialog_window;
use super::updater::perform_update;
use anyhow::{Context as _, Result};
use anyhow::{Context, Result};
use windows::{
Win32::{
Foundation::{HWND, LPARAM, WPARAM},

View File

@@ -4,7 +4,7 @@ use std::{
time::{Duration, Instant},
};
use anyhow::{Context as _, Result};
use anyhow::{Context, Result};
use windows::Win32::{
Foundation::{HWND, LPARAM, WPARAM},
System::Threading::CREATE_NEW_PROCESS_GROUP,
@@ -124,7 +124,9 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>) -> Result<()>
for job in JOBS.iter() {
let start = Instant::now();
loop {
anyhow::ensure!(start.elapsed().as_secs() <= 2, "Timed out");
if start.elapsed().as_secs() > 2 {
return Err(anyhow::anyhow!("Timed out"));
}
match (*job)(app_dir) {
Ok(_) => {
unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };

View File

@@ -3,7 +3,7 @@ mod models;
use std::collections::HashMap;
use std::pin::Pin;
use anyhow::{Context as _, Error, Result, anyhow};
use anyhow::{Error, Result, anyhow};
use aws_sdk_bedrockruntime as bedrock;
pub use aws_sdk_bedrockruntime as bedrock_client;
pub use aws_sdk_bedrockruntime::types::{
@@ -97,7 +97,7 @@ pub async fn stream_completion(
}
})
.await
.context("spawning a task")?
.map_err(|err| anyhow!("failed to spawn task: {err:?}"))?
}
pub fn aws_document_to_value(document: &Document) -> Value {

View File

@@ -1,3 +1,4 @@
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use strum::EnumIter;
@@ -106,7 +107,7 @@ impl Model {
} else if id.starts_with("claude-3-7-sonnet-thinking") {
Ok(Self::Claude3_7SonnetThinking)
} else {
anyhow::bail!("invalid model id {id}");
Err(anyhow!("invalid model id"))
}
}
@@ -293,7 +294,7 @@ impl Model {
}
}
pub fn cross_region_inference_id(&self, region: &str) -> anyhow::Result<String> {
pub fn cross_region_inference_id(&self, region: &str) -> Result<String, anyhow::Error> {
let region_group = if region.starts_with("us-gov-") {
"us-gov"
} else if region.starts_with("us-") {
@@ -306,7 +307,8 @@ impl Model {
// Canada and South America regions - default to US profiles
"us"
} else {
anyhow::bail!("Unsupported Region {region}");
// Unknown region
return Err(anyhow!("Unsupported Region"));
};
let model_id = self.id();

View File

@@ -2,7 +2,7 @@ pub mod participant;
pub mod room;
use crate::call_settings::CallSettings;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use audio::Audio;
use client::{ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, proto};
use collections::HashSet;
@@ -187,7 +187,7 @@ impl ActiveCall {
let invite = if let Some(room) = room {
cx.spawn(async move |_, cx| {
let room = room.await.map_err(|err| anyhow!("{err:?}"))?;
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
@@ -236,7 +236,7 @@ impl ActiveCall {
.shared();
self.pending_room_creation = Some(room.clone());
cx.background_spawn(async move {
room.await.map_err(|err| anyhow!("{err:?}"))?;
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
@@ -326,7 +326,7 @@ impl ActiveCall {
.0
.borrow_mut()
.take()
.context("no incoming call")?;
.ok_or_else(|| anyhow!("no incoming call"))?;
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
@@ -399,9 +399,12 @@ impl ActiveCall {
project: Entity<Project>,
cx: &mut Context<Self>,
) -> Result<()> {
let (room, _) = self.room.as_ref().context("no active call")?;
self.report_call_event("Project Unshared", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("Project Unshared", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
}
pub fn location(&self) -> Option<&WeakEntity<Project>> {

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result};
use anyhow::{Result, anyhow};
use client::{ParticipantIndex, User, proto};
use collections::HashMap;
use gpui::WeakEntity;
@@ -18,17 +18,17 @@ pub enum ParticipantLocation {
impl ParticipantLocation {
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
match location
.and_then(|l| l.variant)
.context("participant location was not provided")?
{
proto::participant_location::Variant::SharedProject(project) => {
match location.and_then(|l| l.variant) {
Some(proto::participant_location::Variant::SharedProject(project)) => {
Ok(Self::SharedProject {
project_id: project.id,
})
}
proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject),
proto::participant_location::Variant::External(_) => Ok(Self::External),
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
Ok(Self::UnsharedProject)
}
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
None => Err(anyhow!("participant location was not provided")),
}
}
}

View File

@@ -2,7 +2,7 @@ use crate::{
call_settings::CallSettings,
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use audio::{Audio, Sound};
use client::{
ChannelId, Client, ParticipantIndex, TypedEnvelope, User, UserStore,
@@ -165,7 +165,7 @@ impl Room {
) -> Task<Result<Entity<Self>>> {
cx.spawn(async move |cx| {
let response = client.request(proto::CreateRoom {}).await?;
let room_proto = response.room.context("invalid room")?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new(|cx| {
let mut room = Self::new(
room_proto.id,
@@ -270,7 +270,7 @@ impl Room {
user_store: Entity<UserStore>,
mut cx: AsyncApp,
) -> Result<Entity<Self>> {
let room_proto = response.room.context("invalid room")?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
let room = cx.new(|cx| {
Self::new(
room_proto.id,
@@ -360,7 +360,7 @@ impl Room {
log::info!("detected client disconnection");
this.upgrade()
.context("room was dropped")?
.ok_or_else(|| anyhow!("room was dropped"))?
.update(cx, |this, cx| {
this.status = RoomStatus::Rejoining;
cx.notify();
@@ -428,7 +428,9 @@ impl Room {
log::info!("reconnection failed, leaving room");
this.update(cx, |this, cx| this.leave(cx))?.await?;
}
anyhow::bail!("can't reconnect to room: client failed to re-establish connection");
Err(anyhow!(
"can't reconnect to room: client failed to re-establish connection"
))
}
fn rejoin(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
@@ -492,7 +494,7 @@ impl Room {
let response = response.await?;
let message_id = response.message_id;
let response = response.payload;
let room_proto = response.room.context("invalid room")?;
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
this.update(cx, |this, cx| {
this.status = RoomStatus::Online;
this.apply_room_update(room_proto, cx)?;
@@ -643,7 +645,10 @@ impl Room {
envelope: TypedEnvelope<proto::RoomUpdated>,
mut cx: AsyncApp,
) -> Result<()> {
let room = envelope.payload.room.context("invalid room")?;
let room = envelope
.payload
.room
.ok_or_else(|| anyhow!("invalid room"))?;
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
}
@@ -932,15 +937,12 @@ impl Room {
} => {
let user_id = participant.identity().0.parse()?;
let track_id = track.sid();
let participant =
self.remote_participants
.get_mut(&user_id)
.with_context(|| {
format!(
"{:?} subscribed to track by unknown participant {user_id}",
self.client.user_id()
)
})?;
let participant = self.remote_participants.get_mut(&user_id).ok_or_else(|| {
anyhow!(
"{:?} subscribed to track by unknown participant {user_id}",
self.client.user_id()
)
})?;
if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) {
if publication.is_audio() {
publication.set_enabled(false, cx);
@@ -970,15 +972,12 @@ impl Room {
track, participant, ..
} => {
let user_id = participant.identity().0.parse()?;
let participant =
self.remote_participants
.get_mut(&user_id)
.with_context(|| {
format!(
"{:?}, unsubscribed from track by unknown participant {user_id}",
self.client.user_id()
)
})?;
let participant = self.remote_participants.get_mut(&user_id).ok_or_else(|| {
anyhow!(
"{:?}, unsubscribed from track by unknown participant {user_id}",
self.client.user_id()
)
})?;
match track {
livekit_client::RemoteTrack::Audio(track) => {
participant.audio_tracks.remove(&track.sid());
@@ -1325,7 +1324,7 @@ impl Room {
let live_kit = this
.live_kit
.as_mut()
.context("live-kit was not initialized")?;
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
let canceled = if let LocalTrack::Pending {
publish_id: cur_publish_id,
@@ -1390,7 +1389,7 @@ impl Room {
cx.spawn(async move |this, cx| {
let sources = sources.await??;
let source = sources.first().context("no display found")?;
let source = sources.first().ok_or_else(|| anyhow!("no display found"))?;
let publication = participant.publish_screenshare_track(&**source, cx).await;
@@ -1398,7 +1397,7 @@ impl Room {
let live_kit = this
.live_kit
.as_mut()
.context("live-kit was not initialized")?;
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
let canceled = if let LocalTrack::Pending {
publish_id: cur_publish_id,
@@ -1486,14 +1485,16 @@ impl Room {
}
pub fn unshare_screen(&mut self, cx: &mut Context<Self>) -> Result<()> {
anyhow::ensure!(!self.status.is_offline(), "room is offline");
if self.status.is_offline() {
return Err(anyhow!("room is offline"));
}
let live_kit = self
.live_kit
.as_mut()
.context("live-kit was not initialized")?;
.ok_or_else(|| anyhow!("live-kit was not initialized"))?;
match mem::take(&mut live_kit.screen_track) {
LocalTrack::None => anyhow::bail!("screen was not shared"),
LocalTrack::None => Err(anyhow!("screen was not shared")),
LocalTrack::Pending { .. } => {
cx.notify();
Ok(())

View File

@@ -1,5 +1,5 @@
use crate::{Channel, ChannelStore};
use anyhow::{Context as _, Result};
use anyhow::{Result, anyhow};
use client::{
ChannelId, Client, Subscription, TypedEnvelope, UserId, proto,
user::{User, UserStore},
@@ -170,16 +170,15 @@ impl ChannelChat {
message: MessageParams,
cx: &mut Context<Self>,
) -> Result<Task<Result<u64>>> {
anyhow::ensure!(
!message.text.trim().is_empty(),
"message body can't be empty"
);
if message.text.trim().is_empty() {
Err(anyhow!("message body can't be empty"))?;
}
let current_user = self
.user_store
.read(cx)
.current_user()
.context("current_user is not present")?;
.ok_or_else(|| anyhow!("current_user is not present"))?;
let channel_id = self.channel_id;
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
@@ -216,7 +215,7 @@ impl ChannelChat {
});
let response = request.await?;
drop(outgoing_message_guard);
let response = response.message.context("invalid message")?;
let response = response.message.ok_or_else(|| anyhow!("invalid message"))?;
let id = response.id;
let message = ChannelMessage::from_proto(response, &user_store, cx).await?;
this.update(cx, |this, cx| {
@@ -471,7 +470,7 @@ impl ChannelChat {
});
let response = request.await?;
let message = ChannelMessage::from_proto(
response.message.context("invalid message")?,
response.message.ok_or_else(|| anyhow!("invalid message"))?,
&user_store,
cx,
)
@@ -532,7 +531,10 @@ impl ChannelChat {
mut cx: AsyncApp,
) -> Result<()> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let message = message.payload.message.context("empty message")?;
let message = message
.payload
.message
.ok_or_else(|| anyhow!("empty message"))?;
let message_id = message.id;
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
@@ -564,7 +566,10 @@ impl ChannelChat {
mut cx: AsyncApp,
) -> Result<()> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let message = message.payload.message.context("empty message")?;
let message = message
.payload
.message
.ok_or_else(|| anyhow!("empty message"))?;
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
@@ -748,7 +753,10 @@ impl ChannelMessage {
.collect(),
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
sender,
nonce: message.nonce.context("nonce is required")?.into(),
nonce: message
.nonce
.ok_or_else(|| anyhow!("nonce is required"))?
.into(),
reply_to_message_id: message.reply_to_message_id,
edited_at,
})

View File

@@ -1,7 +1,7 @@
mod channel_index;
use crate::{ChannelMessage, channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Result, anyhow};
use channel_index::ChannelIndex;
use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore};
use collections::{HashMap, HashSet, hash_map};
@@ -332,7 +332,9 @@ impl ChannelStore {
cx.spawn(async move |this, cx| {
if let Some(request) = request {
let response = request.await?;
let this = this.upgrade().context("channel store dropped")?;
let this = this
.upgrade()
.ok_or_else(|| anyhow!("channel store dropped"))?;
let user_store = this.update(cx, |this, _| this.user_store.clone())?;
ChannelMessage::from_proto_vec(response.messages, &user_store, cx).await
} else {
@@ -480,7 +482,7 @@ impl ChannelStore {
.spawn(async move |this, cx| {
let channel = this.update(cx, |this, _| {
this.channel_for_id(channel_id).cloned().ok_or_else(|| {
Arc::new(anyhow!("no channel for id: {channel_id}"))
Arc::new(anyhow!("no channel for id: {}", channel_id))
})
})??;
@@ -512,7 +514,7 @@ impl ChannelStore {
}
}
};
cx.background_spawn(async move { task.await.map_err(|error| anyhow!("{error}")) })
cx.background_spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) })
}
pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool {
@@ -576,7 +578,9 @@ impl ChannelStore {
})
.await?;
let channel = response.channel.context("missing channel in response")?;
let channel = response
.channel
.ok_or_else(|| anyhow!("missing channel in response"))?;
let channel_id = ChannelId(channel.id);
this.update(cx, |this, cx| {
@@ -748,7 +752,7 @@ impl ChannelStore {
})
.await?
.channel
.context("missing channel in response")?;
.ok_or_else(|| anyhow!("missing channel in response"))?;
this.update(cx, |this, cx| {
let task = this.update_channels(
proto::UpdateChannels {

View File

@@ -169,7 +169,7 @@ fn main() -> Result<()> {
"To retrieve the system specs on the command line, run the following command:",
&format!("{} --system-specs", path.display()),
];
anyhow::bail!(msg.join("\n"));
return Err(anyhow::anyhow!(msg.join("\n")));
}
#[cfg(all(
@@ -255,10 +255,11 @@ fn main() -> Result<()> {
}
}
anyhow::ensure!(
args.dev_server_token.is_none(),
"Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
);
if let Some(_) = args.dev_server_token {
return Err(anyhow::anyhow!(
"Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
))?;
}
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
let exit_status = exit_status.clone();
@@ -399,7 +400,7 @@ mod linux {
time::Duration,
};
use anyhow::{Context as _, anyhow};
use anyhow::anyhow;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
@@ -416,7 +417,9 @@ mod linux {
path.to_path_buf().canonicalize()?
} else {
let cli = env::current_exe()?;
let dir = cli.parent().context("no parent path for cli")?;
let dir = cli
.parent()
.ok_or_else(|| anyhow!("no parent path for cli"))?;
// libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
// ./zed is for the target directory in development builds.
@@ -425,8 +428,8 @@ mod linux {
possible_locations
.iter()
.find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
.with_context(|| {
format!("could not find any of: {}", possible_locations.join(", "))
.ok_or_else(|| {
anyhow!("could not find any of: {}", possible_locations.join(", "))
})?
};
@@ -756,7 +759,7 @@ mod windows {
#[cfg(target_os = "macos")]
mod mac_os {
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use core_foundation::{
array::{CFArray, CFIndex},
base::TCFType as _,
@@ -797,10 +800,9 @@ mod mac_os {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();
while app_path.extension() != Some(OsStr::new("app")) {
anyhow::ensure!(
app_path.pop(),
"cannot find app bundle containing {cli_path:?}"
);
if !app_path.pop() {
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
}
}
Ok(app_path)
}

View File

@@ -19,7 +19,6 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
collections.workspace = true
@@ -30,7 +29,6 @@ gpui.workspace = true
gpui_tokio.workspace = true
http_client.workspace = true
http_client_tls.workspace = true
httparse = "1.10"
log.workspace = true
paths.workspace = true
parking_lot.workspace = true
@@ -71,10 +69,3 @@ windows.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
cocoa.workspace = true
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
tokio-native-tls = "0.3"
[target.'cfg(not(any(target_os = "windows", target_os = "macos")))'.dependencies]
rustls-pki-types = "1.12"
tokio-rustls = { version = "0.26", features = ["tls12", "ring"], default-features = false }

View File

@@ -1,7 +1,7 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
mod proxy;
mod socks;
pub mod telemetry;
pub mod user;
pub mod zed_urls;
@@ -24,13 +24,13 @@ use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use parking_lot::RwLock;
use postage::watch;
use proxy::connect_proxy_stream;
use rand::prelude::*;
use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use socks::connect_socks_proxy_stream;
use std::pin::Pin;
use std::{
any::TypeId,
@@ -490,14 +490,14 @@ impl<T: 'static> Drop for PendingEntitySubscription<T> {
}
}
#[derive(Copy, Clone, Deserialize, Debug)]
#[derive(Copy, Clone)]
pub struct TelemetrySettings {
pub diagnostics: bool,
pub metrics: bool,
}
/// Control what info is collected by Zed.
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TelemetrySettingsContent {
/// Send debug info like crash reports.
///
@@ -515,7 +515,25 @@ impl settings::Settings for TelemetrySettings {
type FileContent = TelemetrySettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
sources.json_merge()
Ok(Self {
diagnostics: sources
.user
.as_ref()
.or(sources.server.as_ref())
.and_then(|v| v.diagnostics)
.unwrap_or(
sources
.default
.diagnostics
.ok_or_else(Self::missing_default)?,
),
metrics: sources
.user
.as_ref()
.or(sources.server.as_ref())
.and_then(|v| v.metrics)
.unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?),
})
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
@@ -711,10 +729,9 @@ impl Client {
let id = (TypeId::of::<T>(), remote_id);
let mut state = self.handler_set.lock();
anyhow::ensure!(
!state.entities_by_type_and_remote_id.contains_key(&id),
"already subscribed to entity"
);
if state.entities_by_type_and_remote_id.contains_key(&id) {
return Err(anyhow!("already subscribed to entity"));
}
state
.entities_by_type_and_remote_id
@@ -963,7 +980,10 @@ impl Client {
hello_message_type_name
)
})?;
let peer_id = hello.payload.peer_id.context("invalid peer id")?;
let peer_id = hello
.payload
.peer_id
.ok_or_else(|| anyhow!("invalid peer id"))?;
Ok(peer_id)
};
@@ -1073,19 +1093,22 @@ impl Client {
}
let response = http.get(&url, Default::default(), false).await?;
anyhow::ensure!(
response.status().is_redirection(),
"unexpected /rpc response status {}",
response.status()
);
let collab_url = response
.headers()
.get("Location")
.context("missing location header in /rpc response")?
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string();
Url::parse(&collab_url).with_context(|| format!("parsing colab rpc url {collab_url}"))
let collab_url = if response.status().is_redirection() {
response
.headers()
.get("Location")
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string()
} else {
Err(anyhow!(
"unexpected /rpc response status {}",
response.status()
))?
};
Url::parse(&collab_url).context("invalid rpc url")
}
}
@@ -1127,13 +1150,13 @@ impl Client {
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
.context("missing host in rpc url")?;
.ok_or_else(|| anyhow!("missing host in rpc url"))?;
let stream = {
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
let _guard = handle.enter();
match proxy {
Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?,
Some(proxy) => connect_socks_proxy_stream(&proxy, rpc_host).await?,
None => Box::new(TcpStream::connect(rpc_host).await?),
}
};
@@ -1282,13 +1305,16 @@ impl Client {
)
.context("failed to respond to login http request")?;
return Ok((
user_id.context("missing user_id parameter")?,
access_token.context("missing access_token parameter")?,
user_id
.ok_or_else(|| anyhow!("missing user_id parameter"))?,
access_token.ok_or_else(|| {
anyhow!("missing access_token parameter")
})?,
));
}
}
anyhow::bail!("didn't receive login redirect");
Err(anyhow!("didn't receive login redirect"))
})
.await?;
@@ -1406,12 +1432,13 @@ impl Client {
let mut response = http.send(request).await?;
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
anyhow::ensure!(
response.status().is_success(),
"admin user request failed {} - {}",
response.status().as_u16(),
body,
);
if !response.status().is_success() {
Err(anyhow!(
"admin user request failed {} - {}",
response.status().as_u16(),
body,
))?;
}
let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
// Use the admin API token to authenticate as the impersonated user.
@@ -1448,7 +1475,7 @@ impl Client {
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
Ok(connection_id)
} else {
anyhow::bail!("not connected");
Err(anyhow!("not connected"))
}
}

View File

@@ -1,66 +0,0 @@
//! client proxy
mod http_proxy;
mod socks_proxy;
use anyhow::{Context as _, Result};
use http_client::Url;
use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy};
use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy};
pub(crate) async fn connect_proxy_stream(
proxy: &Url,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy) else {
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
// SOCKS proxies are often used in contexts where security and privacy are critical,
// so any fallback could expose users to significant risks.
anyhow::bail!("Parsing proxy url failed");
};
// Connect to proxy and wrap protocol later
let stream = tokio::net::TcpStream::connect((proxy_domain.as_str(), proxy_port))
.await
.context("Failed to connect to proxy")?;
let proxy_stream = match proxy_type {
ProxyType::SocksProxy(proxy) => connect_socks_proxy_stream(stream, proxy, rpc_host).await?,
ProxyType::HttpProxy(proxy) => {
connect_http_proxy_stream(stream, proxy, rpc_host, &proxy_domain).await?
}
};
Ok(proxy_stream)
}
enum ProxyType<'t> {
SocksProxy(SocksVersion<'t>),
HttpProxy(HttpProxyType<'t>),
}
fn parse_proxy_type<'t>(proxy: &'t Url) -> Option<((String, u16), ProxyType<'t>)> {
let scheme = proxy.scheme();
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;
let proxy_type = match scheme {
scheme if scheme.starts_with("socks") => {
Some(ProxyType::SocksProxy(parse_socks_proxy(scheme, proxy)))
}
scheme if scheme.starts_with("http") => {
Some(ProxyType::HttpProxy(parse_http_proxy(scheme, proxy)))
}
_ => None,
}?;
Some(((host, port), proxy_type))
}
pub(crate) trait AsyncReadWrite:
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static
{
}
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> AsyncReadWrite
for T
{
}

View File

@@ -1,193 +0,0 @@
use anyhow::{Context, Result};
use base64::Engine;
use httparse::{EMPTY_HEADER, Response};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufStream},
net::TcpStream,
};
#[cfg(any(target_os = "windows", target_os = "macos"))]
use tokio_native_tls::{TlsConnector, native_tls};
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
use tokio_rustls::TlsConnector;
use url::Url;
use super::AsyncReadWrite;
pub(super) enum HttpProxyType<'t> {
HTTP(Option<HttpProxyAuthorization<'t>>),
HTTPS(Option<HttpProxyAuthorization<'t>>),
}
pub(super) struct HttpProxyAuthorization<'t> {
username: &'t str,
password: &'t str,
}
pub(super) fn parse_http_proxy<'t>(scheme: &str, proxy: &'t Url) -> HttpProxyType<'t> {
let auth = proxy.password().map(|password| HttpProxyAuthorization {
username: proxy.username(),
password,
});
if scheme.starts_with("https") {
HttpProxyType::HTTPS(auth)
} else {
HttpProxyType::HTTP(auth)
}
}
pub(crate) async fn connect_http_proxy_stream(
stream: TcpStream,
http_proxy: HttpProxyType<'_>,
rpc_host: (&str, u16),
proxy_domain: &str,
) -> Result<Box<dyn AsyncReadWrite>> {
match http_proxy {
HttpProxyType::HTTP(auth) => http_connect(stream, rpc_host, auth).await,
HttpProxyType::HTTPS(auth) => https_connect(stream, rpc_host, auth, proxy_domain).await,
}
.context("error connecting to http/https proxy")
}
async fn http_connect<T>(
stream: T,
target: (&str, u16),
auth: Option<HttpProxyAuthorization<'_>>,
) -> Result<Box<dyn AsyncReadWrite>>
where
T: AsyncReadWrite,
{
let mut stream = BufStream::new(stream);
let request = make_request(target, auth);
stream.write_all(request.as_bytes()).await?;
stream.flush().await?;
check_response(&mut stream).await?;
Ok(Box::new(stream))
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
async fn https_connect<T>(
stream: T,
target: (&str, u16),
auth: Option<HttpProxyAuthorization<'_>>,
proxy_domain: &str,
) -> Result<Box<dyn AsyncReadWrite>>
where
T: AsyncReadWrite,
{
let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?);
let stream = tls_connector.connect(proxy_domain, stream).await?;
http_connect(stream, target, auth).await
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
async fn https_connect<T>(
stream: T,
target: (&str, u16),
auth: Option<HttpProxyAuthorization<'_>>,
proxy_domain: &str,
) -> Result<Box<dyn AsyncReadWrite>>
where
T: AsyncReadWrite,
{
let proxy_domain = rustls_pki_types::ServerName::try_from(proxy_domain)
.context("Address resolution failed")?
.to_owned();
let tls_connector = TlsConnector::from(std::sync::Arc::new(http_client_tls::tls_config()));
let stream = tls_connector.connect(proxy_domain, stream).await?;
http_connect(stream, target, auth).await
}
fn make_request(target: (&str, u16), auth: Option<HttpProxyAuthorization<'_>>) -> String {
let (host, port) = target;
let mut request = format!(
"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\nProxy-Connection: Keep-Alive\r\n"
);
if let Some(HttpProxyAuthorization { username, password }) = auth {
let auth =
base64::prelude::BASE64_STANDARD.encode(format!("{username}:{password}").as_bytes());
let auth = format!("Proxy-Authorization: Basic {auth}\r\n");
request.push_str(&auth);
}
request.push_str("\r\n");
request
}
async fn check_response<T>(stream: &mut BufStream<T>) -> Result<()>
where
T: AsyncReadWrite,
{
let response = recv_response(stream).await?;
let mut dummy_headers = [EMPTY_HEADER; MAX_RESPONSE_HEADERS];
let mut parser = Response::new(&mut dummy_headers);
parser.parse(response.as_bytes())?;
match parser.code {
Some(code) => {
if code == 200 {
Ok(())
} else {
Err(anyhow::anyhow!(
"Proxy connection failed with HTTP code: {code}"
))
}
}
None => Err(anyhow::anyhow!(
"Proxy connection failed with no HTTP code: {}",
parser.reason.unwrap_or("Unknown reason")
)),
}
}
const MAX_RESPONSE_HEADER_LENGTH: usize = 4096;
const MAX_RESPONSE_HEADERS: usize = 16;
async fn recv_response<T>(stream: &mut BufStream<T>) -> Result<String>
where
T: AsyncReadWrite,
{
let mut response = String::new();
loop {
if stream.read_line(&mut response).await? == 0 {
return Err(anyhow::anyhow!("End of stream"));
}
if MAX_RESPONSE_HEADER_LENGTH < response.len() {
return Err(anyhow::anyhow!("Maximum response header length exceeded"));
}
if response.ends_with("\r\n\r\n") {
return Ok(response);
}
}
}
#[cfg(test)]
mod tests {
use url::Url;
use super::{HttpProxyAuthorization, HttpProxyType, parse_http_proxy};
#[test]
fn test_parse_http_proxy() {
let proxy = Url::parse("http://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_http_proxy(scheme, &proxy);
assert!(matches!(version, HttpProxyType::HTTP(None)))
}
#[test]
fn test_parse_http_proxy_with_auth() {
let proxy = Url::parse("http://username:password@proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_http_proxy(scheme, &proxy);
assert!(matches!(
version,
HttpProxyType::HTTP(Some(HttpProxyAuthorization {
username: "username",
password: "password"
}))
))
}
}

View File

@@ -1,226 +0,0 @@
//! socks proxy
use anyhow::{Context as _, Result};
use http_client::Url;
use tokio::net::TcpStream;
use tokio_socks::{
IntoTargetAddr, TargetAddr,
tcp::{Socks4Stream, Socks5Stream},
};
use super::AsyncReadWrite;
/// Identification to a Socks V4 Proxy
pub(super) struct Socks4Identification<'a> {
user_id: &'a str,
}
/// Authorization to a Socks V5 Proxy
pub(super) struct Socks5Authorization<'a> {
username: &'a str,
password: &'a str,
}
/// Socks Proxy Protocol Version
///
/// V4 allows idenfication using a user_id
/// V5 allows authorization using a username and password
pub(super) enum SocksVersion<'a> {
V4 {
local_dns: bool,
identification: Option<Socks4Identification<'a>>,
},
V5 {
local_dns: bool,
authorization: Option<Socks5Authorization<'a>>,
},
}
pub(super) fn parse_socks_proxy<'t>(scheme: &str, proxy: &'t Url) -> SocksVersion<'t> {
if scheme.starts_with("socks4") {
let identification = match proxy.username() {
"" => None,
username => Some(Socks4Identification { user_id: username }),
};
SocksVersion::V4 {
local_dns: scheme != "socks4a",
identification,
}
} else {
let authorization = proxy.password().map(|password| Socks5Authorization {
username: proxy.username(),
password,
});
SocksVersion::V5 {
local_dns: scheme != "socks5h",
authorization,
}
}
}
pub(super) async fn connect_socks_proxy_stream(
stream: TcpStream,
socks_version: SocksVersion<'_>,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let rpc_host = rpc_host
.into_target_addr()
.context("Failed to parse target addr")?;
let local_dns = match &socks_version {
SocksVersion::V4 { local_dns, .. } => local_dns,
SocksVersion::V5 { local_dns, .. } => local_dns,
};
let rpc_host = match (rpc_host, local_dns) {
(TargetAddr::Domain(domain, port), true) => {
let ip_addr = tokio::net::lookup_host((domain.as_ref(), port))
.await
.with_context(|| format!("Failed to lookup domain {}", domain))?
.next()
.ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?;
TargetAddr::Ip(ip_addr)
}
(rpc_host, _) => rpc_host,
};
match socks_version {
SocksVersion::V4 {
identification: None,
..
} => {
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Ok(Box::new(socks))
}
SocksVersion::V4 {
identification: Some(Socks4Identification { user_id }),
..
} => {
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
.await
.context("error connecting to socks")?;
Ok(Box::new(socks))
}
SocksVersion::V5 {
authorization: None,
..
} => {
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Ok(Box::new(socks))
}
SocksVersion::V5 {
authorization: Some(Socks5Authorization { username, password }),
..
} => {
let socks = Socks5Stream::connect_with_password_and_socket(
stream, rpc_host, username, password,
)
.await
.context("error connecting to socks")?;
Ok(Box::new(socks))
}
}
}
#[cfg(test)]
mod tests {
use url::Url;
use super::*;
#[test]
fn parse_socks4() {
let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V4 {
local_dns: true,
identification: None
}
))
}
#[test]
fn parse_socks4_with_identification() {
let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V4 {
local_dns: true,
identification: Some(Socks4Identification { user_id: "userid" })
}
))
}
#[test]
fn parse_socks4_with_remote_dns() {
let proxy = Url::parse("socks4a://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V4 {
local_dns: false,
identification: None
}
))
}
#[test]
fn parse_socks5() {
let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V5 {
local_dns: true,
authorization: None
}
))
}
#[test]
fn parse_socks5_with_authorization() {
let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V5 {
local_dns: true,
authorization: Some(Socks5Authorization {
username: "username",
password: "password"
})
}
))
}
#[test]
fn parse_socks5_with_remote_dns() {
let proxy = Url::parse("socks5h://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V5 {
local_dns: false,
authorization: None
}
))
}
}

176
crates/client/src/socks.rs Normal file
View File

@@ -0,0 +1,176 @@
//! socks proxy
use anyhow::{Context, Result, anyhow};
use http_client::Url;
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
/// Identification to a Socks V4 Proxy
struct Socks4Identification<'a> {
user_id: &'a str,
}
/// Authorization to a Socks V5 Proxy
struct Socks5Authorization<'a> {
username: &'a str,
password: &'a str,
}
/// Socks Proxy Protocol Version
///
/// V4 allows idenfication using a user_id
/// V5 allows authorization using a username and password
enum SocksVersion<'a> {
V4(Option<Socks4Identification<'a>>),
V5(Option<Socks5Authorization<'a>>),
}
pub(crate) async fn connect_socks_proxy_stream(
proxy: &Url,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let Some((socks_proxy, version)) = parse_socks_proxy(proxy) else {
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
// SOCKS proxies are often used in contexts where security and privacy are critical,
// so any fallback could expose users to significant risks.
return Err(anyhow!("Parsing proxy url failed"));
};
// Connect to proxy and wrap protocol later
let stream = tokio::net::TcpStream::connect(socks_proxy)
.await
.context("Failed to connect to socks proxy")?;
let socks: Box<dyn AsyncReadWrite> = match version {
SocksVersion::V4(None) => {
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
SocksVersion::V5(None) => {
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
let socks = Socks5Stream::connect_with_password_and_socket(
stream, rpc_host, username, password,
)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
};
Ok(socks)
}
fn parse_socks_proxy(proxy: &Url) -> Option<((String, u16), SocksVersion<'_>)> {
let scheme = proxy.scheme();
let socks_version = if scheme.starts_with("socks4") {
let identification = match proxy.username() {
"" => None,
username => Some(Socks4Identification { user_id: username }),
};
SocksVersion::V4(identification)
} else if scheme.starts_with("socks") {
let authorization = proxy.password().map(|password| Socks5Authorization {
username: proxy.username(),
password,
});
SocksVersion::V5(authorization)
} else {
return None;
};
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;
Some(((host, port), socks_version))
}
pub(crate) trait AsyncReadWrite:
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static
{
}
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> AsyncReadWrite
for T
{
}
#[cfg(test)]
mod tests {
use url::Url;
use super::*;
#[test]
fn parse_socks4() {
let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(version, SocksVersion::V4(None)))
}
#[test]
fn parse_socks4_with_identification() {
let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(
version,
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
))
}
#[test]
fn parse_socks5() {
let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(version, SocksVersion::V5(None)))
}
#[test]
fn parse_socks5_with_authorization() {
let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(
version,
SocksVersion::V5(Some(Socks5Authorization {
username: "username",
password: "password"
}))
))
}
/// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
/// SOCKS proxies are often used in contexts where security and privacy are critical,
/// so any fallback could expose users to significant risks.
#[tokio::test]
async fn fails_on_bad_proxy() {
// Should fail connecting because http is not a valid Socks proxy scheme
let proxy = Url::parse("http://localhost:2313").unwrap();
let result = connect_socks_proxy_stream(&proxy, ("test", 1080)).await;
match result {
Err(e) => assert_eq!(e.to_string(), "Parsing proxy url failed"),
Ok(_) => panic!("Connecting on bad proxy should fail"),
};
}
}

Some files were not shown because too many files have changed in this diff Show More