Compare commits

..

15 Commits

Author SHA1 Message Date
Nathan Sobo
878312a812 Fix warnings
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-04-23 15:05:54 -06:00
Nathan Sobo
6dc61cefb0 Send serialized EvaluatedExample structs to Snowflake
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-04-23 14:53:31 -06:00
Nathan Sobo
88008c940b Register assertion groups in programmatic tests
This will let us track how many assertions were even run out of the
total we may expect.

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-04-23 14:44:06 -06:00
Smit Barmase
636c6e7f2d editor: Make SelectNext and SelectPrevious preserve cursor direction (#29293)
Closes #27652

Now, if the last selection is reversed, subsequent `SelectNext` or
`SelectPrevious` selection will also be reversed.


https://github.com/user-attachments/assets/dff31abf-ac9e-4d35-bd2c-34e7b0f3ca23

Release Notes:

- Fixed an issue where `SelectNext` and `SelectPrevious` did not
preserve the last selection's cursor direction.
2025-04-24 00:17:17 +05:30
Agus Zubiaga
45d3f5168a eval: New add_arg_to_trait_method example (#29297)
Release Notes:

- N/A

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-04-23 18:46:39 +00:00
Danilo Leal
8366cd0b52 agent: Render diffs for the edit file tool (#29234)
This PR implements the `ToolCard` for the edit file tool, which allow us
to display an editor with a diff in the thread view with the changes
performed by the model.

- [x] Fix buffer sometimes displaying empty
- [x] Stop buffer from scrolling together with the thread
- [x] Fix multibuffer header sometimes appearing
- [x] Fix buffer height issue
- [x] Implement "full height" expand button
- [x] Add "Jump To File" functionality
- [x] Polish and refine styles

Release Notes:

- agent: Added diff preview cards in the thread view for edits performed
by the agent.

---------

Co-authored-by: João Marcos <marcospb19@hotmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-04-23 15:43:33 -03:00
Richard Feldman
f6774ae60d More graceful invalid JSON handling (#29295)
Now we're more tolerant of invalid JSON coming back from the model
(possibly because it was incomplete and we're streaming), plus if we do
end up with invalid JSON once it has all streamed back, we report what
the malformed JSON actually was:

<img width="444" alt="Screenshot 2025-04-23 at 1 49 14 PM"
src="https://github.com/user-attachments/assets/480f5da7-869b-49f3-9ffd-8f08ccddb33d"
/>

Release Notes:

- N/A
2025-04-23 14:08:26 -04:00
Marshall Bowers
92e810bfec language_models: Pass up mode for completion requests through Zed (#29294)
This PR makes it so we pass up the `mode` for completion requests
through the Zed provider.

Release Notes:

- N/A
2025-04-23 18:02:03 +00:00
Cole Miller
724c935196 Highlight merge conflicts and provide for resolving them (#28065)
TODO:

- [x] Make it work in the project diff:
  - [x] Support non-singleton buffers
  - [x] Adjust excerpt boundaries to show full conflicts
- [x] Write tests for conflict-related events and state management
- [x] Prevent hunk buttons from appearing inside conflicts
- [x] Make sure it works over SSH, collab
- [x] Allow separate theming of markers

Bonus:

- [ ] Count of conflicts in toolbar
- [ ] Keyboard-driven navigation and resolution
- [ ] ~~Inlay hints to contextualize "ours"/"theirs"~~

Release Notes:

- Implemented initial support for resolving merge conflicts.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-04-23 12:38:46 -04:00
Kirill Bulatov
ef54b58346 Fix relative paths not properly resolved in the terminal during cmd-click (#29289)
Closes https://github.com/zed-industries/zed/pull/28342
Closes https://github.com/zed-industries/zed/issues/28339
Fixes
https://github.com/zed-industries/zed/pull/29274#issuecomment-2824794396

Release Notes:

- Fixed relative paths not properly resolved in the terminal during
cmd-click
2025-04-23 19:36:58 +03:00
Joseph T. Lyons
01bdd170ec Bump Zed to v0.185 (#29287)
Release Notes:

-N/A
2025-04-23 16:20:08 +00:00
Cole Miller
4b9f4feff1 debugger: Fix stack frame list flickering (#29282)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-04-23 16:12:53 +00:00
Bibiana André
19fb1e1b0d Fix workspace bottom obscured when bottom dock is full height (#27689)
When dragging the pane separator of the bottom dock to full window
height, the contents at the bottom of the dock and workspace window
overflowed the screen, becoming obscured. This happened because setting
a new size in resize_bottom_dock(...) was not taking in consideration
the top bounds of the workspace window, which caused the bottom bounds
of both dock and workspace to overflow. The issue was fixed by
subtracting the workspace.bounds.top() value to the dock's new size.

Closes #12966

Release Notes:

- N/A
2025-04-23 15:43:20 +00:00
Marshall Bowers
f2cb6d69d5 collab: Add head_commit_details column to project_repositories (#29284)
This PR adds the `head_commit_details` column to the
`project_repositories` table, since it was missed in
https://github.com/zed-industries/zed/pull/29007.

Release Notes:

- N/A
2025-04-23 15:35:49 +00:00
Bennet Bo Fenner
822b6f837d agent: Expose web search tool to beta users (#29273)
This gives all beta users access to the web search tool

Release Notes:

- agent: Added `web_search` tool
2025-04-23 15:30:20 +00:00
92 changed files with 3299 additions and 1088 deletions

415
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -605,7 +605,7 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.6.1"
zed_llm_client = "0.7.0"
zstd = "0.11"
metal = "0.29"

View File

@@ -433,47 +433,39 @@ fn render_markdown_code_block(
workspace
.update(cx, {
|workspace, cx| {
if let Some(project_path) = workspace
let Some(project_path) = workspace
.project()
.read(cx)
.find_project_path(&path_range.path, cx)
{
let target = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
});
let open_task = workspace.open_path(
project_path,
None,
true,
window,
cx,
);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(target) = target {
if let Some(active_editor) =
item.downcast::<Editor>()
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor
.go_to_singleton_buffer_point(
target, window, cx,
);
})
.log_err();
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
else {
return;
};
let Some(target) = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
}) else {
return;
};
let open_task =
workspace.open_path(project_path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
target, window, cx,
);
})
.ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
.ok();
@@ -807,10 +799,11 @@ impl ActiveThread {
self.thread.read(cx).summary_or_default()
}
pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
pub fn cancel_last_completion(&mut self, window: &mut Window, cx: &mut App) -> bool {
self.last_error.take();
self.thread
.update(cx, |thread, cx| thread.cancel_last_completion(cx))
self.thread.update(cx, |thread, cx| {
thread.cancel_last_completion(Some(window.window_handle()), cx)
})
}
pub fn last_error(&self) -> Option<ThreadError> {
@@ -1314,7 +1307,7 @@ impl ActiveThread {
fn confirm_editing_message(
&mut self,
_: &menu::Confirm,
_: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some((message_id, state)) = self.editing_message.take() else {
@@ -1344,7 +1337,7 @@ impl ActiveThread {
self.thread.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model.model, cx)
thread.send_to_model(model.model, Some(window.window_handle()), cx);
});
cx.notify();
}
@@ -1850,11 +1843,9 @@ impl ActiveThread {
.gap_2()
.children(message_content)
.when(has_tool_uses, |parent| {
parent.children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, window, cx)),
)
parent.children(tool_uses.into_iter().map(|tool_use| {
self.render_tool_use(tool_use, window, workspace.clone(), cx)
}))
}),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
@@ -2447,10 +2438,11 @@ impl ActiveThread {
&self,
tool_use: ToolUse,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
return card.render(&tool_use.status, window, cx);
return card.render(&tool_use.status, window, workspace, cx);
}
let is_open = self
@@ -3047,7 +3039,7 @@ impl ActiveThread {
&mut self,
tool_use_id: LanguageModelToolUseId,
_: &ClickEvent,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
@@ -3063,6 +3055,7 @@ impl ActiveThread {
c.input.clone(),
&c.messages,
c.tool.clone(),
Some(window.window_handle()),
cx,
);
});
@@ -3074,11 +3067,12 @@ impl ActiveThread {
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
_: &ClickEvent,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
let window_handle = window.window_handle();
self.thread.update(cx, |thread, cx| {
thread.deny_tool_use(tool_use_id, tool_name, cx);
thread.deny_tool_use(tool_use_id, tool_name, Some(window_handle), cx);
});
}

View File

@@ -356,14 +356,9 @@ impl AssistantPanel {
&self.thread_store
}
fn cancel(
&mut self,
_: &editor::actions::Cancel,
_window: &mut Window,
cx: &mut Context<Self>,
) {
fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.thread
.update(cx, |thread, cx| thread.cancel_last_completion(cx));
.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
}
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {

View File

@@ -1328,7 +1328,7 @@ impl InlineAssistant {
editor.highlight_rows::<InlineAssist>(
row_range,
cx.theme().status().info_background,
false,
Default::default(),
cx,
);
}
@@ -1393,7 +1393,7 @@ impl InlineAssistant {
editor.highlight_rows::<DeletedLines>(
Anchor::min()..Anchor::max(),
cx.theme().status().deleted_background,
false,
Default::default(),
cx,
);
editor

View File

@@ -195,6 +195,7 @@ impl MessageEditor {
editor.set_mode(EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: false,
})
} else {
editor.set_mode(EditorMode::AutoHeight {
@@ -277,6 +278,7 @@ impl MessageEditor {
let context_store = self.context_store.clone();
let git_store = self.project.read(cx).git_store().clone();
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
let window_handle = window.window_handle();
cx.spawn(async move |this, cx| {
let checkpoint = checkpoint.await.ok();
@@ -333,7 +335,7 @@ impl MessageEditor {
thread
.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(model, cx);
thread.send_to_model(model, Some(window_handle), cx);
})
.log_err();
})
@@ -341,9 +343,9 @@ impl MessageEditor {
}
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let cancelled = self
.thread
.update(cx, |thread, cx| thread.cancel_last_completion(cx));
let cancelled = self.thread.update(cx, |thread, cx| {
thread.cancel_last_completion(Some(window.window_handle()), cx)
});
if cancelled {
self.set_editor_is_expanded(false, cx);

View File

@@ -13,7 +13,9 @@ use feature_flags::{self, FeatureFlagAppExt};
use futures::future::Shared;
use futures::{FutureExt, StreamExt as _};
use git::repository::DiffType;
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelImage, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
@@ -951,7 +953,12 @@ impl Thread {
self.remaining_turns = remaining_turns;
}
pub fn send_to_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
pub fn send_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
if self.remaining_turns == 0 {
return;
}
@@ -982,7 +989,7 @@ impl Thread {
};
}
self.stream_completion(request, model, cx);
self.stream_completion(request, model, window, cx);
}
pub fn used_tools_since_last_user_message(&self) -> bool {
@@ -1201,6 +1208,7 @@ impl Thread {
&mut self,
request: LanguageModelRequest,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
@@ -1382,7 +1390,7 @@ impl Thread {
match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
let tool_uses = thread.use_pending_tools(cx);
let tool_uses = thread.use_pending_tools(window, cx);
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
}
StopReason::EndTurn => {}
@@ -1427,7 +1435,7 @@ impl Thread {
}));
}
thread.cancel_last_completion(cx);
thread.cancel_last_completion(window, cx);
}
}
cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new)));
@@ -1596,7 +1604,11 @@ impl Thread {
)
}
pub fn use_pending_tools(&mut self, cx: &mut Context<Self>) -> Vec<PendingToolUse> {
pub fn use_pending_tools(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) -> Vec<PendingToolUse> {
self.auto_capture_telemetry(cx);
let request = self.to_completion_request(cx);
let messages = Arc::new(request.messages);
@@ -1628,6 +1640,7 @@ impl Thread {
tool_use.input.clone(),
&messages,
tool,
window,
cx,
);
}
@@ -1644,9 +1657,10 @@ impl Thread {
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
tool: Arc<dyn Tool>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) {
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, window, cx);
self.tool_use
.run_pending_tool(tool_use_id, ui_text.into(), task);
}
@@ -1657,6 +1671,7 @@ impl Thread {
messages: &[LanguageModelRequestMessage],
input: serde_json::Value,
tool: Arc<dyn Tool>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) -> Task<()> {
let tool_name: Arc<str> = tool.name().into();
@@ -1669,6 +1684,7 @@ impl Thread {
messages,
self.project.clone(),
self.action_log.clone(),
window,
cx,
)
};
@@ -1691,7 +1707,7 @@ impl Thread {
output,
cx,
);
thread.tool_finished(tool_use_id, pending_tool_use, false, cx);
thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx);
})
.ok();
}
@@ -1703,6 +1719,7 @@ impl Thread {
tool_use_id: LanguageModelToolUseId,
pending_tool_use: Option<PendingToolUse>,
canceled: bool,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
if self.all_tools_finished() {
@@ -1710,7 +1727,7 @@ impl Thread {
if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
self.attach_tool_results(cx);
if !canceled {
self.send_to_model(model, cx);
self.send_to_model(model, window, cx);
}
}
}
@@ -1732,7 +1749,11 @@ impl Thread {
/// Cancels the last pending completion, if there are any pending.
///
/// Returns whether a completion was canceled.
pub fn cancel_last_completion(&mut self, cx: &mut Context<Self>) -> bool {
pub fn cancel_last_completion(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) -> bool {
let canceled = if self.pending_completions.pop().is_some() {
true
} else {
@@ -1743,6 +1764,7 @@ impl Thread {
pending_tool_use.id.clone(),
Some(pending_tool_use),
true,
window,
cx,
);
}
@@ -2199,6 +2221,7 @@ impl Thread {
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
let err = Err(anyhow::anyhow!(
@@ -2207,7 +2230,7 @@ impl Thread {
self.tool_use
.insert_tool_output(tool_use_id.clone(), tool_name, err, cx);
self.tool_finished(tool_use_id.clone(), None, true, cx);
self.tool_finished(tool_use_id.clone(), None, true, window, cx);
}
}

View File

@@ -23,7 +23,6 @@ use gpui::{
use language::LanguageRegistry;
use language_model::{
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
ZED_CLOUD_PROVIDER_ID,
};
use project::Project;
use prompt_library::{PromptLibrary, open_prompt_library};
@@ -489,8 +488,8 @@ impl AssistantPanel {
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
// the provider, we want to show a nudge to sign in.
let show_zed_ai_notice = client_status.is_signed_out()
&& model.map_or(true, |model| model.provider.id().0 == ZED_CLOUD_PROVIDER_ID);
let show_zed_ai_notice =
client_status.is_signed_out() && model.map_or(true, |model| model.is_provided_by_zed());
self.show_zed_ai_notice = show_zed_ai_notice;
cx.notify();

View File

@@ -1226,7 +1226,7 @@ impl InlineAssistant {
editor.highlight_rows::<InlineAssist>(
row_range,
cx.theme().status().info_background,
false,
Default::default(),
cx,
);
}
@@ -1291,7 +1291,7 @@ impl InlineAssistant {
editor.highlight_rows::<DeletedLines>(
Anchor::min()..Anchor::max(),
cx.theme().status().deleted_background,
false,
Default::default(),
cx,
);
editor

View File

@@ -28,6 +28,7 @@ serde.workspace = true
serde_json.workspace = true
text.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View File

@@ -10,14 +10,16 @@ use std::sync::Arc;
use anyhow::Result;
use gpui::AnyElement;
use gpui::AnyWindowHandle;
use gpui::Context;
use gpui::IntoElement;
use gpui::Window;
use gpui::{App, Entity, SharedString, Task};
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use icons::IconName;
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use workspace::Workspace;
pub use crate::action_log::*;
pub use crate::tool_registry::*;
@@ -65,6 +67,7 @@ pub trait ToolCard: 'static + Sized {
&mut self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement;
}
@@ -76,6 +79,7 @@ pub struct AnyToolCard {
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement,
}
@@ -86,11 +90,14 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
entity: gpui::AnyEntity,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement {
let entity = entity.downcast::<T>().unwrap();
entity.update(cx, |entity, cx| {
entity.render(status, window, cx).into_any_element()
entity
.render(status, window, workspace, cx)
.into_any_element()
})
}
@@ -102,8 +109,14 @@ impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
}
impl AnyToolCard {
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
(self.render)(self.entity.clone(), status, window, cx)
pub fn render(
&self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut App,
) -> AnyElement {
(self.render)(self.entity.clone(), status, window, workspace, cx)
}
}
@@ -163,6 +176,7 @@ pub trait Tool: 'static + Send + Sync {
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult;
}

View File

@@ -14,10 +14,11 @@ path = "src/assistant_tools.rs"
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
collections.workspace = true
component.workspace = true
feature_flags.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
@@ -36,11 +37,14 @@ serde_json.workspace = true
ui.workspace = true
util.workspace = true
web_search.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
worktree.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }

View File

@@ -29,9 +29,9 @@ use std::sync::Arc;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use feature_flags::FeatureFlagAppExt;
use gpui::App;
use http_client::HttpClientWithUrl;
use language_model::LanguageModelRegistry;
use move_path_tool::MovePathTool;
use web_search_tool::WebSearchTool;
@@ -56,7 +56,10 @@ use crate::symbol_info_tool::SymbolInfoTool;
use crate::terminal_tool::TerminalTool;
use crate::thinking_tool::ThinkingTool;
pub use create_file_tool::CreateFileToolInput;
pub use edit_file_tool::EditFileToolInput;
pub use path_search_tool::PathSearchToolInput;
pub use read_file_tool::ReadFileToolInput;
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
assistant_tool::init(cx);
@@ -85,34 +88,45 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(ThinkingTool);
registry.register_tool(FetchTool::new(http_client));
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
move |is_enabled, cx| {
if is_enabled {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
cx.subscribe(
&LanguageModelRegistry::global(cx),
move |registry, event, cx| match event {
language_model::Event::DefaultModelChanged => {
let using_zed_provider = registry
.read(cx)
.default_model()
.map_or(false, |default| default.is_provided_by_zed());
if using_zed_provider {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
}
}
}
})
_ => {}
},
)
.detach();
}
#[cfg(test)]
mod tests {
use client::Client;
use clock::FakeSystemClock;
use http_client::FakeHttpClient;
use super::*;
#[gpui::test]
fn test_builtin_tool_schema_compatibility(cx: &mut App) {
crate::init(
Arc::new(http_client::HttpClientWithUrl::new(
FakeHttpClient::with_200_response(),
"https://zed.dev",
None,
)),
settings::init(cx);
let client = Client::new(
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_200_response(),
cx,
);
language_model::init(client.clone(), cx);
crate::init(client.http_client(), cx);
for tool in ToolRegistry::global(cx).tools() {
let actual_schema = tool

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolWorkingSet};
use futures::future::join_all;
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -218,6 +218,7 @@ impl Tool for BatchTool {
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<BatchToolInput>(input) {
@@ -258,7 +259,9 @@ impl Tool for BatchTool {
let action_log = action_log.clone();
let messages = messages.clone();
let tool_result = cx
.update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))
.update(|cx| {
tool.run(invocation.input, &messages, project, action_log, window, cx)
})
.map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?;
tasks.push(tool_result.output);

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{self, Anchor, Buffer, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{self, LspAction, Project};
@@ -140,6 +140,7 @@ impl Tool for CodeActionTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CodeActionToolInput>(input) {

View File

@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use collections::IndexMap;
use gpui::{App, AsyncApp, Entity, Task};
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
use language::{OutlineItem, ParseStatus, Point};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, Symbol};
@@ -128,6 +128,7 @@ impl Tool for CodeSymbolsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CodeSymbolsInput>(input) {

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -102,6 +102,7 @@ impl Tool for ContentsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ContentsToolInput>(input) {

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -76,6 +77,7 @@ impl Tool for CopyPathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CopyPathToolInput>(input) {

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -67,6 +68,7 @@ impl Tool for CreateDirectoryTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
@@ -89,6 +90,7 @@ impl Tool for CreateFileTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{Project, ProjectPath};
use schemars::JsonSchema;
@@ -62,6 +62,7 @@ impl Tool for DeletePathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -82,6 +82,7 @@ impl Tool for DiagnosticsTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
match serde_json::from_value::<DiagnosticsToolInput>(input)

View File

@@ -1,15 +1,28 @@
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
use crate::{
replace::{replace_exact, replace_with_flexible_indent},
schema::json_schema_for,
};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, AsyncApp, Entity, Task};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUseStatus};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
use gpui::{
AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId, Task, WeakEntity,
};
use language::{
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use ui::IconName;
use crate::replace::replace_exact;
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use ui::{Disclosure, Tooltip, Window, prelude::*};
use util::ResultExt;
use workspace::Workspace;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
@@ -113,6 +126,7 @@ impl Tool for EditFileTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<EditFileToolInput>(input) {
@@ -120,7 +134,18 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
cx.spawn(async move |cx: &mut AsyncApp| {
let card = window.and_then(|window| {
window
.update(cx, |_, window, cx| {
cx.new(|cx| {
EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
})
})
.ok()
});
let card_clone = card.clone();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
@@ -128,26 +153,38 @@ impl Tool for EditFileTool {
})??;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
if input.old_string.is_empty() {
return Err(anyhow!("`old_string` cannot be empty. Use a different tool if you want to create a file."));
return Err(anyhow!(
"`old_string` can't be empty, use another tool if you want to create a file."
));
}
if input.old_string == input.new_string {
return Err(anyhow!("The `old_string` and `new_string` are identical, so no changes would be made."));
return Err(anyhow!(
"The `old_string` and `new_string` are identical, so no changes would be made."
));
}
let result = cx
.background_spawn(async move {
// Try to match exactly
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&input.old_string, &input.new_string, &snapshot))?;
.await
// If that fails, try being flexible about indentation
.or_else(|| {
replace_with_flexible_indent(
&input.old_string,
&input.new_string,
&snapshot,
)
})?;
if diff.edits.is_empty() {
return None;
@@ -177,41 +214,409 @@ impl Tool for EditFileTool {
}
})?;
return Err(err)
return Err(err);
};
let snapshot = cx.update(|cx| {
action_log.update(cx, |log, cx| {
log.track_buffer(buffer.clone(), cx)
});
action_log.update(cx, |log, cx| log.track_buffer(buffer.clone(), cx));
let snapshot = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, cx);
buffer.finalize_last_transaction();
buffer.snapshot()
});
action_log.update(cx, |log, cx| {
log.buffer_edited(buffer.clone(), cx)
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
snapshot
})?;
project.update( cx, |project, cx| {
project.save_buffer(buffer, cx)
})?.await?;
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await?;
let diff_str = cx.background_spawn(async move {
let new_text = snapshot.text();
language::unified_diff(&old_text, &new_text)
}).await;
let new_text = snapshot.text();
let diff_str = cx
.background_spawn({
let old_text = old_text.clone();
let new_text = new_text.clone();
async move { language::unified_diff(&old_text, &new_text) }
})
.await;
if let Some(card) = card_clone {
card.update(cx, |card, cx| {
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
})
.log_err();
}
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input.path.display(), diff_str))
Ok(format!(
"Edited {}:\n\n```diff\n{}\n```",
input.path.display(),
diff_str
))
});
}).into()
ToolResult {
output: task,
card: card.map(AnyToolCard::from),
}
}
}
pub struct EditFileToolCard {
path: PathBuf,
editor: Entity<Editor>,
multibuffer: Entity<MultiBuffer>,
project: Entity<Project>,
diff_task: Option<Task<Result<()>>>,
preview_expanded: bool,
full_height_expanded: bool,
editor_unique_id: EntityId,
}
impl EditFileToolCard {
fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
let editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: true,
},
multibuffer.clone(),
Some(project.clone()),
window,
cx,
);
editor.set_show_scrollbars(false, cx);
editor.set_show_gutter(false, cx);
editor.disable_inline_diagnostics();
editor.disable_scrolling(cx);
editor.disable_expand_excerpt_buttons(cx);
editor.set_show_breakpoints(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_expand_all_diff_hunks(cx);
editor
});
Self {
editor_unique_id: editor.entity_id(),
path,
project,
editor,
multibuffer,
diff_task: None,
preview_expanded: true,
full_height_expanded: false,
}
}
fn set_diff(
&mut self,
path: Arc<Path>,
old_text: String,
new_text: String,
cx: &mut Context<Self>,
) {
let language_registry = self.project.read(cx).languages().clone();
self.diff_task = Some(cx.spawn(async move |this, cx| {
let buffer = build_buffer(new_text, path.clone(), &language_registry, cx).await?;
let buffer_diff = build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multibuffer, cx| {
let snapshot = buffer.read(cx).snapshot();
let diff = buffer_diff.read(cx);
let diff_hunk_ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
.collect::<Vec<_>>();
let (_, is_newly_added) = multibuffer.set_excerpts_for_path(
PathKey::for_buffer(&buffer, cx),
buffer,
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
debug_assert!(is_newly_added);
multibuffer.add_diff(buffer_diff, cx);
});
cx.notify();
})
}));
}
}
impl ToolCard for EditFileToolCard {
fn render(
&mut self,
status: &ToolUseStatus,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let failed = matches!(status, ToolUseStatus::Error(_));
let path_label_button = h_flex()
.id(("edit-tool-path-label-button", self.editor_unique_id))
.w_full()
.max_w_full()
.px_1()
.gap_0p5()
.cursor_pointer()
.rounded_sm()
.opacity(0.8)
.hover(|label| {
label
.opacity(1.)
.bg(cx.theme().colors().element_hover.opacity(0.5))
})
.tooltip(Tooltip::text("Jump to File"))
.child(
h_flex()
.child(
Icon::new(IconName::Pencil)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
div()
.text_size(rems(0.8125))
.child(self.path.display().to_string())
.ml_1p5()
.mr_0p5(),
)
.child(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
.color(Color::Ignored),
),
)
.on_click({
let path = self.path.clone();
let workspace = workspace.clone();
move |_, window, cx| {
workspace
.update(cx, {
|workspace, cx| {
let Some(project_path) =
workspace.project().read(cx).find_project_path(&path, cx)
else {
return;
};
let open_task =
workspace.open_path(project_path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
language::Point::new(0, 0),
window,
cx,
);
})
.log_err();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
.ok();
}
})
.into_any_element();
let codeblock_header_bg = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let codeblock_header = h_flex()
.flex_none()
.p_1()
.gap_1()
.justify_between()
.rounded_t_md()
.when(!failed, |header| header.bg(codeblock_header_bg))
.child(path_label_button)
.map(|container| {
if failed {
container.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
} else {
container.child(
Disclosure::new(
("edit-file-disclosure", self.editor_unique_id),
self.preview_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.preview_expanded = !this.preview_expanded;
},
)),
)
}
});
let editor = self.editor.update(cx, |editor, cx| {
editor.render(window, cx).into_any_element()
});
let (full_height_icon, full_height_tooltip_label) = if self.full_height_expanded {
(IconName::ChevronUp, "Collapse Code Block")
} else {
(IconName::ChevronDown, "Expand Code Block")
};
let gradient_overlay = div()
.absolute()
.bottom_0()
.left_0()
.w_full()
.h_2_5()
.rounded_b_lg()
.bg(gpui::linear_gradient(
0.,
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
));
let border_color = cx.theme().colors().border.opacity(0.6);
v_flex()
.mb_2()
.border_1()
.when(failed, |card| card.border_dashed())
.border_color(border_color)
.rounded_lg()
.overflow_hidden()
.child(codeblock_header)
.when(!failed && self.preview_expanded, |card| {
card.child(
v_flex()
.relative()
.overflow_hidden()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.map(|editor_container| {
if self.full_height_expanded {
editor_container.h_full()
} else {
editor_container.max_h_64()
}
})
.child(div().pl_1().child(editor))
.when(!self.full_height_expanded, |editor_container| {
editor_container.child(gradient_overlay)
}),
)
})
.when(!failed && self.preview_expanded, |card| {
card.child(
h_flex()
.id(("edit-tool-card-inner-hflex", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.rounded_b_md()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
)
})
}
}
async fn build_buffer(
mut text: String,
path: Arc<Path>,
language_registry: &Arc<language::LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<Buffer>> {
let line_ending = LineEnding::detect(&text);
LineEnding::normalize(&mut text);
let text = Rope::from(text);
let language = cx
.update(|_cx| language_registry.language_for_file_path(&path))?
.await
.ok();
let buffer = cx.new(|cx| {
let buffer = TextBuffer::new_normalized(
0,
cx.entity_id().as_non_zero_u64().into(),
line_ending,
text,
);
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
buffer.set_language(language, cx);
buffer
})?;
Ok(buffer)
}
async fn build_buffer_diff(
mut old_text: String,
buffer: &Entity<Buffer>,
language_registry: &Arc<LanguageRegistry>,
cx: &mut AsyncApp,
) -> Result<Entity<BufferDiff>> {
LineEnding::normalize(&mut old_text);
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
let base_buffer = cx
.update(|cx| {
Buffer::build_snapshot(
old_text.clone().into(),
buffer.language().cloned(),
Some(language_registry.clone()),
cx,
)
})?
.await;
let diff_snapshot = cx
.update(|cx| {
BufferDiffSnapshot::new_with_base_buffer(
buffer.text.clone(),
Some(old_text.into()),
base_buffer,
cx,
)
})?
.await;
cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer.text, cx);
diff.set_snapshot(diff_snapshot, &buffer.text, cx);
diff
})
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -6,7 +6,7 @@ use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::AsyncReadExt as _;
use gpui::{App, AppContext as _, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use http_client::{AsyncBody, HttpClientWithUrl};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -145,6 +145,7 @@ impl Tool for FetchTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<FetchToolInput>(input) {

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::StreamExt;
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::OffsetRangeExt;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{
@@ -96,6 +96,7 @@ impl Tool for GrepTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
const CONTEXT_LINES: u32 = 2;
@@ -405,7 +406,7 @@ mod tests {
) -> String {
let tool = Arc::new(GrepTool);
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
let task = cx.update(|cx| tool.run(input, &[], project, action_log, cx));
let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx));
match task.output.await {
Ok(result) => result,

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -76,6 +76,7 @@ impl Tool for ListDirectoryTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -89,6 +89,7 @@ impl Tool for MovePathTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<MovePathToolInput>(input) {

View File

@@ -4,7 +4,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use chrono::{Local, Utc};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -59,6 +59,7 @@ impl Tool for NowTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
_cx: &mut App,
) -> ToolResult {
let input: NowToolInput = match serde_json::from_value(input) {

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -52,6 +52,7 @@ impl Tool for OpenTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input: OpenToolInput = match serde_json::from_value(input) {

View File

@@ -1,7 +1,7 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -70,6 +70,7 @@ impl Tool for PathSearchTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let (offset, glob) = match serde_json::from_value::<PathSearchToolInput>(input) {

View File

@@ -1,7 +1,8 @@
use crate::{code_symbols_tool::file_outline, schema::json_schema_for};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use indoc::formatdoc;
use itertools::Itertools;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
@@ -87,6 +88,7 @@ impl Tool for ReadFileTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<ReadFileToolInput>(input) {
@@ -193,7 +195,7 @@ mod test {
"path": "root/nonexistent_file.txt"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -223,7 +225,7 @@ mod test {
"path": "root/small_file.txt"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -253,7 +255,7 @@ mod test {
"path": "root/large_file.rs"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log.clone(), cx)
.run(input, &[], project.clone(), action_log.clone(), None, cx)
.output
})
.await;
@@ -277,7 +279,7 @@ mod test {
"offset": 1
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;
@@ -323,7 +325,7 @@ mod test {
"end_line": 4
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, cx)
.run(input, &[], project.clone(), action_log, None, cx)
.output
})
.await;

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{self, Buffer, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -87,6 +87,7 @@ impl Tool for RenameTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<RenameToolInput>(input) {

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, AsyncApp, Entity, Task};
use gpui::{AnyWindowHandle, App, AsyncApp, Entity, Task};
use language::{self, Anchor, Buffer, BufferSnapshot, Location, Point, ToPoint, ToPointUtf16};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -121,6 +121,7 @@ impl Tool for SymbolInfoTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<SymbolInfoToolInput>(input) {

View File

@@ -3,7 +3,7 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::io::BufReader;
use futures::{AsyncBufReadExt, AsyncReadExt, FutureExt};
use gpui::{App, AppContext, Entity, Task};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -78,6 +78,7 @@ impl Tool for TerminalTool {
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input: TerminalToolInput = match serde_json::from_value(input) {

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
@@ -50,6 +50,7 @@ impl Tool for ThinkingTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
_cx: &mut App,
) -> ToolResult {
// This tool just "thinks out loud" and doesn't perform any actions.

View File

@@ -5,13 +5,16 @@ use crate::ui::ToolCallCardHeader;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{Future, FutureExt, TryFutureExt};
use gpui::{App, AppContext, Context, Entity, IntoElement, Task, Window};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::{IconName, Tooltip, prelude::*};
use web_search::WebSearchRegistry;
use workspace::Workspace;
use zed_llm_client::{WebSearchCitation, WebSearchResponse};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -54,6 +57,7 @@ impl Tool for WebSearchTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
@@ -111,6 +115,7 @@ impl ToolCard for WebSearchToolCard {
&mut self,
_status: &ToolUseStatus,
_window: &mut Window,
_workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let header = match self.response.as_ref() {
@@ -220,8 +225,13 @@ impl Component for WebSearchTool {
div()
.size_full()
.child(in_progress_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Pending, window, cx)
.into_any_element()
tool.render(
&ToolUseStatus::Pending,
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),
@@ -230,8 +240,13 @@ impl Component for WebSearchTool {
div()
.size_full()
.child(successful_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Finished("".into()), window, cx)
.into_any_element()
tool.render(
&ToolUseStatus::Finished("".into()),
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),
@@ -240,8 +255,13 @@ impl Component for WebSearchTool {
div()
.size_full()
.child(error_search.update(cx, |tool, cx| {
tool.render(&ToolUseStatus::Error("".into()), window, cx)
.into_any_element()
tool.render(
&ToolUseStatus::Error("".into()),
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
),

View File

@@ -0,0 +1,2 @@
alter table project_repositories
add column head_commit_details varchar;

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use anyhow::{Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
use gpui::{App, Entity, Task};
use gpui::{AnyWindowHandle, App, Entity, Task};
use icons::IconName;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
@@ -77,6 +77,7 @@ impl Tool for ContextServerTool {
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {

View File

@@ -411,12 +411,12 @@ impl RunningState {
.log_err();
if let Some(thread_id) = thread_id {
this.select_thread(*thread_id, cx);
this.select_thread(*thread_id, window, cx);
}
}
SessionEvent::Threads => {
let threads = this.session.update(cx, |this, cx| this.threads(cx));
this.select_current_thread(&threads, cx);
this.select_current_thread(&threads, window, cx);
}
SessionEvent::CapabilitiesLoaded => {
let capabilities = this.capabilities(cx);
@@ -731,6 +731,7 @@ impl RunningState {
pub fn select_current_thread(
&mut self,
threads: &Vec<(Thread, ThreadStatus)>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let selected_thread = self
@@ -743,7 +744,7 @@ impl RunningState {
};
if Some(ThreadId(selected_thread.id)) != self.thread_id {
self.select_thread(ThreadId(selected_thread.id), cx);
self.select_thread(ThreadId(selected_thread.id), window, cx);
}
}
@@ -756,7 +757,7 @@ impl RunningState {
.map(|id| self.session().read(cx).thread_status(id))
}
fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
if self.thread_id.is_some_and(|id| id == thread_id) {
return;
}
@@ -764,8 +765,7 @@ impl RunningState {
self.thread_id = Some(thread_id);
self.stack_frame_list
.update(cx, |list, cx| list.refresh(cx));
cx.notify();
.update(cx, |list, cx| list.schedule_refresh(true, window, cx));
}
pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
@@ -917,9 +917,9 @@ impl RunningState {
for (thread, _) in threads {
let state = state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |_, cx| {
this = this.entry(thread.name, None, move |window, cx| {
state.update(cx, |state, cx| {
state.select_thread(ThreadId(thread_id), cx);
state.select_thread(ThreadId(thread_id), window, cx);
});
});
}

View File

@@ -1,5 +1,6 @@
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Result, anyhow};
use dap::StackFrameId;
@@ -28,11 +29,11 @@ pub struct StackFrameList {
_subscription: Subscription,
session: Entity<Session>,
state: WeakEntity<RunningState>,
invalidate: bool,
entries: Vec<StackFrameEntry>,
workspace: WeakEntity<Workspace>,
selected_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
_refresh_task: Task<()>,
}
#[allow(clippy::large_enum_variant)]
@@ -68,14 +69,17 @@ impl StackFrameList {
);
let _subscription =
cx.subscribe_in(&session, window, |this, _, event, _, cx| match event {
SessionEvent::Stopped(_) | SessionEvent::StackTrace | SessionEvent::Threads => {
this.refresh(cx);
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
SessionEvent::Threads => {
this.schedule_refresh(false, window, cx);
}
SessionEvent::Stopped(..) | SessionEvent::StackTrace => {
this.schedule_refresh(true, window, cx);
}
_ => {}
});
Self {
let mut this = Self {
scrollbar_state: ScrollbarState::new(list.clone()),
list,
session,
@@ -83,10 +87,12 @@ impl StackFrameList {
focus_handle,
state,
_subscription,
invalidate: true,
entries: Default::default(),
selected_stack_frame_id: None,
}
_refresh_task: Task::ready(()),
};
this.schedule_refresh(true, window, cx);
this
}
#[cfg(test)]
@@ -136,10 +142,32 @@ impl StackFrameList {
self.selected_stack_frame_id
}
pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
self.invalidate = true;
self.entries.clear();
cx.notify();
pub(super) fn schedule_refresh(
&mut self,
select_first: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
self._refresh_task = cx.spawn_in(window, async move |this, cx| {
let debounce = this
.update(cx, |this, cx| {
let new_stack_frames = this.stack_frames(cx);
new_stack_frames.is_empty() && !this.entries.is_empty()
})
.ok()
.unwrap_or_default();
if debounce {
cx.background_executor().timer(REFRESH_DEBOUNCE).await;
}
this.update_in(cx, |this, window, cx| {
this.build_entries(select_first, window, cx);
cx.notify();
})
.ok();
})
}
pub fn build_entries(
@@ -515,13 +543,7 @@ impl StackFrameList {
}
impl Render for StackFrameList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.invalidate {
self.build_entries(self.entries.is_empty(), window, cx);
self.invalidate = false;
cx.notify();
}
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.p_1()

View File

@@ -152,7 +152,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
session
.mode()
.as_running()
@@ -162,6 +162,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
window,
cx,
);
});
@@ -330,7 +331,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
session
.mode()
.as_running()
@@ -340,6 +341,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
window,
cx,
);
});
@@ -704,7 +706,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
session
.mode()
.as_running()
@@ -714,6 +716,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
window,
cx,
);
});

View File

@@ -269,6 +269,12 @@ enum DocumentHighlightWrite {}
enum InputComposition {}
enum SelectedTextHighlight {}
pub enum ConflictsOuter {}
pub enum ConflictsOurs {}
pub enum ConflictsTheirs {}
pub enum ConflictsOursMarker {}
pub enum ConflictsTheirsMarker {}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Navigated {
Yes,
@@ -427,6 +433,8 @@ pub enum EditorMode {
scale_ui_elements_with_buffer_font_size: bool,
/// When set to `true`, the editor will render a background for the active line.
show_active_line_background: bool,
/// When set to `true`, the editor's height will be determined by its content.
sized_by_content: bool,
},
}
@@ -435,6 +443,7 @@ impl EditorMode {
Self::Full {
scale_ui_elements_with_buffer_font_size: true,
show_active_line_background: true,
sized_by_content: false,
}
}
@@ -694,6 +703,10 @@ pub trait Addon: 'static {
}
fn to_any(&self) -> &dyn std::any::Any;
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
None
}
}
/// A set of caret positions, registered when the editor was edited.
@@ -788,6 +801,8 @@ pub struct Editor {
show_breadcrumbs: bool,
show_gutter: bool,
show_scrollbars: bool,
disable_scrolling: bool,
disable_expand_excerpt_buttons: bool,
show_line_numbers: Option<bool>,
use_relative_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
@@ -1083,11 +1098,27 @@ impl SelectionHistory {
}
}
#[derive(Clone, Copy)]
pub struct RowHighlightOptions {
pub autoscroll: bool,
pub include_gutter: bool,
}
impl Default for RowHighlightOptions {
fn default() -> Self {
Self {
autoscroll: Default::default(),
include_gutter: true,
}
}
}
struct RowHighlight {
index: usize,
range: Range<Anchor>,
color: Hsla,
should_autoscroll: bool,
options: RowHighlightOptions,
type_id: TypeId,
}
#[derive(Clone, Debug)]
@@ -1563,11 +1594,13 @@ impl Editor {
blink_manager: blink_manager.clone(),
show_local_selections: true,
show_scrollbars: true,
disable_scrolling: true,
mode,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode.is_full(),
show_line_numbers: None,
use_relative_line_numbers: None,
disable_expand_excerpt_buttons: false,
show_git_diff_gutter: None,
show_code_actions: None,
show_runnables: None,
@@ -5942,7 +5975,10 @@ impl Editor {
self.highlight_rows::<EditPredictionPreview>(
target..target,
cx.theme().colors().editor_highlighted_line_background,
true,
RowHighlightOptions {
autoscroll: true,
..Default::default()
},
cx,
);
self.request_autoscroll(Autoscroll::fit(), cx);
@@ -11798,6 +11834,7 @@ impl Editor {
fn select_next_match_ranges(
this: &mut Editor,
range: Range<usize>,
reversed: bool,
replace_newest: bool,
auto_scroll: Option<Autoscroll>,
window: &mut Window,
@@ -11808,7 +11845,11 @@ impl Editor {
if replace_newest {
s.delete(s.newest_anchor().id);
}
s.insert_range(range.clone());
if reversed {
s.insert_range(range.end..range.start);
} else {
s.insert_range(range);
}
});
}
@@ -11859,6 +11900,7 @@ impl Editor {
select_next_match_ranges(
self,
next_selected_range,
last_selection.reversed,
replace_newest,
autoscroll,
window,
@@ -11917,6 +11959,7 @@ impl Editor {
select_next_match_ranges(
self,
selection.start..selection.end,
selection.reversed,
replace_newest,
autoscroll,
window,
@@ -12084,7 +12127,11 @@ impl Editor {
if action.replace_newest {
s.delete(s.newest_anchor().id);
}
s.insert_range(next_selected_range);
if last_selection.reversed {
s.insert_range(next_selected_range.end..next_selected_range.start);
} else {
s.insert_range(next_selected_range);
}
});
} else {
select_prev_state.done = true;
@@ -13449,7 +13496,7 @@ impl Editor {
start..end,
highlight_color
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
false,
Default::default(),
cx,
);
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
@@ -16146,11 +16193,21 @@ impl Editor {
cx.notify();
}
pub fn disable_scrolling(&mut self, cx: &mut Context<Self>) {
self.disable_scrolling = true;
cx.notify();
}
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context<Self>) {
self.show_line_numbers = Some(show_line_numbers);
cx.notify();
}
pub fn disable_expand_excerpt_buttons(&mut self, cx: &mut Context<Self>) {
self.disable_expand_excerpt_buttons = true;
cx.notify();
}
pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context<Self>) {
self.show_git_diff_gutter = Some(show_git_diff_gutter);
cx.notify();
@@ -16765,7 +16822,7 @@ impl Editor {
&mut self,
range: Range<Anchor>,
color: Hsla,
should_autoscroll: bool,
options: RowHighlightOptions,
cx: &mut Context<Self>,
) {
let snapshot = self.buffer().read(cx).snapshot(cx);
@@ -16797,7 +16854,7 @@ impl Editor {
merged = true;
prev_highlight.index = index;
prev_highlight.color = color;
prev_highlight.should_autoscroll = should_autoscroll;
prev_highlight.options = options;
}
}
@@ -16808,7 +16865,8 @@ impl Editor {
range: range.clone(),
index,
color,
should_autoscroll,
options,
type_id: TypeId::of::<T>(),
},
);
}
@@ -16914,7 +16972,15 @@ impl Editor {
used_highlight_orders.entry(row).or_insert(highlight.index);
if highlight.index >= *used_index {
*used_index = highlight.index;
unique_rows.insert(DisplayRow(row), highlight.color.into());
unique_rows.insert(
DisplayRow(row),
LineHighlight {
include_gutter: highlight.options.include_gutter,
border: None,
background: highlight.color.into(),
type_id: Some(highlight.type_id),
},
);
}
}
unique_rows
@@ -16930,7 +16996,7 @@ impl Editor {
.values()
.flat_map(|highlighted_rows| highlighted_rows.iter())
.filter_map(|highlight| {
if highlight.should_autoscroll {
if highlight.options.autoscroll {
Some(highlight.range.start.to_display_point(snapshot).row())
} else {
None
@@ -17405,13 +17471,19 @@ impl Editor {
});
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
}
multi_buffer::Event::ExcerptsRemoved { ids } => {
multi_buffer::Event::ExcerptsRemoved {
ids,
removed_buffer_ids,
} => {
self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
let buffer = self.buffer.read(cx);
self.registered_buffers
.retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some());
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
cx.emit(EditorEvent::ExcerptsRemoved {
ids: ids.clone(),
removed_buffer_ids: removed_buffer_ids.clone(),
})
}
multi_buffer::Event::ExcerptsEdited {
excerpt_ids,
@@ -18219,6 +18291,13 @@ impl Editor {
.and_then(|item| item.to_any().downcast_ref::<T>())
}
pub fn addon_mut<T: Addon>(&mut self) -> Option<&mut T> {
let type_id = std::any::TypeId::of::<T>();
self.addons
.get_mut(&type_id)
.and_then(|item| item.to_any_mut()?.downcast_mut::<T>())
}
fn character_size(&self, window: &mut Window) -> gpui::Size<Pixels> {
let text_layout_details = self.text_layout_details(window);
let style = &text_layout_details.editor_style;
@@ -19732,6 +19811,7 @@ pub enum EditorEvent {
},
ExcerptsRemoved {
ids: Vec<ExcerptId>,
removed_buffer_ids: Vec<BufferId>,
},
BufferFoldToggled {
ids: Vec<ExcerptId>,
@@ -20672,24 +20752,8 @@ impl Render for MissingEditPredictionKeybindingTooltip {
pub struct LineHighlight {
pub background: Background,
pub border: Option<gpui::Hsla>,
}
impl From<Hsla> for LineHighlight {
fn from(hsla: Hsla) -> Self {
Self {
background: hsla.into(),
border: None,
}
}
}
impl From<Background> for LineHighlight {
fn from(background: Background) -> Self {
Self {
background,
border: None,
}
}
pub include_gutter: bool,
pub type_id: Option<TypeId>,
}
fn render_diff_hunk_controls(

View File

@@ -5825,6 +5825,13 @@ async fn test_select_next(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
// Test selection direction should be preserved
cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
.unwrap();
cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
}
#[gpui::test]
@@ -6011,6 +6018,25 @@ let «fooˇ» = 2;
let foo = 2;
let foo = «2ˇ»;"#,
);
// Test last selection direction should be preserved
cx.set_state(
r#"let foo = 2;
let foo = 2;
let «fooˇ» = 2;
let «ˇfoo» = 2;
let foo = 2;"#,
);
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
.unwrap();
cx.assert_editor_state(
r#"let foo = 2;
let foo = 2;
let «fooˇ» = 2;
let «ˇfoo» = 2;
let «ˇfoo» = 2;"#,
);
}
#[gpui::test]
@@ -6138,25 +6164,26 @@ async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
// selection direction is preserved
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
.unwrap();
cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
}
#[gpui::test]
@@ -10419,6 +10446,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: false,
},
multi_buffer.clone(),
Some(project.clone()),

View File

@@ -1,6 +1,7 @@
use crate::{
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
ChunkRendererContext, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId,
ChunkRendererContext, ChunkReplacement, ConflictsOurs, ConflictsOursMarker, ConflictsOuter,
ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId,
DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
@@ -2183,6 +2184,10 @@ impl EditorElement {
window: &mut Window,
cx: &mut App,
) -> Vec<Option<(AnyElement, gpui::Point<Pixels>)>> {
if self.editor.read(cx).disable_expand_excerpt_buttons {
return vec![];
}
let editor_font_size = self.style.text.font_size.to_pixels(window.rem_size()) * 1.2;
let scroll_top = scroll_position.y * line_height;
@@ -4036,6 +4041,7 @@ impl EditorElement {
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
highlighted_rows: &BTreeMap<DisplayRow, LineHighlight>,
editor: Entity<Editor>,
window: &mut Window,
cx: &mut App,
@@ -4064,6 +4070,22 @@ impl EditorElement {
{
continue;
}
if highlighted_rows
.get(&display_row_range.start)
.and_then(|highlight| highlight.type_id)
.is_some_and(|type_id| {
[
TypeId::of::<ConflictsOuter>(),
TypeId::of::<ConflictsOursMarker>(),
TypeId::of::<ConflictsOurs>(),
TypeId::of::<ConflictsTheirs>(),
TypeId::of::<ConflictsTheirsMarker>(),
]
.contains(&type_id)
})
{
continue;
}
let row_ix = (display_row_range.start - row_range.start).0 as usize;
if row_infos[row_ix].diff_status.is_none() {
continue;
@@ -4258,14 +4280,21 @@ impl EditorElement {
highlight_row_end: DisplayRow,
highlight: crate::LineHighlight,
edges| {
let mut origin_x = layout.hitbox.left();
let mut width = layout.hitbox.size.width;
if !highlight.include_gutter {
origin_x += layout.gutter_hitbox.size.width;
width -= layout.gutter_hitbox.size.width;
}
let origin = point(
layout.hitbox.origin.x,
origin_x,
layout.hitbox.origin.y
+ (highlight_row_start.as_f32() - scroll_top)
* layout.position_map.line_height,
);
let size = size(
layout.hitbox.size.width,
width,
layout.position_map.line_height
* highlight_row_end.next_row().minus(highlight_row_start) as f32,
);
@@ -5512,7 +5541,9 @@ impl EditorElement {
}
fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) {
self.paint_scroll_wheel_listener(layout, window, cx);
if !self.editor.read(cx).disable_scrolling {
self.paint_scroll_wheel_listener(layout, window, cx);
}
window.on_mouse_event({
let position_map = layout.position_map.clone();
@@ -6563,10 +6594,21 @@ impl Element for EditorElement {
},
)
}
EditorMode::Full { .. } => {
EditorMode::Full {
sized_by_content, ..
} => {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = relative(1.).into();
if sized_by_content {
let snapshot = editor.snapshot(window, cx);
let line_height =
self.style.text.line_height_in_pixels(window.rem_size());
let scroll_height =
(snapshot.max_point().row().next_row().0 as f32) * line_height;
style.size.height = scroll_height.into();
} else {
style.size.height = relative(1.).into();
}
window.request_layout(style, None, cx)
}
};
@@ -6789,10 +6831,16 @@ impl Element for EditorElement {
} else {
background_color.opacity(0.36)
}),
include_gutter: true,
type_id: None,
};
let filled_highlight =
solid_background(background_color.opacity(hunk_opacity)).into();
let filled_highlight = LineHighlight {
background: solid_background(background_color.opacity(hunk_opacity)),
border: None,
include_gutter: true,
type_id: None,
};
let background = if Self::diff_hunk_hollow(diff_status, cx) {
hollow_highlight
@@ -7551,6 +7599,7 @@ impl Element for EditorElement {
line_height,
scroll_pixel_position,
&display_hunks,
&highlighted_rows,
self.editor.clone(),
window,
cx,

View File

@@ -288,7 +288,7 @@ impl FollowableItem for Editor {
}
true
}
EditorEvent::ExcerptsRemoved { ids } => {
EditorEvent::ExcerptsRemoved { ids, .. } => {
update
.deleted_excerpts
.extend(ids.iter().map(ExcerptId::to_proto));

View File

@@ -11,6 +11,7 @@ assistant_tool.workspace = true
assistant_tools.workspace = true
async-trait.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
clap.workspace = true
client.workspace = true
@@ -23,7 +24,7 @@ fs.workspace = true
futures.workspace = true
gpui.workspace = true
gpui_tokio.workspace = true
handlebars = { workspace = true, features = ["rust-embed"] }
handlebars.workspace = true
language.workspace = true
language_extension.workspace = true
language_model.workspace = true
@@ -36,7 +37,6 @@ prompt_store.workspace = true
regex.workspace = true
release_channel.workspace = true
reqwest_client.workspace = true
rust-embed.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
@@ -48,7 +48,6 @@ unindent.workspace = true
util.workspace = true
uuid = { version = "1.6", features = ["v4"] }
workspace-hack.workspace = true
[[bin]]
name = "eval"
path = "src/eval.rs"

View File

@@ -1,16 +1,19 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::fmt::Write;
use std::fmt::{self};
use crate::example::AssertionGroupId;
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
pub struct AssertionsReport {
pub ran: Vec<RanAssertion>,
pub max: Option<usize>,
pub ran: Vec<Assertion>,
pub groups: BTreeSet<AssertionGroupId>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RanAssertion {
pub id: String,
pub struct Assertion {
pub group_id: AssertionGroupId,
pub result: Result<RanAssertionResult, String>,
}
@@ -21,19 +24,12 @@ pub struct RanAssertionResult {
}
impl AssertionsReport {
pub fn new(max: Option<usize>) -> Self {
AssertionsReport {
ran: Vec::new(),
max,
}
}
pub fn is_empty(&self) -> bool {
self.ran.is_empty()
}
pub fn total_count(&self) -> usize {
self.run_count().max(self.max.unwrap_or(0))
self.run_count()
}
pub fn run_count(&self) -> usize {
@@ -91,7 +87,7 @@ pub fn display_error_row(f: &mut String, round: usize, error: String) -> fmt::Re
)
}
pub fn display_table_row(f: &mut String, round: usize, assertion: &RanAssertion) -> fmt::Result {
pub fn display_table_row(f: &mut String, round: usize, assertion: &Assertion) -> fmt::Result {
let result = match &assertion.result {
Ok(result) if result.passed => "\x1b[32m✔ Passed\x1b[0m",
Ok(_) => "\x1b[31m✗ Failed\x1b[0m",
@@ -102,7 +98,7 @@ pub fn display_table_row(f: &mut String, round: usize, assertion: &RanAssertion)
f,
"│ {:^ROUND_WIDTH$} │ {:<ASSERTIONS_WIDTH$} │ {:>RESULTS_WIDTH$} │",
round,
truncate(&assertion.id, ASSERTIONS_WIDTH),
truncate(&assertion.group_id.to_string(), ASSERTIONS_WIDTH),
result
)
}

View File

@@ -1,14 +1,14 @@
mod assertions;
mod example;
mod example2;
mod examples;
mod ids;
mod instance;
mod templates;
mod tool_metrics;
use assertions::display_error_row;
use example::ExampleMetadata;
use instance::{ExampleInstance, JudgeOutput, RunOutput, run_git};
use serde::Serialize;
pub(crate) use tool_metrics::*;
use ::fs::RealFs;
@@ -621,7 +621,7 @@ pub fn git_branch_for_path(repo_path: &Path) -> String {
}
async fn judge_example(
example: ExampleInstance,
instance: ExampleInstance,
model: Arc<dyn LanguageModel>,
zed_commit_sha: &str,
zed_branch_name: &str,
@@ -630,35 +630,41 @@ async fn judge_example(
enable_telemetry: bool,
cx: &AsyncApp,
) -> JudgeOutput {
let judge_output = example.judge(model.clone(), &run_output, cx).await;
let judge_output = instance.judge(model.clone(), &run_output, cx).await;
let evaluated_example = EvaluatedExample {
example: instance.example.meta(),
run: RunMetadata {
zed_commit_sha: zed_commit_sha.to_string(),
zed_branch_name: zed_branch_name.to_string(),
run_id: run_id.to_string(),
},
run_output: run_output.clone(),
judge_output: judge_output.clone(),
};
if enable_telemetry {
telemetry::event!(
"Agent Example Evaluated",
zed_commit_sha = zed_commit_sha,
zed_branch_name = zed_branch_name,
run_id = run_id,
example_name = example.name.clone(),
example_repetition = example.repetition,
diff_evaluation = judge_output.diff.clone(),
thread_evaluation = judge_output.thread.clone(),
tool_metrics = run_output.tool_metrics,
response_count = run_output.response_count,
token_usage = run_output.token_usage,
model = model.telemetry_id(),
model_provider = model.provider_id().to_string(),
repository_url = example.repo_url(),
repository_revision = example.revision(),
diagnostic_summary_before = run_output.diagnostic_summary_before,
diagnostic_summary_after = run_output.diagnostic_summary_after,
diagnostics_before = run_output.diagnostics_before,
diagnostics_after = run_output.diagnostics_after,
);
telemetry::event!("Agent Example Evaluated", evaluated_example);
}
judge_output
}
#[derive(Serialize)]
struct EvaluatedExample {
example: ExampleMetadata,
run: RunMetadata,
run_output: RunOutput,
judge_output: JudgeOutput,
}
#[derive(Serialize)]
struct RunMetadata {
zed_commit_sha: String,
zed_branch_name: String,
run_id: String,
}
const HEADER_WIDTH: usize = 65;
fn print_h1(header: &str) {

View File

@@ -1,20 +1,24 @@
use std::{
error::Error,
fmt::{self, Debug},
path::Path,
sync::{Arc, Mutex},
time::Duration,
};
use crate::{
ToolMetrics,
assertions::{AssertionsReport, RanAssertion, RanAssertionResult},
assertions::{Assertion, AssertionsReport, RanAssertionResult},
};
use agent::ThreadEvent;
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use buffer_diff::DiffHunkStatus;
use collections::HashMap;
use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
use gpui::{AppContext, AsyncApp, Entity};
use language_model::{LanguageModel, Role, StopReason};
use serde::{Deserialize, Serialize};
pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
@@ -32,20 +36,19 @@ pub trait Example {
#[derive(Clone, Debug)]
pub struct JudgeAssertion {
pub id: String,
pub group_id: String,
pub description: String,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize)]
pub struct ExampleMetadata {
pub name: String,
pub url: String,
pub revision: String,
pub language_server: Option<LanguageServer>,
pub max_assertions: Option<usize>,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize)]
pub struct LanguageServer {
pub file_extension: String,
pub allow_preexisting_diagnostics: bool,
@@ -79,7 +82,6 @@ impl fmt::Display for FailedAssertion {
impl Error for FailedAssertion {}
pub struct ExampleContext {
meta: ExampleMetadata,
log_prefix: String,
agent_thread: Entity<agent::Thread>,
app: AsyncApp,
@@ -90,16 +92,14 @@ pub struct ExampleContext {
impl ExampleContext {
pub fn new(
meta: ExampleMetadata,
log_prefix: String,
agent_thread: Entity<agent::Thread>,
model: Arc<dyn LanguageModel>,
app: AsyncApp,
) -> Self {
let assertions = AssertionsReport::new(meta.max_assertions);
let assertions = AssertionsReport::default();
Self {
meta,
log_prefix,
agent_thread,
assertions,
@@ -117,60 +117,20 @@ impl ExampleContext {
.unwrap();
}
pub fn assert(&mut self, expected: bool, message: impl ToString) -> Result<()> {
let message = message.to_string();
self.log_assertion(
if expected {
Ok(())
} else {
Err(anyhow::Error::from(FailedAssertion(message.clone())))
},
message,
)
pub fn assertion(&mut self, key: impl Into<String>) -> AssertionGroupId {
let group_id = AssertionGroupId(key.into());
self.assertions.groups.insert(group_id.clone());
group_id
}
pub fn assert_some<T>(&mut self, option: Option<T>, message: impl ToString) -> Result<T> {
let message = message.to_string();
self.log_assertion(
match option {
Some(value) => Ok(value),
None => Err(anyhow::Error::from(FailedAssertion(message.clone()))),
},
message,
)
}
#[allow(dead_code)]
pub fn assert_eq<T: PartialEq + Debug>(
fn log_assertion<T>(
&mut self,
left: T,
right: T,
message: impl ToString,
) -> Result<()> {
let message = message.to_string();
self.log_assertion(
if left == right {
Ok(())
} else {
println!("{}{:#?} != {:#?}", self.log_prefix, left, right);
Err(anyhow::Error::from(FailedAssertion(message.clone())))
},
message,
)
}
fn log_assertion<T>(&mut self, result: Result<T>, message: String) -> Result<T> {
if let Some(max) = self.meta.max_assertions {
if self.assertions.run_count() > max {
return Err(anyhow!(
"More assertions were run than the stated max_assertions of {}",
max
));
}
}
self.assertions.ran.push(RanAssertion {
id: message.clone(),
group_id: AssertionGroupId,
result: Result<T>,
message: String,
) -> Result<T> {
self.assertions.ran.push(Assertion {
group_id,
result: Ok(RanAssertionResult {
analysis: None,
passed: result.is_ok(),
@@ -234,9 +194,9 @@ impl ExampleContext {
let mut tool_metrics = tool_metrics.lock().unwrap();
if let Some(tool_result) = thread.tool_result(&tool_use_id) {
let message = if tool_result.is_error {
format!("TOOL FAILED: {}", tool_use.name)
format!("✖︎ {}", tool_use.name)
} else {
format!("TOOL FINISHED: {}", tool_use.name)
format!("✔︎ {}", tool_use.name)
};
println!("{log_prefix}{message}");
tool_metrics
@@ -278,7 +238,7 @@ impl ExampleContext {
let message_count_before = self.app.update_entity(&self.agent_thread, |thread, cx| {
thread.set_remaining_turns(iterations);
thread.send_to_model(model, cx);
thread.send_to_model(model, None, cx);
thread.messages().len()
})?;
@@ -320,6 +280,103 @@ impl ExampleContext {
Ok(response)
}
pub fn edits(&self) -> HashMap<Arc<Path>, FileEdits> {
self.app
.read_entity(&self.agent_thread, |thread, cx| {
let action_log = thread.action_log().read(cx);
HashMap::from_iter(action_log.changed_buffers(cx).into_iter().map(
|(buffer, diff)| {
let snapshot = buffer.read(cx).snapshot();
let file = snapshot.file().unwrap();
let diff = diff.read(cx);
let base_text = diff.base_text().text();
let hunks = diff
.hunks(&snapshot, cx)
.map(|hunk| FileEditHunk {
base_text: base_text[hunk.diff_base_byte_range.clone()].to_string(),
text: snapshot
.text_for_range(hunk.range.clone())
.collect::<String>(),
status: hunk.status(),
})
.collect();
(file.path().clone(), FileEdits { hunks })
},
))
})
.unwrap()
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Ord, PartialOrd, PartialEq, Eq)]
pub struct AssertionGroupId(pub String);
impl AssertionGroupId {
pub fn assert(
&self,
expected: bool,
message: impl ToString,
cx: &mut ExampleContext,
) -> Result<()> {
let message = message.to_string();
cx.log_assertion(
self.clone(),
if expected {
Ok(())
} else {
Err(anyhow::Error::from(FailedAssertion(message.clone())))
},
message,
)
}
pub fn assert_some<T>(
&self,
option: Option<T>,
message: impl ToString,
cx: &mut ExampleContext,
) -> Result<T> {
let message = message.to_string();
cx.log_assertion(
self.clone(),
match option {
Some(value) => Ok(value),
None => Err(anyhow::Error::from(FailedAssertion(message.clone()))),
},
message,
)
}
#[allow(dead_code)]
pub fn assert_eq<T: PartialEq + Debug>(
&self,
left: T,
right: T,
message: impl ToString,
cx: &mut ExampleContext,
) -> Result<()> {
let message = message.to_string();
cx.log_assertion(
self.clone(),
if left == right {
Ok(())
} else {
println!("{}{:#?} != {:#?}", cx.log_prefix, left, right);
Err(anyhow::Error::from(FailedAssertion(message.clone())))
},
message,
)
}
}
impl fmt::Display for AssertionGroupId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
#[derive(Debug)]
@@ -334,6 +391,7 @@ impl Response {
pub fn expect_tool(
&self,
group_id: AssertionGroupId,
tool_name: &'static str,
cx: &mut ExampleContext,
) -> Result<&ToolUse> {
@@ -342,7 +400,11 @@ impl Response {
.iter()
.find(|tool_use| tool_use.name == tool_name)
});
cx.assert_some(result, format!("called `{}`", tool_name))
group_id.assert_some(result, format!("called `{}`", tool_name), cx)
}
pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUse> {
self.messages.iter().flat_map(|msg| &msg.tool_use)
}
}
@@ -355,17 +417,37 @@ pub struct Message {
#[derive(Debug)]
pub struct ToolUse {
name: String,
pub name: String,
value: serde_json::Value,
}
impl ToolUse {
pub fn expect_input<Input>(&self, cx: &mut ExampleContext) -> Result<Input>
pub fn parse_input<Input>(&self) -> Result<Input>
where
Input: for<'de> serde::Deserialize<'de>,
{
let result =
serde_json::from_value::<Input>(self.value.clone()).map_err(|err| anyhow!(err));
cx.log_assertion(result, format!("valid `{}` input", &self.name))
serde_json::from_value::<Input>(self.value.clone()).map_err(|err| anyhow!(err))
}
}
#[derive(Debug)]
pub struct FileEdits {
hunks: Vec<FileEditHunk>,
}
#[derive(Debug)]
struct FileEditHunk {
base_text: String,
text: String,
status: DiffHunkStatus,
}
impl FileEdits {
pub fn has_added_line(&self, line: &str) -> bool {
self.hunks.iter().any(|hunk| {
hunk.status == DiffHunkStatus::added_none()
&& hunk.base_text.is_empty()
&& hunk.text.contains(line)
})
}
}

View File

@@ -1,251 +0,0 @@
use crate::{example::LanguageServer, examples::ExampleToml};
use collections::BTreeMap;
use futures::future::LocalBoxFuture;
use gpui::SharedString;
use smol::future::FutureExt;
use std::{fs, path::Path, sync::Arc};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExampleId(SharedString);
impl<T: Into<SharedString>> From<T> for ExampleId {
fn from(value: T) -> Self {
Self(value.into())
}
}
#[derive(Clone, Debug)]
pub struct ExampleMetadata {
pub id: ExampleId,
pub url: String,
pub revision: String,
pub language_server: Option<LanguageServer>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AssertionId(SharedString);
impl<T: Into<SharedString>> From<T> for AssertionId {
fn from(value: T) -> Self {
Self(value.into())
}
}
#[derive(Default)]
pub struct ExampleSetup {
assertions: BTreeMap<AssertionId, Vec<Assertion>>,
}
impl ExampleSetup {
fn assertion(&mut self, id: impl Into<SharedString>) -> AssertionId {
todo!()
}
}
struct Assertion {
condition: bool,
message: SharedString,
}
struct ExampleRun {
metadata: ExampleMetadata,
example: Option<Arc<dyn ErasedExample>>,
assertions: BTreeMap<AssertionId, Vec<Assertion>>,
}
impl ExampleRun {
pub fn assert(
&mut self,
assertion_id: &AssertionId,
condition: bool,
message: impl Into<SharedString>,
) {
self.assertions
.get_mut(&assertion_id)
.expect("assertion not found")
.push(Assertion {
condition,
message: message.into(),
});
}
pub fn evaluate(&mut self) {
let example = self.example.take().unwrap();
example.evaluate(self);
self.example = Some(example);
}
}
trait Example: 'static {
fn metadata() -> ExampleMetadata
where
Self: Sized;
fn new(cx: &mut ExampleSetup) -> Self
where
Self: Sized;
async fn evaluate(&self, cx: &mut ExampleRun) -> anyhow::Result<()>;
}
trait ErasedExample {
fn evaluate<'a>(&'a self, cx: &'a mut ExampleRun) -> LocalBoxFuture<'a, anyhow::Result<()>>;
}
struct Erased<T>(T);
impl<T: Example> ErasedExample for Erased<T> {
fn evaluate<'a>(&'a self, cx: &'a mut ExampleRun) -> LocalBoxFuture<'a, anyhow::Result<()>> {
self.0.evaluate(cx).boxed_local()
}
}
struct MyExample {
my_assertion: AssertionId,
}
impl Example for MyExample {
fn metadata() -> ExampleMetadata
where
Self: Sized,
{
ExampleMetadata {
id: "my_example".into(),
url: "".into(),
revision: "".into(),
language_server: None,
}
}
fn new(cx: &mut ExampleSetup) -> Self
where
Self: Sized,
{
Self {
my_assertion: cx.assertion("doesnt_do_stupid_thing"),
}
}
async fn evaluate(&self, cx: &mut ExampleRun) -> anyhow::Result<()> {
cx.assert(&self.my_assertion, true, "true was false");
Ok(())
}
}
struct TomlExample {
diff_assertions: BTreeMap<AssertionId, SharedString>,
thread_assertions: BTreeMap<AssertionId, SharedString>,
}
impl ErasedExample for TomlExample {
fn evaluate<'a>(&'a self, cx: &'a mut ExampleRun) -> LocalBoxFuture<'a, anyhow::Result<()>> {
// let sample = cx.sample().await?;
// for (assertion_id, condition) in self.diff_assertions.iter() {
// let prompt = diff_prompt.render(diff, condition);
// cx.judge(assertion_id, prompt).await?;
// }
// for (assertion_id, condition) in self.diff_assertions.iter() {
// cx.judge(assertion_id, condition).await?;
// }
todo!()
}
}
#[derive(Default)]
struct EvalSuite {
example_builders: BTreeMap<ExampleId, Box<dyn Fn() -> ExampleRun>>,
}
impl EvalSuite {
pub fn examples_to_run(&self, trials: usize) -> BTreeMap<ExampleId, Vec<ExampleRun>> {
let mut examples = BTreeMap::new();
for (example_id, builder) in &self.example_builders {
examples.insert(
example_id.clone(),
(0..trials).map(|_| (builder)()).collect(),
);
}
examples
}
pub fn register<T: Example>(&mut self) {
let metadata = T::metadata();
self.example_builders.insert(
metadata.id.clone(),
Box::new(move || {
let mut cx = ExampleSetup::default();
let example = T::new(&mut cx);
ExampleRun {
metadata: metadata.clone(),
example: Some(Arc::new(Erased(example))),
assertions: cx.assertions,
}
}),
);
}
pub fn register_toml(&mut self, toml_abs_path: &Path) {
let name = toml_abs_path
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
let id = ExampleId(name.into());
let base: ExampleToml =
toml::from_str(&fs::read_to_string(&toml_abs_path).unwrap()).unwrap();
let language_server = if base.require_lsp {
Some(crate::example::LanguageServer {
file_extension: base
.language_extension
.expect("Language extension is required when require_lsp = true"),
allow_preexisting_diagnostics: base.allow_preexisting_diagnostics,
})
} else {
None
};
let metadata = ExampleMetadata {
id: id.clone(),
url: base.url,
revision: base.revision,
language_server,
};
self.example_builders.insert(
id,
Box::new(move || {
let mut cx = ExampleSetup::default();
let mut diff_assertions = BTreeMap::new();
let mut thread_assertions = BTreeMap::new();
for (assertion_key, condition) in base.diff_assertions.iter() {
let assertion_id = cx.assertion(assertion_key);
diff_assertions.insert(assertion_id, condition.into());
}
for (assertion_key, condition) in base.thread_assertions.iter() {
let assertion_id = cx.assertion(assertion_key);
thread_assertions.insert(assertion_id.into(), condition.into());
}
ExampleRun {
metadata: metadata.clone(),
example: Some(Arc::new(TomlExample {
diff_assertions,
thread_assertions,
})),
assertions: cx.assertions,
}
}),
);
}
}
fn main() {
// let mut suite = EvalSuite::default();
// suite.register::<MyExample>();
// for path in list_files(..) {
// suite.register_static()
// }
// for (example_id, runs) in suite.examples_to_run(2) {}
}

View File

@@ -0,0 +1,161 @@
use std::{collections::HashSet, path::Path};
use anyhow::Result;
use assistant_tools::{CreateFileToolInput, EditFileToolInput, ReadFileToolInput};
use async_trait::async_trait;
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion, LanguageServer};
pub struct AddArgToTraitMethod;
#[async_trait(?Send)]
impl Example for AddArgToTraitMethod {
fn meta(&self) -> ExampleMetadata {
ExampleMetadata {
name: "add_arg_to_trait_method".to_string(),
url: "https://github.com/zed-industries/zed.git".to_string(),
revision: "f69aeb6311dde3c0b8979c293d019d66498d54f2".to_string(),
language_server: Some(LanguageServer {
file_extension: "rs".to_string(),
allow_preexisting_diagnostics: false,
}),
}
}
async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
let read_before_edit = cx.assertion("read_before_edit");
let added_any_param = cx.assertion("added_any_param");
let added_unused_param = cx.assertion("added_unused_param");
let added_used_param_to_batch_tool = cx.assertion("added_used_param_to_batch_tool");
const FILENAME: &str = "assistant_tool.rs";
cx.push_user_message(format!(
r#"
Add a `window: Option<gpui::AnyWindowHandle>` argument to the `Tool::run` trait method in {FILENAME},
and update all the implementations of the trait and call sites accordingly.
"#
));
let response = cx.run_to_end().await?;
// Reads files before it edits them
let mut read_files = HashSet::new();
for tool_use in response.tool_uses() {
match tool_use.name.as_str() {
"read_file" => {
if let Ok(input) = tool_use.parse_input::<ReadFileToolInput>() {
read_files.insert(input.path);
}
}
"create_file" => {
if let Ok(input) = tool_use.parse_input::<CreateFileToolInput>() {
read_files.insert(input.path);
}
}
"edit_file" => {
if let Ok(input) = tool_use.parse_input::<EditFileToolInput>() {
read_before_edit
.assert(
read_files.contains(input.path.to_str().unwrap()),
format!(
"Read before edit: {}",
&input.path.file_stem().unwrap().to_str().unwrap()
),
cx,
)
.ok();
}
}
_ => {}
}
}
// Adds ignored argument to all but `batch_tool`
let add_ignored_window_paths = &[
"code_action_tool",
"code_symbols_tool",
"contents_tool",
"copy_path_tool",
"create_directory_tool",
"create_file_tool",
"delete_path_tool",
"diagnostics_tool",
"edit_file_tool",
"fetch_tool",
"grep_tool",
"list_directory_tool",
"move_path_tool",
"now_tool",
"open_tool",
"path_search_tool",
"read_file_tool",
"rename_tool",
"symbol_info_tool",
"terminal_tool",
"thinking_tool",
"web_search_tool",
];
let edits = cx.edits();
for tool_name in add_ignored_window_paths {
let path_str = format!("crates/assistant_tools/src/{}.rs", tool_name);
let edits = edits.get(Path::new(&path_str));
let ignored = edits.map_or(false, |edits| {
edits.has_added_line(" _window: Option<gpui::AnyWindowHandle>,\n")
});
let uningored = edits.map_or(false, |edits| {
edits.has_added_line(" window: Option<gpui::AnyWindowHandle>,\n")
});
added_any_param
.assert(
ignored || uningored,
format!("Argument: {}", tool_name),
cx,
)
.ok();
added_unused_param
.assert(ignored, format!("`_` prefix: {}", tool_name), cx)
.ok();
}
// Adds unignored argument to `batch_tool`
let batch_tool_edits = edits.get(Path::new("crates/assistant_tools/src/batch_tool.rs"));
added_used_param_to_batch_tool
.assert(
batch_tool_edits.map_or(false, |edits| {
edits.has_added_line(" window: Option<gpui::AnyWindowHandle>,\n")
}),
"Argument: batch_tool",
cx,
)
.ok();
Ok(())
}
fn diff_assertions(&self) -> Vec<JudgeAssertion> {
vec![
JudgeAssertion {
group_id: "batch tool passes window to each".to_string(),
description:
"batch_tool is modified to pass a clone of the window to each tool it calls."
.to_string(),
},
JudgeAssertion {
group_id: "tool tests updated".to_string(),
description:
"tool tests are updated to pass the new `window` argument (`None` is ok)."
.to_string(),
},
]
}
}

View File

@@ -15,11 +15,14 @@ impl Example for FileSearchExample {
url: "https://github.com/zed-industries/zed.git".to_string(),
revision: "03ecb88fe30794873f191ddb728f597935b3101c".to_string(),
language_server: None,
max_assertions: Some(4),
}
}
async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
let ends_with_filename = cx.assertion("ends_with_filename");
let correct_glob = cx.assertion("correct_glob");
let used_path_search = cx.assertion("used_path_search");
const FILENAME: &str = "find_replace_file_tool.rs";
cx.push_user_message(format!(
r#"
@@ -32,13 +35,14 @@ impl Example for FileSearchExample {
));
let response = cx.run_turn().await?;
let tool_use = response.expect_tool("path_search", cx)?;
let input = tool_use.expect_input::<PathSearchToolInput>(cx)?;
let tool_use = response.expect_tool(used_path_search, "path_search", cx)?;
let input = tool_use.parse_input::<PathSearchToolInput>()?;
let glob = input.glob;
cx.assert(
ends_with_filename.assert(
glob.ends_with(FILENAME),
format!("glob ends with `{FILENAME}`"),
cx,
)?;
let without_filename = glob.replace(FILENAME, "");
@@ -46,7 +50,7 @@ impl Example for FileSearchExample {
.unwrap()
.is_match(&without_filename);
cx.assert(matches, "glob starts with either `**` or `zed`")?;
correct_glob.assert(matches, "glob starts with either `**` or `zed`", cx)?;
Ok(())
}

View File

@@ -11,10 +11,14 @@ use util::serde::default_true;
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion};
mod add_arg_to_trait_method;
mod file_search;
pub fn all(examples_dir: &Path) -> Vec<Rc<dyn Example>> {
let mut threads: Vec<Rc<dyn Example>> = vec![Rc::new(file_search::FileSearchExample)];
let mut threads: Vec<Rc<dyn Example>> = vec![
Rc::new(file_search::FileSearchExample),
Rc::new(add_arg_to_trait_method::AddArgToTraitMethod),
];
for example_path in list_declarative_examples(examples_dir).unwrap() {
threads.push(Rc::new(DeclarativeExample::load(&example_path).unwrap()));
@@ -33,14 +37,14 @@ struct DeclarativeExample {
impl DeclarativeExample {
pub fn load(example_path: &Path) -> Result<Self> {
let name = Self::name_from_path(example_path);
let base: ExampleToml = toml::from_str(&fs::read_to_string(&example_path)?)?;
let toml: ExampleToml = toml::from_str(&fs::read_to_string(&example_path)?)?;
let language_server = if base.require_lsp {
let language_server = if toml.require_lsp {
Some(crate::example::LanguageServer {
file_extension: base
file_extension: toml
.language_extension
.expect("Language extension is required when require_lsp = true"),
allow_preexisting_diagnostics: base.allow_preexisting_diagnostics,
allow_preexisting_diagnostics: toml.allow_preexisting_diagnostics,
})
} else {
None
@@ -48,24 +52,29 @@ impl DeclarativeExample {
let metadata = ExampleMetadata {
name,
url: base.url,
revision: base.revision,
url: toml.url,
revision: toml.revision,
language_server,
max_assertions: None,
};
Ok(DeclarativeExample {
metadata,
prompt: base.prompt,
thread_assertions: base
prompt: toml.prompt,
thread_assertions: toml
.thread_assertions
.into_iter()
.map(|(id, description)| JudgeAssertion { id, description })
.map(|(id, description)| JudgeAssertion {
group_id: id,
description,
})
.collect(),
diff_assertions: base
diff_assertions: toml
.diff_assertions
.into_iter()
.map(|(id, description)| JudgeAssertion { id, description })
.map(|(id, description)| JudgeAssertion {
group_id: id,
description,
})
.collect(),
})
}

View File

@@ -29,15 +29,15 @@ use util::ResultExt as _;
use util::command::new_smol_command;
use util::markdown::MarkdownString;
use crate::assertions::{AssertionsReport, RanAssertion, RanAssertionResult};
use crate::example::{Example, ExampleContext, FailedAssertion, JudgeAssertion};
use crate::assertions::{Assertion, AssertionsReport, RanAssertionResult};
use crate::example::{AssertionGroupId, Example, ExampleContext, FailedAssertion, JudgeAssertion};
use crate::{AgentAppState, ToolMetrics};
pub const ZED_REPO_URL: &str = "https://github.com/zed-industries/zed.git";
#[derive(Clone)]
pub struct ExampleInstance {
pub thread: Rc<dyn Example>,
pub example: Rc<dyn Example>,
pub name: String,
pub run_directory: PathBuf,
pub log_prefix: String,
@@ -100,7 +100,7 @@ impl ExampleInstance {
Self {
name,
thread,
example: thread,
log_prefix: String::new(),
run_directory,
repetition,
@@ -110,11 +110,7 @@ impl ExampleInstance {
}
pub fn repo_url(&self) -> String {
self.thread.meta().url
}
pub fn revision(&self) -> String {
self.thread.meta().revision
self.example.meta().url
}
pub fn worktree_name(&self) -> String {
@@ -132,7 +128,7 @@ impl ExampleInstance {
/// Set up the example by checking out the specified Git revision
pub async fn fetch(&mut self) -> Result<()> {
let meta = self.thread.meta();
let meta = self.example.meta();
let revision_exists = run_git(
&self.repo_path,
@@ -155,7 +151,7 @@ impl ExampleInstance {
/// Set up the example by checking out the specified Git revision
pub async fn setup(&mut self) -> Result<()> {
let worktree_path = self.worktree_path();
let meta = self.thread.meta();
let meta = self.example.meta();
if worktree_path.is_dir() {
println!("{}Resetting existing worktree", self.log_prefix);
@@ -194,7 +190,7 @@ impl ExampleInstance {
pub fn worktree_path(&self) -> PathBuf {
self.worktrees_dir
.join(self.worktree_name())
.join(self.thread.meta().repo_name())
.join(self.example.meta().repo_name())
}
pub fn run(
@@ -220,7 +216,7 @@ impl ExampleInstance {
let tools = cx.new(|_| ToolWorkingSet::default());
let thread_store =
ThreadStore::load(project.clone(), tools, app_state.prompt_builder.clone(), cx);
let meta = self.thread.meta();
let meta = self.example.meta();
let this = self.clone();
cx.spawn(async move |cx| {
@@ -353,8 +349,8 @@ impl ExampleInstance {
});
})?;
let mut example_cx = ExampleContext::new(meta.clone(), this.log_prefix.clone(), thread.clone(), model.clone(), cx.clone());
let result = this.thread.conversation(&mut example_cx).await;
let mut example_cx = ExampleContext::new(this.log_prefix.clone(), thread.clone(), model.clone(), cx.clone());
let result = this.example.conversation(&mut example_cx).await;
if let Err(err) = result {
if !err.is::<FailedAssertion>() {
@@ -428,7 +424,7 @@ impl ExampleInstance {
let worktree_path = self.worktree_path();
run_git(&worktree_path, &["add", "."]).await?;
let mut diff_args = vec!["diff", "--staged"];
if self.thread.meta().url == ZED_REPO_URL {
if self.example.meta().url == ZED_REPO_URL {
diff_args.push(":(exclude).rules");
}
run_git(&worktree_path, &diff_args).await
@@ -469,7 +465,7 @@ impl ExampleInstance {
run_output: &RunOutput,
cx: &AsyncApp,
) -> (String, AssertionsReport) {
let diff_assertions = self.thread.diff_assertions();
let diff_assertions = self.example.diff_assertions();
if diff_assertions.is_empty() {
return (
@@ -480,8 +476,8 @@ impl ExampleInstance {
println!("{}Running diff judge", self.log_prefix);
let judge_diff_prompt = include_str!("templates/judge_diff.hbs");
let judge_diff_prompt_name = "judge_diff";
let judge_diff_prompt = include_str!("judge_diff_prompt.hbs");
let judge_diff_prompt_name = "judge_diff_prompt";
let mut hbs = Handlebars::new();
hbs.register_template_string(judge_diff_prompt_name, judge_diff_prompt)
.unwrap();
@@ -516,7 +512,7 @@ impl ExampleInstance {
run_output: &RunOutput,
cx: &AsyncApp,
) -> (String, AssertionsReport) {
let thread_assertions = self.thread.thread_assertions();
let thread_assertions = self.example.thread_assertions();
if thread_assertions.is_empty() {
return (
@@ -525,7 +521,7 @@ impl ExampleInstance {
);
}
let judge_thread_prompt = include_str!("templates/judge_thread.hbs");
let judge_thread_prompt = include_str!("judge_thread_prompt.hbs");
let judge_diff_prompt_name = "judge_thread_prompt";
let mut hbs = Handlebars::new();
hbs.register_template_string(judge_diff_prompt_name, judge_thread_prompt)
@@ -591,15 +587,15 @@ impl ExampleInstance {
};
if result.is_ok() {
println!("{}{}", log_prefix, assertion.id);
println!("{}{}", log_prefix, assertion.group_id);
} else {
println!("{}{}", log_prefix, assertion.id);
println!("{}{}", log_prefix, assertion.group_id);
}
(
response,
RanAssertion {
id: assertion.id,
Assertion {
group_id: AssertionGroupId(assertion.group_id),
result,
},
)
@@ -610,7 +606,7 @@ impl ExampleInstance {
let mut report = AssertionsReport::default();
for (response, assertion) in future::join_all(assertions).await {
writeln!(&mut responses, "# {}", assertion.id).unwrap();
writeln!(&mut responses, "# {}", assertion.group_id).unwrap();
writeln!(&mut responses, "{}\n\n", response).unwrap();
report.ran.push(assertion);
}

View File

@@ -1,7 +1,12 @@
You are an expert software developer. Your task is to evaluate a diff produced by an AI agent. Here is the prompt and the diff:
You are an expert software developer. Your task is to evaluate a diff produced by an AI agent
in response to a prompt. Here is the prompt and the diff:
<prompt>
{{{prompt}}}
</prompt>
<diff>
{{repository_diff}}
{{{repository_diff}}}
</diff>
Evaluate whether or not the diff passes the following assertion:

View File

@@ -1,43 +0,0 @@
use anyhow::Result;
use handlebars::Handlebars;
use rust_embed::RustEmbed;
use serde::Serialize;
use std::sync::Arc;
#[derive(RustEmbed)]
#[folder = "src/templates"]
#[include = "*.hbs"]
struct Assets;
pub struct Templates(Handlebars<'static>);
impl Templates {
pub fn new() -> Arc<Self> {
let mut handlebars = Handlebars::new();
handlebars.register_embed_templates::<Assets>().unwrap();
// Disable HTML escaping so you don't .
handlebars.register_escape_fn(|text| text.into());
Arc::new(Self(handlebars))
}
}
pub trait Template: Sized {
const TEMPLATE_NAME: &'static str;
fn render(&self, templates: &Templates) -> Result<String>
where
Self: Serialize + Sized,
{
Ok(templates.0.render(Self::TEMPLATE_NAME, self)?)
}
}
#[derive(Serialize)]
pub struct JudgeDiffTemplate {
repository_diff: String,
assertion: String,
}
impl Template for JudgeDiffTemplate {
const TEMPLATE_NAME: &'static str = "judge_diff.hbs";
}

View File

@@ -84,11 +84,6 @@ impl FeatureFlag for ZedPro {
const NAME: &'static str = "zed-pro";
}
pub struct ZedProWebSearchTool {}
impl FeatureFlag for ZedProWebSearchTool {
const NAME: &'static str = "zed-pro-web-search-tool";
}
pub struct NotebookFeatureFlag;
impl FeatureFlag for NotebookFeatureFlag {

View File

@@ -34,6 +34,7 @@ pub struct FakeGitRepositoryState {
pub blames: HashMap<RepoPath, Blame>,
pub current_branch_name: Option<String>,
pub branches: HashSet<String>,
pub merge_head_shas: Vec<String>,
pub simulated_index_write_error_message: Option<String>,
}
@@ -47,12 +48,20 @@ impl FakeGitRepositoryState {
blames: Default::default(),
current_branch_name: Default::default(),
branches: Default::default(),
merge_head_shas: Default::default(),
simulated_index_write_error_message: Default::default(),
}
}
}
impl FakeGitRepository {
fn with_state<F, T>(&self, write: bool, f: F) -> Result<T>
where
F: FnOnce(&mut FakeGitRepositoryState) -> T,
{
self.fs.with_git_state(&self.dot_git_path, write, f)
}
fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
where
F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
@@ -137,11 +146,18 @@ impl GitRepository for FakeGitRepository {
}
fn merge_head_shas(&self) -> Vec<String> {
vec![]
self.with_state(false, |state| state.merge_head_shas.clone())
.unwrap()
}
fn show(&self, _commit: String) -> BoxFuture<Result<CommitDetails>> {
unimplemented!()
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
async {
Ok(CommitDetails {
sha: commit.into(),
..Default::default()
})
}
.boxed()
}
fn reset(

View File

@@ -133,7 +133,7 @@ pub struct CommitSummary {
pub has_parent: bool,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
pub struct CommitDetails {
pub sha: SharedString,
pub message: SharedString,

View File

@@ -0,0 +1,473 @@
use collections::{HashMap, HashSet};
use editor::{
ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker,
Editor, EditorEvent, ExcerptId, MultiBuffer, RowHighlightOptions,
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
};
use gpui::{
App, Context, Entity, InteractiveElement as _, ParentElement as _, Subscription, WeakEntity,
};
use language::{Anchor, Buffer, BufferId};
use project::{ConflictRegion, ConflictSet, ConflictSetUpdate};
use std::{ops::Range, sync::Arc};
use ui::{
ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
StyledTypography as _, div, h_flex, rems,
};
pub(crate) struct ConflictAddon {
buffers: HashMap<BufferId, BufferConflicts>,
}
impl ConflictAddon {
pub(crate) fn conflict_set(&self, buffer_id: BufferId) -> Option<Entity<ConflictSet>> {
self.buffers
.get(&buffer_id)
.map(|entry| entry.conflict_set.clone())
}
}
struct BufferConflicts {
block_ids: Vec<(Range<Anchor>, CustomBlockId)>,
conflict_set: Entity<ConflictSet>,
_subscription: Subscription,
}
impl editor::Addon for ConflictAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) {
// Only show conflict UI for singletons and in the project diff.
if !editor.buffer().read(cx).is_singleton()
&& !editor.buffer().read(cx).all_diff_hunks_expanded()
{
return;
}
editor.register_addon(ConflictAddon {
buffers: Default::default(),
});
let buffers = buffer.read(cx).all_buffers().clone();
for buffer in buffers {
buffer_added(editor, buffer, cx);
}
cx.subscribe(&cx.entity(), |editor, _, event, cx| match event {
EditorEvent::ExcerptsAdded { buffer, .. } => buffer_added(editor, buffer.clone(), cx),
EditorEvent::ExcerptsExpanded { ids } => {
let multibuffer = editor.buffer().read(cx).snapshot(cx);
for excerpt_id in ids {
let Some(buffer) = multibuffer.buffer_for_excerpt(*excerpt_id) else {
continue;
};
let addon = editor.addon::<ConflictAddon>().unwrap();
let Some(conflict_set) = addon.conflict_set(buffer.remote_id()).clone() else {
return;
};
excerpt_for_buffer_updated(editor, conflict_set, cx);
}
}
EditorEvent::ExcerptsRemoved {
removed_buffer_ids, ..
} => buffers_removed(editor, removed_buffer_ids, cx),
_ => {}
})
.detach();
}
fn excerpt_for_buffer_updated(
editor: &mut Editor,
conflict_set: Entity<ConflictSet>,
cx: &mut Context<Editor>,
) {
let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
conflicts_updated(
editor,
conflict_set,
&ConflictSetUpdate {
buffer_range: None,
old_range: 0..conflicts_len,
new_range: 0..conflicts_len,
},
cx,
);
}
fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
let Some(project) = &editor.project else {
return;
};
let git_store = project.read(cx).git_store().clone();
let buffer_conflicts = editor
.addon_mut::<ConflictAddon>()
.unwrap()
.buffers
.entry(buffer.read(cx).remote_id())
.or_insert_with(|| {
let conflict_set = git_store.update(cx, |git_store, cx| {
git_store.open_conflict_set(buffer.clone(), cx)
});
let subscription = cx.subscribe(&conflict_set, conflicts_updated);
BufferConflicts {
block_ids: Vec::new(),
conflict_set: conflict_set.clone(),
_subscription: subscription,
}
});
let conflict_set = buffer_conflicts.conflict_set.clone();
let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
let addon_conflicts_len = buffer_conflicts.block_ids.len();
conflicts_updated(
editor,
conflict_set,
&ConflictSetUpdate {
buffer_range: None,
old_range: 0..addon_conflicts_len,
new_range: 0..conflicts_len,
},
cx,
);
}
fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
let mut removed_block_ids = HashSet::default();
editor
.addon_mut::<ConflictAddon>()
.unwrap()
.buffers
.retain(|buffer_id, buffer| {
if removed_buffer_ids.contains(&buffer_id) {
removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
false
} else {
true
}
});
editor.remove_blocks(removed_block_ids, None, cx);
}
fn conflicts_updated(
editor: &mut Editor,
conflict_set: Entity<ConflictSet>,
event: &ConflictSetUpdate,
cx: &mut Context<Editor>,
) {
let buffer_id = conflict_set.read(cx).snapshot.buffer_id;
let conflict_set = conflict_set.read(cx).snapshot();
let multibuffer = editor.buffer().read(cx);
let snapshot = multibuffer.snapshot(cx);
let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
let Some(buffer_snapshot) = excerpts
.first()
.and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
else {
return;
};
// Remove obsolete highlights and blocks
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
let old_conflicts = buffer_conflicts.block_ids[event.old_range.clone()].to_owned();
let mut removed_highlighted_ranges = Vec::new();
let mut removed_block_ids = HashSet::default();
for (conflict_range, block_id) in old_conflicts {
let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
let precedes_start = range
.context
.start
.cmp(&conflict_range.start, &buffer_snapshot)
.is_le();
let follows_end = range
.context
.end
.cmp(&conflict_range.start, &buffer_snapshot)
.is_ge();
precedes_start && follows_end
}) else {
continue;
};
let excerpt_id = *excerpt_id;
let Some(range) = snapshot
.anchor_in_excerpt(excerpt_id, conflict_range.start)
.zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
.map(|(start, end)| start..end)
else {
continue;
};
removed_highlighted_ranges.push(range.clone());
removed_block_ids.insert(block_id);
}
editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
editor
.remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx);
editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx);
editor.remove_highlighted_rows::<ConflictsTheirsMarker>(
removed_highlighted_ranges.clone(),
cx,
);
editor.remove_blocks(removed_block_ids, None, cx);
}
// Add new highlights and blocks
let editor_handle = cx.weak_entity();
let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
let mut blocks = Vec::new();
for conflict in new_conflicts {
let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
let precedes_start = range
.context
.start
.cmp(&conflict.range.start, &buffer_snapshot)
.is_le();
let follows_end = range
.context
.end
.cmp(&conflict.range.start, &buffer_snapshot)
.is_ge();
precedes_start && follows_end
}) else {
continue;
};
let excerpt_id = *excerpt_id;
update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
continue;
};
let editor_handle = editor_handle.clone();
blocks.push(BlockProperties {
placement: BlockPlacement::Above(anchor),
height: Some(1),
style: BlockStyle::Fixed,
render: Arc::new({
let conflict = conflict.clone();
move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
}),
priority: 0,
})
}
let new_block_ids = editor.insert_blocks(blocks, None, cx);
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
buffer_conflicts.block_ids.splice(
event.old_range.clone(),
new_conflicts
.iter()
.map(|conflict| conflict.range.clone())
.zip(new_block_ids),
);
}
}
fn update_conflict_highlighting(
editor: &mut Editor,
conflict: &ConflictRegion,
buffer: &editor::MultiBufferSnapshot,
excerpt_id: editor::ExcerptId,
cx: &mut Context<Editor>,
) {
log::debug!("update conflict highlighting for {conflict:?}");
let theme = cx.theme().clone();
let colors = theme.colors();
let outer_start = buffer
.anchor_in_excerpt(excerpt_id, conflict.range.start)
.unwrap();
let outer_end = buffer
.anchor_in_excerpt(excerpt_id, conflict.range.end)
.unwrap();
let our_start = buffer
.anchor_in_excerpt(excerpt_id, conflict.ours.start)
.unwrap();
let our_end = buffer
.anchor_in_excerpt(excerpt_id, conflict.ours.end)
.unwrap();
let their_start = buffer
.anchor_in_excerpt(excerpt_id, conflict.theirs.start)
.unwrap();
let their_end = buffer
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
.unwrap();
let ours_background = colors.version_control_conflict_ours_background;
let ours_marker = colors.version_control_conflict_ours_marker_background;
let theirs_background = colors.version_control_conflict_theirs_background;
let theirs_marker = colors.version_control_conflict_theirs_marker_background;
let divider_background = colors.version_control_conflict_divider_background;
let options = RowHighlightOptions {
include_gutter: false,
..Default::default()
};
// Prevent diff hunk highlighting within the entire conflict region.
editor.highlight_rows::<ConflictsOuter>(
outer_start..outer_end,
divider_background,
options,
cx,
);
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
editor.highlight_rows::<ConflictsTheirs>(
their_start..their_end,
theirs_background,
options,
cx,
);
editor.highlight_rows::<ConflictsTheirsMarker>(
their_end..outer_end,
theirs_marker,
options,
cx,
);
}
fn render_conflict_buttons(
conflict: &ConflictRegion,
excerpt_id: ExcerptId,
editor: WeakEntity<Editor>,
cx: &mut BlockContext,
) -> AnyElement {
h_flex()
.h(cx.line_height)
.items_end()
.ml(cx.gutter_dimensions.width)
.id(cx.block_id)
.gap_0p5()
.child(
div()
.id("ours")
.px_1()
.child("Take Ours")
.rounded_t(rems(0.2))
.text_ui_sm(cx)
.hover(|this| this.bg(cx.theme().colors().element_background))
.cursor_pointer()
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
let ours = conflict.ours.clone();
move |_, _, cx| {
resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx)
}
}),
)
.child(
div()
.id("theirs")
.px_1()
.child("Take Theirs")
.rounded_t(rems(0.2))
.text_ui_sm(cx)
.hover(|this| this.bg(cx.theme().colors().element_background))
.cursor_pointer()
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
let theirs = conflict.theirs.clone();
move |_, _, cx| {
resolve_conflict(
editor.clone(),
excerpt_id,
&conflict,
&[theirs.clone()],
cx,
)
}
}),
)
.child(
div()
.id("both")
.px_1()
.child("Take Both")
.rounded_t(rems(0.2))
.text_ui_sm(cx)
.hover(|this| this.bg(cx.theme().colors().element_background))
.cursor_pointer()
.on_click({
let editor = editor.clone();
let conflict = conflict.clone();
let ours = conflict.ours.clone();
let theirs = conflict.theirs.clone();
move |_, _, cx| {
resolve_conflict(
editor.clone(),
excerpt_id,
&conflict,
&[ours.clone(), theirs.clone()],
cx,
)
}
}),
)
.into_any()
}
fn resolve_conflict(
editor: WeakEntity<Editor>,
excerpt_id: ExcerptId,
resolved_conflict: &ConflictRegion,
ranges: &[Range<Anchor>],
cx: &mut App,
) {
let Some(editor) = editor.upgrade() else {
return;
};
let multibuffer = editor.read(cx).buffer().read(cx);
let snapshot = multibuffer.snapshot(cx);
let Some(buffer) = resolved_conflict
.ours
.end
.buffer_id
.and_then(|buffer_id| multibuffer.buffer(buffer_id))
else {
return;
};
let buffer_snapshot = buffer.read(cx).snapshot();
resolved_conflict.resolve(buffer, ranges, cx);
editor.update(cx, |editor, cx| {
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
return;
};
let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
range
.start
.cmp(&resolved_conflict.range.start, &buffer_snapshot)
}) else {
return;
};
let &(_, block_id) = &state.block_ids[ix];
let start = snapshot
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
.unwrap();
let end = snapshot
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
.unwrap();
editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
})
}

View File

@@ -447,7 +447,7 @@ impl GitPanel {
.ok();
}
GitStoreEvent::RepositoryUpdated(_, _, _) => {}
GitStoreEvent::JobsUpdated => {}
GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
},
)
.detach();
@@ -1650,7 +1650,7 @@ impl GitPanel {
if let Some(merge_message) = self
.active_repository
.as_ref()
.and_then(|repo| repo.read(cx).merge_message.as_ref())
.and_then(|repo| repo.read(cx).merge.message.as_ref())
{
return Some(merge_message.to_string());
}

View File

@@ -3,6 +3,7 @@ use std::any::Any;
use ::settings::Settings;
use command_palette_hooks::CommandPaletteFilter;
use commit_modal::CommitModal;
use editor::Editor;
mod blame_ui;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
@@ -20,6 +21,7 @@ pub mod branch_picker;
mod commit_modal;
pub mod commit_tooltip;
mod commit_view;
mod conflict_view;
pub mod git_panel;
mod git_panel_settings;
pub mod onboarding;
@@ -35,6 +37,11 @@ pub fn init(cx: &mut App) {
editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
cx.observe_new(|editor: &mut Editor, _, cx| {
conflict_view::register_editor(editor, editor.buffer().clone(), cx);
})
.detach();
cx.observe_new(|workspace: &mut Workspace, _, cx| {
ProjectDiff::register(workspace, cx);
CommitModal::register(workspace);

View File

@@ -1,4 +1,5 @@
use crate::{
conflict_view::ConflictAddon,
git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
remote_button::{render_publish_button, render_push_button},
};
@@ -26,7 +27,10 @@ use project::{
Project, ProjectPath,
git_store::{GitStore, GitStoreEvent, RepositoryEvent},
};
use std::any::{Any, TypeId};
use std::{
any::{Any, TypeId},
ops::Range,
};
use theme::ActiveTheme;
use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt as _;
@@ -48,7 +52,6 @@ pub struct ProjectDiff {
focus_handle: FocusHandle,
update_needed: postage::watch::Sender<()>,
pending_scroll: Option<PathKey>,
current_branch: Option<Branch>,
_task: Task<Result<()>>,
_subscription: Subscription,
}
@@ -61,9 +64,9 @@ struct DiffBuffer {
file_status: FileStatus,
}
const CONFLICT_NAMESPACE: u32 = 0;
const TRACKED_NAMESPACE: u32 = 1;
const NEW_NAMESPACE: u32 = 2;
const CONFLICT_NAMESPACE: u32 = 1;
const TRACKED_NAMESPACE: u32 = 2;
const NEW_NAMESPACE: u32 = 3;
impl ProjectDiff {
pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
@@ -154,7 +157,8 @@ impl ProjectDiff {
window,
move |this, _git_store, event, _window, _cx| match event {
GitStoreEvent::ActiveRepositoryChanged(_)
| GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, true) => {
| GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, true)
| GitStoreEvent::ConflictsUpdated => {
*this.update_needed.borrow_mut() = ();
}
_ => {}
@@ -178,7 +182,6 @@ impl ProjectDiff {
multibuffer,
pending_scroll: None,
update_needed: send,
current_branch: None,
_task: worker,
_subscription: git_store_subscription,
}
@@ -395,11 +398,25 @@ impl ProjectDiff {
let buffer = diff_buffer.buffer;
let diff = diff_buffer.diff;
let conflict_addon = self
.editor
.read(cx)
.addon::<ConflictAddon>()
.expect("project diff editor should have a conflict addon");
let snapshot = buffer.read(cx).snapshot();
let diff = diff.read(cx);
let diff_hunk_ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
.map(|diff_hunk| diff_hunk.buffer_range.clone());
let conflicts = conflict_addon
.conflict_set(snapshot.remote_id())
.map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone())
.unwrap_or_default();
let conflicts = conflicts.iter().map(|conflict| conflict.range.clone());
let excerpt_ranges = merge_anchor_ranges(diff_hunk_ranges, conflicts, &snapshot)
.map(|range| range.to_point(&snapshot))
.collect::<Vec<_>>();
let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| {
@@ -407,7 +424,7 @@ impl ProjectDiff {
let (_, is_newly_added) = multibuffer.set_excerpts_for_path(
path_key.clone(),
buffer,
diff_hunk_ranges,
excerpt_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
@@ -450,18 +467,6 @@ impl ProjectDiff {
cx: &mut AsyncWindowContext,
) -> Result<()> {
while let Some(_) = recv.next().await {
this.update(cx, |this, cx| {
let new_branch = this
.git_store
.read(cx)
.active_repository()
.and_then(|active_repository| active_repository.read(cx).branch.clone());
if new_branch != this.current_branch {
this.current_branch = new_branch;
cx.notify();
}
})?;
let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?;
for buffer_to_load in buffers_to_load {
if let Some(buffer) = buffer_to_load.await.log_err() {
@@ -1127,47 +1132,6 @@ impl RenderOnce for ProjectDiffEmptyState {
}
}
// .when(self.can_push_and_pull, |this| {
// let remote_button = crate::render_remote_button(
// "project-diff-remote-button",
// &branch,
// self.focus_handle.clone(),
// false,
// );
// match remote_button {
// Some(button) => {
// this.child(h_flex().justify_around().child(button))
// }
// None => this.child(
// h_flex()
// .justify_around()
// .child(Label::new("Remote up to date")),
// ),
// }
// }),
//
// // .map(|this| {
// this.child(h_flex().justify_around().mt_1().child(
// Button::new("project-diff-close-button", "Close").when_some(
// self.focus_handle.clone(),
// |this, focus_handle| {
// this.key_binding(KeyBinding::for_action_in(
// &CloseActiveItem::default(),
// &focus_handle,
// window,
// cx,
// ))
// .on_click(move |_, window, cx| {
// window.focus(&focus_handle);
// window
// .dispatch_action(Box::new(CloseActiveItem::default()), cx);
// })
// },
// ),
// ))
// }),
mod preview {
use git::repository::{
Branch, CommitSummary, Upstream, UpstreamTracking, UpstreamTrackingStatus,
@@ -1293,6 +1257,53 @@ mod preview {
}
}
fn merge_anchor_ranges<'a>(
left: impl 'a + Iterator<Item = Range<Anchor>>,
right: impl 'a + Iterator<Item = Range<Anchor>>,
snapshot: &'a language::BufferSnapshot,
) -> impl 'a + Iterator<Item = Range<Anchor>> {
let mut left = left.fuse().peekable();
let mut right = right.fuse().peekable();
std::iter::from_fn(move || {
let Some(left_range) = left.peek() else {
return right.next();
};
let Some(right_range) = right.peek() else {
return left.next();
};
let mut next_range = if left_range.start.cmp(&right_range.start, snapshot).is_lt() {
left.next().unwrap()
} else {
right.next().unwrap()
};
// Extend the basic range while there's overlap with a range from either stream.
loop {
if let Some(left_range) = left
.peek()
.filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le())
.cloned()
{
left.next();
next_range.end = left_range.end;
} else if let Some(right_range) = right
.peek()
.filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le())
.cloned()
{
right.next();
next_range.end = right_range.end;
} else {
break;
}
}
Some(next_range)
})
}
#[cfg(not(target_os = "windows"))]
#[cfg(test)]
mod tests {

View File

@@ -2,7 +2,8 @@ pub mod cursor_position;
use cursor_position::{LineIndicatorFormat, UserCaretPosition};
use editor::{
Anchor, Editor, MultiBufferSnapshot, ToOffset, ToPoint, actions::Tab, scroll::Autoscroll,
Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, ToOffset, ToPoint, actions::Tab,
scroll::Autoscroll,
};
use gpui::{
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Styled,
@@ -180,7 +181,10 @@ impl GoToLine {
editor.highlight_rows::<GoToLineRowHighlights>(
start..end,
cx.theme().colors().editor_highlighted_line_background,
true,
RowHighlightOptions {
autoscroll: true,
..Default::default()
},
cx,
);
editor.request_autoscroll(Autoscroll::center(), cx);

View File

@@ -15,7 +15,7 @@ use std::{
num::NonZeroU64,
sync::{
Arc, Weak,
atomic::{AtomicUsize, Ordering::SeqCst},
atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
},
thread::panicking,
};
@@ -572,6 +572,30 @@ impl AnyWeakEntity {
)
}
}
/// Creates a weak entity that can never be upgraded.
pub fn new_invalid() -> Self {
/// To hold the invariant that all ids are unique, and considering that slotmap
/// increases their IDs from `0`, we can decrease ours from `u64::MAX` so these
/// two will never conflict (u64 is way too large).
static UNIQUE_NON_CONFLICTING_ID_GENERATOR: AtomicU64 = AtomicU64::new(u64::MAX);
let entity_id = UNIQUE_NON_CONFLICTING_ID_GENERATOR.fetch_sub(1, SeqCst);
Self {
// Safety:
// Docs say this is safe but can be unspecified if slotmap changes the representation
// after `1.0.7`, that said, providing a valid entity_id here is not necessary as long
// as we guarantee that that `entity_id` is never used if `entity_ref_counts` equals
// to `Weak::new()` (that is, it's unable to upgrade), that is the invariant that
// actually needs to be hold true.
//
// And there is no sane reason to read an entity slot if `entity_ref_counts` can't be
// read in the first place, so we're good!
entity_id: entity_id.into(),
entity_type: TypeId::of::<()>(),
entity_ref_counts: Weak::new(),
}
}
}
impl std::fmt::Debug for AnyWeakEntity {
@@ -707,6 +731,14 @@ impl<T: 'static> WeakEntity<T> {
.map(|this| cx.read_entity(&this, read)),
)
}
/// Create a new weak entity that can never be upgraded.
pub fn new_invalid() -> Self {
Self {
any_entity: AnyWeakEntity::new_invalid(),
entity_type: PhantomData,
}
}
}
impl<T> Hash for WeakEntity<T> {

View File

@@ -42,6 +42,10 @@ impl ConfiguredModel {
pub fn is_same_as(&self, other: &ConfiguredModel) -> bool {
self.model.id() == other.model.id() && self.provider.id() == other.provider.id()
}
pub fn is_provided_by_zed(&self) -> bool {
self.provider.id().0 == crate::ZED_CLOUD_PROVIDER_ID
}
}
pub enum Event {

View File

@@ -714,39 +714,32 @@ pub fn map_to_language_model_completion_events(
if let Some(tool_use) = state.tool_uses_by_index.get_mut(&index) {
tool_use.input_json.push_str(&partial_json);
return Some((
vec![maybe!({
Ok(LanguageModelCompletionEvent::ToolUse(
// Try to convert invalid (incomplete) JSON into
// valid JSON that serde can accept, e.g. by closing
// unclosed delimiters. This way, we can update the
// UI with whatever has been streamed back so far.
if let Ok(input) = serde_json::Value::from_str(
&partial_json_fixer::fix_json(&tool_use.input_json),
) {
return Some((
vec![Ok(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: tool_use.id.clone().into(),
name: tool_use.name.clone().into(),
is_input_complete: false,
input: if tool_use.input_json.is_empty() {
serde_json::Value::Object(
serde_json::Map::default(),
)
} else {
serde_json::Value::from_str(
// Convert invalid (incomplete) JSON into
// JSON that serde will accept, e.g. by closing
// unclosed delimiters. This way, we can update
// the UI with whatever has been streamed back so far.
&partial_json_fixer::fix_json(
&tool_use.input_json,
),
)
.map_err(|err| anyhow!(err))?
},
input,
},
))
})],
state,
));
))],
state,
));
}
}
}
},
Event::ContentBlockStop { index } => {
if let Some(tool_use) = state.tool_uses_by_index.remove(&index) {
let input_json = tool_use.input_json.trim();
return Some((
vec![maybe!({
Ok(LanguageModelCompletionEvent::ToolUse(
@@ -754,15 +747,15 @@ pub fn map_to_language_model_completion_events(
id: tool_use.id.into(),
name: tool_use.name.into(),
is_input_complete: true,
input: if tool_use.input_json.is_empty() {
input: if input_json.is_empty() {
serde_json::Value::Object(
serde_json::Map::default(),
)
} else {
serde_json::Value::from_str(
&tool_use.input_json,
input_json
)
.map_err(|err| anyhow!(err))?
.map_err(|err| anyhow!("Error parsing tool call input JSON: {err:?} - JSON string was: {input_json:?}"))?
},
},
))

View File

@@ -35,7 +35,7 @@ use strum::IntoEnumIterator;
use thiserror::Error;
use ui::{TintColor, prelude::*};
use zed_llm_client::{
CURRENT_PLAN_HEADER_NAME, CompletionBody, EXPIRED_LLM_TOKEN_HEADER_NAME,
CURRENT_PLAN_HEADER_NAME, CompletionBody, CompletionMode, EXPIRED_LLM_TOKEN_HEADER_NAME,
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME, MODEL_REQUESTS_RESOURCE_HEADER_VALUE,
SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
};
@@ -748,6 +748,7 @@ impl LanguageModel for CloudLanguageModel {
CompletionBody {
thread_id,
prompt_id,
mode: Some(CompletionMode::Max),
provider: zed_llm_client::LanguageModelProvider::Anthropic,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
@@ -794,6 +795,7 @@ impl LanguageModel for CloudLanguageModel {
CompletionBody {
thread_id,
prompt_id,
mode: Some(CompletionMode::Max),
provider: zed_llm_client::LanguageModelProvider::OpenAi,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,
@@ -824,6 +826,7 @@ impl LanguageModel for CloudLanguageModel {
CompletionBody {
thread_id,
prompt_id,
mode: Some(CompletionMode::Max),
provider: zed_llm_client::LanguageModelProvider::Google,
model: request.model.clone(),
provider_request: serde_json::to_value(&request)?,

View File

@@ -95,6 +95,7 @@ pub enum Event {
},
ExcerptsRemoved {
ids: Vec<ExcerptId>,
removed_buffer_ids: Vec<BufferId>,
},
ExcerptsExpanded {
ids: Vec<ExcerptId>,
@@ -2021,7 +2022,12 @@ impl MultiBuffer {
pub fn clear(&mut self, cx: &mut Context<Self>) {
self.sync(cx);
let ids = self.excerpt_ids();
self.buffers.borrow_mut().clear();
let removed_buffer_ids = self
.buffers
.borrow_mut()
.drain()
.map(|(id, _)| id)
.collect();
self.excerpts_by_path.clear();
self.paths_by_excerpt.clear();
let mut snapshot = self.snapshot.borrow_mut();
@@ -2046,7 +2052,10 @@ impl MultiBuffer {
singleton_buffer_edited: false,
edited_buffer: None,
});
cx.emit(Event::ExcerptsRemoved { ids });
cx.emit(Event::ExcerptsRemoved {
ids,
removed_buffer_ids,
});
cx.notify();
}
@@ -2310,9 +2319,9 @@ impl MultiBuffer {
new_excerpts.append(suffix, &());
drop(cursor);
snapshot.excerpts = new_excerpts;
for buffer_id in removed_buffer_ids {
self.diffs.remove(&buffer_id);
snapshot.diffs.remove(&buffer_id);
for buffer_id in &removed_buffer_ids {
self.diffs.remove(buffer_id);
snapshot.diffs.remove(buffer_id);
}
if changed_trailing_excerpt {
@@ -2325,7 +2334,10 @@ impl MultiBuffer {
singleton_buffer_edited: false,
edited_buffer: None,
});
cx.emit(Event::ExcerptsRemoved { ids });
cx.emit(Event::ExcerptsRemoved {
ids,
removed_buffer_ids,
});
cx.notify();
}

View File

@@ -635,7 +635,7 @@ fn test_excerpt_events(cx: &mut App) {
predecessor,
excerpts,
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
Event::ExcerptsRemoved { ids, .. } => follower.remove_excerpts(ids, cx),
Event::Edited { .. } => {
*follower_edit_event_count.write() += 1;
}

View File

@@ -4,6 +4,7 @@ use std::{
sync::Arc,
};
use editor::RowHighlightOptions;
use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll};
use fuzzy::StringMatch;
use gpui::{
@@ -171,7 +172,10 @@ impl OutlineViewDelegate {
active_editor.highlight_rows::<OutlineRowHighlights>(
outline_item.range.start..outline_item.range.end,
cx.theme().colors().editor_highlighted_line_background,
true,
RowHighlightOptions {
autoscroll: true,
..Default::default()
},
cx,
);
active_editor.request_autoscroll(Autoscroll::center(), cx);

View File

@@ -5028,7 +5028,7 @@ fn subscribe_for_editor_events(
.extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
}
EditorEvent::ExcerptsRemoved { ids } => {
EditorEvent::ExcerptsRemoved { ids, .. } => {
let mut ids = ids.iter().collect::<HashSet<_>>();
for excerpts in outline_panel.excerpts.values_mut() {
excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));

View File

@@ -641,6 +641,7 @@ impl CompletionsQuery {
}
}
#[derive(Debug)]
pub enum SessionEvent {
Modules,
LoadedSources,

View File

@@ -1,3 +1,4 @@
mod conflict_set;
pub mod git_traversal;
use crate::{
@@ -10,11 +11,12 @@ use askpass::AskPassDelegate;
use buffer_diff::{BufferDiff, BufferDiffEvent};
use client::ProjectId;
use collections::HashMap;
pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate};
use fs::Fs;
use futures::{
FutureExt as _, StreamExt as _,
FutureExt, StreamExt as _,
channel::{mpsc, oneshot},
future::{self, Shared},
future::{self, Shared, try_join_all},
};
use git::{
BuildPermalinkParams, GitHostingProviderRegistry, WORK_DIRECTORY_REPO_PATH,
@@ -74,7 +76,7 @@ pub struct GitStore {
#[allow(clippy::type_complexity)]
loading_diffs:
HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
diffs: HashMap<BufferId, Entity<BufferDiffState>>,
diffs: HashMap<BufferId, Entity<BufferGitState>>,
shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
_subscriptions: Vec<Subscription>,
}
@@ -85,12 +87,15 @@ struct SharedDiffs {
uncommitted: Option<Entity<BufferDiff>>,
}
struct BufferDiffState {
struct BufferGitState {
unstaged_diff: Option<WeakEntity<BufferDiff>>,
uncommitted_diff: Option<WeakEntity<BufferDiff>>,
conflict_set: Option<WeakEntity<ConflictSet>>,
recalculate_diff_task: Option<Task<Result<()>>>,
reparse_conflict_markers_task: Option<Task<Result<()>>>,
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
conflict_updated_futures: Vec<oneshot::Sender<()>>,
recalculating_tx: postage::watch::Sender<bool>,
/// These operation counts are used to ensure that head and index text
@@ -224,17 +229,26 @@ impl sum_tree::KeyedItem for StatusEntry {
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RepositoryId(pub u64);
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MergeDetails {
pub conflicted_paths: TreeSet<RepoPath>,
pub message: Option<SharedString>,
pub apply_head: Option<CommitDetails>,
pub cherry_pick_head: Option<CommitDetails>,
pub merge_heads: Vec<CommitDetails>,
pub rebase_head: Option<CommitDetails>,
pub revert_head: Option<CommitDetails>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepositorySnapshot {
pub id: RepositoryId,
pub merge_message: Option<SharedString>,
pub statuses_by_path: SumTree<StatusEntry>,
pub work_directory_abs_path: Arc<Path>,
pub branch: Option<Branch>,
pub head_commit: Option<CommitDetails>,
pub merge_conflicts: TreeSet<RepoPath>,
pub merge_head_shas: Vec<SharedString>,
pub scan_id: u64,
pub merge: MergeDetails,
}
type JobId = u64;
@@ -297,6 +311,7 @@ pub enum GitStoreEvent {
RepositoryRemoved(RepositoryId),
IndexWriteError(anyhow::Error),
JobsUpdated,
ConflictsUpdated,
}
impl EventEmitter<RepositoryEvent> for Repository {}
@@ -681,10 +696,11 @@ impl GitStore {
let text_snapshot = buffer.text_snapshot();
this.loading_diffs.remove(&(buffer_id, kind));
let git_store = cx.weak_entity();
let diff_state = this
.diffs
.entry(buffer_id)
.or_insert_with(|| cx.new(|_| BufferDiffState::default()));
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
@@ -737,6 +753,62 @@ impl GitStore {
diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
}
pub fn open_conflict_set(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Entity<ConflictSet> {
log::debug!("open conflict set");
let buffer_id = buffer.read(cx).remote_id();
if let Some(git_state) = self.diffs.get(&buffer_id) {
if let Some(conflict_set) = git_state
.read(cx)
.conflict_set
.as_ref()
.and_then(|weak| weak.upgrade())
{
let conflict_set = conflict_set.clone();
let buffer_snapshot = buffer.read(cx).text_snapshot();
git_state.update(cx, |state, cx| {
let _ = state.reparse_conflict_markers(buffer_snapshot, cx);
});
return conflict_set;
}
}
let is_unmerged = self
.repository_and_path_for_buffer_id(buffer_id, cx)
.map_or(false, |(repo, path)| {
repo.read(cx)
.snapshot
.merge
.conflicted_paths
.contains(&path)
});
let git_store = cx.weak_entity();
let buffer_git_state = self
.diffs
.entry(buffer_id)
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx));
self._subscriptions
.push(cx.subscribe(&conflict_set, |_, _, _, cx| {
cx.emit(GitStoreEvent::ConflictsUpdated);
}));
buffer_git_state.update(cx, |state, cx| {
state.conflict_set = Some(conflict_set.downgrade());
let buffer_snapshot = buffer.read(cx).text_snapshot();
let _ = state.reparse_conflict_markers(buffer_snapshot, cx);
});
conflict_set
}
pub fn project_path_git_status(
&self,
project_path: &ProjectPath,
@@ -1079,6 +1151,35 @@ impl GitStore {
cx: &mut Context<Self>,
) {
let id = repo.read(cx).id;
let merge_conflicts = repo.read(cx).snapshot.merge.conflicted_paths.clone();
for (buffer_id, diff) in self.diffs.iter() {
if let Some((buffer_repo, repo_path)) =
self.repository_and_path_for_buffer_id(*buffer_id, cx)
{
if buffer_repo == repo {
diff.update(cx, |diff, cx| {
if let Some(conflict_set) = &diff.conflict_set {
let conflict_status_changed =
conflict_set.update(cx, |conflict_set, cx| {
let has_conflict = merge_conflicts.contains(&repo_path);
conflict_set.set_has_conflict(has_conflict, cx)
})?;
if conflict_status_changed {
let buffer_store = self.buffer_store.read(cx);
if let Some(buffer) = buffer_store.get(*buffer_id) {
let _ = diff.reparse_conflict_markers(
buffer.read(cx).text_snapshot(),
cx,
);
}
}
}
anyhow::Ok(())
})
.ok();
}
}
}
cx.emit(GitStoreEvent::RepositoryUpdated(
id,
event.clone(),
@@ -1218,9 +1319,15 @@ impl GitStore {
if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
let buffer = buffer.read(cx).text_snapshot();
diff_state.update(cx, |diff_state, cx| {
diff_state.recalculate_diffs(buffer, cx);
futures.extend(diff_state.wait_for_recalculation());
diff_state.recalculate_diffs(buffer.clone(), cx);
futures.extend(diff_state.wait_for_recalculation().map(FutureExt::boxed));
});
futures.push(diff_state.update(cx, |diff_state, cx| {
diff_state
.reparse_conflict_markers(buffer, cx)
.map(|_| {})
.boxed()
}));
}
}
async move {
@@ -2094,13 +2201,86 @@ impl GitStore {
}
}
impl BufferDiffState {
impl BufferGitState {
fn new(_git_store: WeakEntity<GitStore>) -> Self {
Self {
unstaged_diff: Default::default(),
uncommitted_diff: Default::default(),
recalculate_diff_task: Default::default(),
language: Default::default(),
language_registry: Default::default(),
recalculating_tx: postage::watch::channel_with(false).0,
hunk_staging_operation_count: 0,
hunk_staging_operation_count_as_of_write: 0,
head_text: Default::default(),
index_text: Default::default(),
head_changed: Default::default(),
index_changed: Default::default(),
language_changed: Default::default(),
conflict_updated_futures: Default::default(),
conflict_set: Default::default(),
reparse_conflict_markers_task: Default::default(),
}
}
fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.language = buffer.read(cx).language().cloned();
self.language_changed = true;
let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx);
}
fn reparse_conflict_markers(
&mut self,
buffer: text::BufferSnapshot,
cx: &mut Context<Self>,
) -> oneshot::Receiver<()> {
let (tx, rx) = oneshot::channel();
let Some(conflict_set) = self
.conflict_set
.as_ref()
.and_then(|conflict_set| conflict_set.upgrade())
else {
return rx;
};
let old_snapshot = conflict_set.read_with(cx, |conflict_set, _| {
if conflict_set.has_conflict {
Some(conflict_set.snapshot())
} else {
None
}
});
if let Some(old_snapshot) = old_snapshot {
self.conflict_updated_futures.push(tx);
self.reparse_conflict_markers_task = Some(cx.spawn(async move |this, cx| {
let (snapshot, changed_range) = cx
.background_spawn(async move {
let new_snapshot = ConflictSet::parse(&buffer);
let changed_range = old_snapshot.compare(&new_snapshot, &buffer);
(new_snapshot, changed_range)
})
.await;
this.update(cx, |this, cx| {
if let Some(conflict_set) = &this.conflict_set {
conflict_set
.update(cx, |conflict_set, cx| {
conflict_set.set_snapshot(snapshot, changed_range, cx);
})
.ok();
}
let futures = std::mem::take(&mut this.conflict_updated_futures);
for tx in futures {
tx.send(()).ok();
}
})
}))
}
rx
}
fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
}
@@ -2335,26 +2515,6 @@ impl BufferDiffState {
}
}
impl Default for BufferDiffState {
fn default() -> Self {
Self {
unstaged_diff: Default::default(),
uncommitted_diff: Default::default(),
recalculate_diff_task: Default::default(),
language: Default::default(),
language_registry: Default::default(),
recalculating_tx: postage::watch::channel_with(false).0,
hunk_staging_operation_count: 0,
hunk_staging_operation_count_as_of_write: 0,
head_text: Default::default(),
index_text: Default::default(),
head_changed: Default::default(),
index_changed: Default::default(),
language_changed: Default::default(),
}
}
}
fn make_remote_delegate(
this: Entity<GitStore>,
project_id: u64,
@@ -2397,14 +2557,12 @@ impl RepositorySnapshot {
fn empty(id: RepositoryId, work_directory_abs_path: Arc<Path>) -> Self {
Self {
id,
merge_message: None,
statuses_by_path: Default::default(),
work_directory_abs_path,
branch: None,
head_commit: None,
merge_conflicts: Default::default(),
merge_head_shas: Default::default(),
scan_id: 0,
merge: Default::default(),
}
}
@@ -2419,7 +2577,8 @@ impl RepositorySnapshot {
.collect(),
removed_statuses: Default::default(),
current_merge_conflicts: self
.merge_conflicts
.merge
.conflicted_paths
.iter()
.map(|repo_path| repo_path.to_proto())
.collect(),
@@ -2480,7 +2639,8 @@ impl RepositorySnapshot {
updated_statuses,
removed_statuses,
current_merge_conflicts: self
.merge_conflicts
.merge
.conflicted_paths
.iter()
.map(|path| path.as_ref().to_proto())
.collect(),
@@ -2515,7 +2675,7 @@ impl RepositorySnapshot {
}
pub fn has_conflict(&self, repo_path: &RepoPath) -> bool {
self.merge_conflicts.contains(repo_path)
self.merge.conflicted_paths.contains(repo_path)
}
/// This is the name that will be displayed in the repository selector for this repository.
@@ -2529,7 +2689,77 @@ impl RepositorySnapshot {
}
}
impl MergeDetails {
async fn load(
backend: &Arc<dyn GitRepository>,
status: &SumTree<StatusEntry>,
prev_snapshot: &RepositorySnapshot,
) -> Result<(MergeDetails, bool)> {
fn sha_eq<'a>(
l: impl IntoIterator<Item = &'a CommitDetails>,
r: impl IntoIterator<Item = &'a CommitDetails>,
) -> bool {
l.into_iter()
.map(|commit| &commit.sha)
.eq(r.into_iter().map(|commit| &commit.sha))
}
let merge_heads = try_join_all(
backend
.merge_head_shas()
.into_iter()
.map(|sha| backend.show(sha)),
)
.await?;
let cherry_pick_head = backend.show("CHERRY_PICK_HEAD".into()).await.ok();
let rebase_head = backend.show("REBASE_HEAD".into()).await.ok();
let revert_head = backend.show("REVERT_HEAD".into()).await.ok();
let apply_head = backend.show("APPLY_HEAD".into()).await.ok();
let message = backend.merge_message().await.map(SharedString::from);
let merge_heads_changed = !sha_eq(
merge_heads.as_slice(),
prev_snapshot.merge.merge_heads.as_slice(),
) || !sha_eq(
cherry_pick_head.as_ref(),
prev_snapshot.merge.cherry_pick_head.as_ref(),
) || !sha_eq(
apply_head.as_ref(),
prev_snapshot.merge.apply_head.as_ref(),
) || !sha_eq(
rebase_head.as_ref(),
prev_snapshot.merge.rebase_head.as_ref(),
) || !sha_eq(
revert_head.as_ref(),
prev_snapshot.merge.revert_head.as_ref(),
);
let conflicted_paths = if merge_heads_changed {
TreeSet::from_ordered_entries(
status
.iter()
.filter(|entry| entry.status.is_conflicted())
.map(|entry| entry.repo_path.clone()),
)
} else {
prev_snapshot.merge.conflicted_paths.clone()
};
let details = MergeDetails {
conflicted_paths,
message,
apply_head,
cherry_pick_head,
merge_heads,
rebase_head,
revert_head,
};
Ok((details, merge_heads_changed))
}
}
impl Repository {
pub fn snapshot(&self) -> RepositorySnapshot {
self.snapshot.clone()
}
fn local(
id: RepositoryId,
work_directory_abs_path: Arc<Path>,
@@ -3731,7 +3961,7 @@ impl Repository {
.as_ref()
.map(proto_to_commit_details);
self.snapshot.merge_conflicts = conflicted_paths;
self.snapshot.merge.conflicted_paths = conflicted_paths;
let edits = update
.removed_statuses
@@ -4321,16 +4551,6 @@ async fn compute_snapshot(
let branches = backend.branches().await?;
let branch = branches.into_iter().find(|branch| branch.is_head);
let statuses = backend.status(&[WORK_DIRECTORY_REPO_PATH.clone()]).await?;
let merge_message = backend
.merge_message()
.await
.and_then(|msg| Some(msg.lines().nth(0)?.to_owned().into()));
let merge_head_shas = backend
.merge_head_shas()
.into_iter()
.map(SharedString::from)
.collect();
let statuses_by_path = SumTree::from_iter(
statuses
.entries
@@ -4341,47 +4561,36 @@ async fn compute_snapshot(
}),
&(),
);
let (merge_details, merge_heads_changed) =
MergeDetails::load(&backend, &statuses_by_path, &prev_snapshot).await?;
let merge_head_shas_changed = merge_head_shas != prev_snapshot.merge_head_shas;
if merge_head_shas_changed
if merge_heads_changed
|| branch != prev_snapshot.branch
|| statuses_by_path != prev_snapshot.statuses_by_path
{
events.push(RepositoryEvent::Updated { full_scan: true });
}
let mut current_merge_conflicts = TreeSet::default();
for (repo_path, status) in statuses.entries.iter() {
if status.is_conflicted() {
current_merge_conflicts.insert(repo_path.clone());
}
}
// Cache merge conflict paths so they don't change from staging/unstaging,
// until the merge heads change (at commit time, etc.).
let mut merge_conflicts = prev_snapshot.merge_conflicts.clone();
if merge_head_shas_changed {
merge_conflicts = current_merge_conflicts;
if merge_heads_changed {
events.push(RepositoryEvent::MergeHeadsChanged);
}
// Useful when branch is None in detached head state
let head_commit = match backend.head_sha() {
Some(head_sha) => backend.show(head_sha).await.ok(),
Some(head_sha) => backend.show(head_sha).await.log_err(),
None => None,
};
let snapshot = RepositorySnapshot {
id,
merge_message,
statuses_by_path,
work_directory_abs_path,
scan_id: prev_snapshot.scan_id + 1,
branch,
head_commit,
merge_conflicts,
merge_head_shas,
merge: merge_details,
};
Ok((snapshot, events))

View File

@@ -0,0 +1,560 @@
use gpui::{App, Context, Entity, EventEmitter};
use std::{cmp::Ordering, ops::Range, sync::Arc};
use text::{Anchor, BufferId, OffsetRangeExt as _};
pub struct ConflictSet {
pub has_conflict: bool,
pub snapshot: ConflictSetSnapshot,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConflictSetUpdate {
pub buffer_range: Option<Range<Anchor>>,
pub old_range: Range<usize>,
pub new_range: Range<usize>,
}
#[derive(Debug, Clone)]
pub struct ConflictSetSnapshot {
pub buffer_id: BufferId,
pub conflicts: Arc<[ConflictRegion]>,
}
impl ConflictSetSnapshot {
pub fn conflicts_in_range(
&self,
range: Range<Anchor>,
buffer: &text::BufferSnapshot,
) -> &[ConflictRegion] {
let start_ix = self
.conflicts
.binary_search_by(|conflict| {
conflict
.range
.end
.cmp(&range.start, buffer)
.then(Ordering::Greater)
})
.unwrap_err();
let end_ix = start_ix
+ self.conflicts[start_ix..]
.binary_search_by(|conflict| {
conflict
.range
.start
.cmp(&range.end, buffer)
.then(Ordering::Less)
})
.unwrap_err();
&self.conflicts[start_ix..end_ix]
}
pub fn compare(&self, other: &Self, buffer: &text::BufferSnapshot) -> ConflictSetUpdate {
let common_prefix_len = self
.conflicts
.iter()
.zip(other.conflicts.iter())
.take_while(|(old, new)| old == new)
.count();
let common_suffix_len = self.conflicts[common_prefix_len..]
.iter()
.rev()
.zip(other.conflicts[common_prefix_len..].iter().rev())
.take_while(|(old, new)| old == new)
.count();
let old_conflicts =
&self.conflicts[common_prefix_len..(self.conflicts.len() - common_suffix_len)];
let new_conflicts =
&other.conflicts[common_prefix_len..(other.conflicts.len() - common_suffix_len)];
let old_range = common_prefix_len..(common_prefix_len + old_conflicts.len());
let new_range = common_prefix_len..(common_prefix_len + new_conflicts.len());
let start = match (old_conflicts.first(), new_conflicts.first()) {
(None, None) => None,
(None, Some(conflict)) => Some(conflict.range.start),
(Some(conflict), None) => Some(conflict.range.start),
(Some(first), Some(second)) => Some(first.range.start.min(&second.range.start, buffer)),
};
let end = match (old_conflicts.last(), new_conflicts.last()) {
(None, None) => None,
(None, Some(conflict)) => Some(conflict.range.end),
(Some(first), None) => Some(first.range.end),
(Some(first), Some(second)) => Some(first.range.end.max(&second.range.end, buffer)),
};
ConflictSetUpdate {
buffer_range: start.zip(end).map(|(start, end)| start..end),
old_range,
new_range,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConflictRegion {
pub range: Range<Anchor>,
pub ours: Range<Anchor>,
pub theirs: Range<Anchor>,
pub base: Option<Range<Anchor>>,
}
impl ConflictRegion {
pub fn resolve(
&self,
buffer: Entity<language::Buffer>,
ranges: &[Range<Anchor>],
cx: &mut App,
) {
let buffer_snapshot = buffer.read(cx).snapshot();
let mut deletions = Vec::new();
let empty = "";
let outer_range = self.range.to_offset(&buffer_snapshot);
let mut offset = outer_range.start;
for kept_range in ranges {
let kept_range = kept_range.to_offset(&buffer_snapshot);
if kept_range.start > offset {
deletions.push((offset..kept_range.start, empty));
}
offset = kept_range.end;
}
if outer_range.end > offset {
deletions.push((offset..outer_range.end, empty));
}
buffer.update(cx, |buffer, cx| {
buffer.edit(deletions, None, cx);
});
}
}
impl ConflictSet {
pub fn new(buffer_id: BufferId, has_conflict: bool, _: &mut Context<Self>) -> Self {
Self {
has_conflict,
snapshot: ConflictSetSnapshot {
buffer_id,
conflicts: Default::default(),
},
}
}
pub fn set_has_conflict(&mut self, has_conflict: bool, cx: &mut Context<Self>) -> bool {
if has_conflict != self.has_conflict {
self.has_conflict = has_conflict;
if !self.has_conflict {
cx.emit(ConflictSetUpdate {
buffer_range: None,
old_range: 0..self.snapshot.conflicts.len(),
new_range: 0..0,
});
self.snapshot.conflicts = Default::default();
}
true
} else {
false
}
}
pub fn snapshot(&self) -> ConflictSetSnapshot {
self.snapshot.clone()
}
pub fn set_snapshot(
&mut self,
snapshot: ConflictSetSnapshot,
update: ConflictSetUpdate,
cx: &mut Context<Self>,
) {
self.snapshot = snapshot;
cx.emit(update);
}
pub fn parse(buffer: &text::BufferSnapshot) -> ConflictSetSnapshot {
let mut conflicts = Vec::new();
let mut line_pos = 0;
let mut lines = buffer.text_for_range(0..buffer.len()).lines();
let mut conflict_start: Option<usize> = None;
let mut ours_start: Option<usize> = None;
let mut ours_end: Option<usize> = None;
let mut base_start: Option<usize> = None;
let mut base_end: Option<usize> = None;
let mut theirs_start: Option<usize> = None;
while let Some(line) = lines.next() {
let line_end = line_pos + line.len();
if line.starts_with("<<<<<<< ") {
// If we see a new conflict marker while already parsing one,
// abandon the previous one and start a new one
conflict_start = Some(line_pos);
ours_start = Some(line_end + 1);
} else if line.starts_with("||||||| ")
&& conflict_start.is_some()
&& ours_start.is_some()
{
ours_end = Some(line_pos);
base_start = Some(line_end + 1);
} else if line.starts_with("=======")
&& conflict_start.is_some()
&& ours_start.is_some()
{
// Set ours_end if not already set (would be set if we have base markers)
if ours_end.is_none() {
ours_end = Some(line_pos);
} else if base_start.is_some() {
base_end = Some(line_pos);
}
theirs_start = Some(line_end + 1);
} else if line.starts_with(">>>>>>> ")
&& conflict_start.is_some()
&& ours_start.is_some()
&& ours_end.is_some()
&& theirs_start.is_some()
{
let theirs_end = line_pos;
let conflict_end = line_end + 1;
let range = buffer.anchor_after(conflict_start.unwrap())
..buffer.anchor_before(conflict_end);
let ours = buffer.anchor_after(ours_start.unwrap())
..buffer.anchor_before(ours_end.unwrap());
let theirs =
buffer.anchor_after(theirs_start.unwrap())..buffer.anchor_before(theirs_end);
let base = base_start
.zip(base_end)
.map(|(start, end)| buffer.anchor_after(start)..buffer.anchor_before(end));
conflicts.push(ConflictRegion {
range,
ours,
theirs,
base,
});
conflict_start = None;
ours_start = None;
ours_end = None;
base_start = None;
base_end = None;
theirs_start = None;
}
line_pos = line_end + 1;
}
ConflictSetSnapshot {
conflicts: conflicts.into(),
buffer_id: buffer.remote_id(),
}
}
}
impl EventEmitter<ConflictSetUpdate> for ConflictSet {}
#[cfg(test)]
mod tests {
use std::sync::mpsc;
use crate::{Project, project_settings::ProjectSettings};
use super::*;
use fs::FakeFs;
use git::status::{UnmergedStatus, UnmergedStatusCode};
use gpui::{BackgroundExecutor, TestAppContext};
use language::language_settings::AllLanguageSettings;
use serde_json::json;
use settings::Settings as _;
use text::{Buffer, BufferId, ToOffset as _};
use unindent::Unindent as _;
use util::path;
use worktree::WorktreeSettings;
#[test]
fn test_parse_conflicts_in_buffer() {
// Create a buffer with conflict markers
let test_content = r#"
This is some text before the conflict.
<<<<<<< HEAD
This is our version
=======
This is their version
>>>>>>> branch-name
Another conflict:
<<<<<<< HEAD
Our second change
||||||| merged common ancestors
Original content
=======
Their second change
>>>>>>> branch-name
"#
.unindent();
let buffer_id = BufferId::new(1).unwrap();
let buffer = Buffer::new(0, buffer_id, test_content);
let snapshot = buffer.snapshot();
let conflict_snapshot = ConflictSet::parse(&snapshot);
assert_eq!(conflict_snapshot.conflicts.len(), 2);
let first = &conflict_snapshot.conflicts[0];
assert!(first.base.is_none());
let our_text = snapshot
.text_for_range(first.ours.clone())
.collect::<String>();
let their_text = snapshot
.text_for_range(first.theirs.clone())
.collect::<String>();
assert_eq!(our_text, "This is our version\n");
assert_eq!(their_text, "This is their version\n");
let second = &conflict_snapshot.conflicts[1];
assert!(second.base.is_some());
let our_text = snapshot
.text_for_range(second.ours.clone())
.collect::<String>();
let their_text = snapshot
.text_for_range(second.theirs.clone())
.collect::<String>();
let base_text = snapshot
.text_for_range(second.base.as_ref().unwrap().clone())
.collect::<String>();
assert_eq!(our_text, "Our second change\n");
assert_eq!(their_text, "Their second change\n");
assert_eq!(base_text, "Original content\n");
// Test conflicts_in_range
let range = snapshot.anchor_before(0)..snapshot.anchor_before(snapshot.len());
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
assert_eq!(conflicts_in_range.len(), 2);
// Test with a range that includes only the first conflict
let first_conflict_end = conflict_snapshot.conflicts[0].range.end;
let range = snapshot.anchor_before(0)..first_conflict_end;
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
assert_eq!(conflicts_in_range.len(), 1);
// Test with a range that includes only the second conflict
let second_conflict_start = conflict_snapshot.conflicts[1].range.start;
let range = second_conflict_start..snapshot.anchor_before(snapshot.len());
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
assert_eq!(conflicts_in_range.len(), 1);
// Test with a range that doesn't include any conflicts
let range = buffer.anchor_after(first_conflict_end.to_offset(&buffer) + 1)
..buffer.anchor_before(second_conflict_start.to_offset(&buffer) - 1);
let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot);
assert_eq!(conflicts_in_range.len(), 0);
}
#[test]
fn test_nested_conflict_markers() {
// Create a buffer with nested conflict markers
let test_content = r#"
This is some text before the conflict.
<<<<<<< HEAD
This is our version
<<<<<<< HEAD
This is a nested conflict marker
=======
This is their version in a nested conflict
>>>>>>> branch-nested
=======
This is their version
>>>>>>> branch-name
"#
.unindent();
let buffer_id = BufferId::new(1).unwrap();
let buffer = Buffer::new(0, buffer_id, test_content.to_string());
let snapshot = buffer.snapshot();
let conflict_snapshot = ConflictSet::parse(&snapshot);
assert_eq!(conflict_snapshot.conflicts.len(), 1);
// The conflict should have our version, their version, but no base
let conflict = &conflict_snapshot.conflicts[0];
assert!(conflict.base.is_none());
// Check that the nested conflict was detected correctly
let our_text = snapshot
.text_for_range(conflict.ours.clone())
.collect::<String>();
assert_eq!(our_text, "This is a nested conflict marker\n");
let their_text = snapshot
.text_for_range(conflict.theirs.clone())
.collect::<String>();
assert_eq!(their_text, "This is their version in a nested conflict\n");
}
#[test]
fn test_conflicts_in_range() {
// Create a buffer with conflict markers
let test_content = r#"
one
<<<<<<< HEAD1
two
=======
three
>>>>>>> branch1
four
five
<<<<<<< HEAD2
six
=======
seven
>>>>>>> branch2
eight
nine
<<<<<<< HEAD3
ten
=======
eleven
>>>>>>> branch3
twelve
<<<<<<< HEAD4
thirteen
=======
fourteen
>>>>>>> branch4
fifteen
"#
.unindent();
let buffer_id = BufferId::new(1).unwrap();
let buffer = Buffer::new(0, buffer_id, test_content.clone());
let snapshot = buffer.snapshot();
let conflict_snapshot = ConflictSet::parse(&snapshot);
assert_eq!(conflict_snapshot.conflicts.len(), 4);
let range = test_content.find("seven").unwrap()..test_content.find("eleven").unwrap();
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
assert_eq!(
conflict_snapshot.conflicts_in_range(range, &snapshot),
&conflict_snapshot.conflicts[1..=2]
);
let range = test_content.find("one").unwrap()..test_content.find("<<<<<<< HEAD2").unwrap();
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
assert_eq!(
conflict_snapshot.conflicts_in_range(range, &snapshot),
&conflict_snapshot.conflicts[0..=1]
);
let range =
test_content.find("eight").unwrap() - 1..test_content.find(">>>>>>> branch3").unwrap();
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
assert_eq!(
conflict_snapshot.conflicts_in_range(range, &snapshot),
&conflict_snapshot.conflicts[1..=2]
);
let range = test_content.find("thirteen").unwrap() - 1..test_content.len();
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
assert_eq!(
conflict_snapshot.conflicts_in_range(range, &snapshot),
&conflict_snapshot.conflicts[3..=3]
);
}
#[gpui::test]
async fn test_conflict_updates(executor: BackgroundExecutor, cx: &mut TestAppContext) {
env_logger::try_init().ok();
cx.update(|cx| {
settings::init(cx);
WorktreeSettings::register(cx);
ProjectSettings::register(cx);
AllLanguageSettings::register(cx);
});
let initial_text = "
one
two
three
four
five
"
.unindent();
let fs = FakeFs::new(executor);
fs.insert_tree(
path!("/project"),
json!({
".git": {},
"a.txt": initial_text,
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (git_store, buffer) = project.update(cx, |project, cx| {
(
project.git_store().clone(),
project.open_local_buffer(path!("/project/a.txt"), cx),
)
});
let buffer = buffer.await.unwrap();
let conflict_set = git_store.update(cx, |git_store, cx| {
git_store.open_conflict_set(buffer.clone(), cx)
});
let (events_tx, events_rx) = mpsc::channel::<ConflictSetUpdate>();
let _conflict_set_subscription = cx.update(|cx| {
cx.subscribe(&conflict_set, move |_, event, _| {
events_tx.send(event.clone()).ok();
})
});
let conflicts_snapshot = conflict_set.update(cx, |conflict_set, _| conflict_set.snapshot());
assert!(conflicts_snapshot.conflicts.is_empty());
buffer.update(cx, |buffer, cx| {
buffer.edit(
[
(4..4, "<<<<<<< HEAD\n"),
(14..14, "=======\nTWO\n>>>>>>> branch\n"),
],
None,
cx,
);
});
cx.run_until_parked();
events_rx.try_recv().expect_err(
"no conflicts should be registered as long as the file's status is unchanged",
);
fs.with_git_state(path!("/project/.git").as_ref(), true, |state| {
state.unmerged_paths.insert(
"a.txt".into(),
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
},
);
// Cause the repository to emit MergeHeadsChanged.
state.merge_head_shas = vec!["abc".into(), "def".into()]
})
.unwrap();
cx.run_until_parked();
let update = events_rx
.try_recv()
.expect("status change should trigger conflict parsing");
assert_eq!(update.old_range, 0..0);
assert_eq!(update.new_range, 0..1);
let conflict = conflict_set.update(cx, |conflict_set, _| {
conflict_set.snapshot().conflicts[0].clone()
});
cx.update(|cx| {
conflict.resolve(buffer.clone(), &[conflict.theirs.clone()], cx);
});
cx.run_until_parked();
let update = events_rx
.try_recv()
.expect("conflicts should be removed after resolution");
assert_eq!(update.old_range, 0..1);
assert_eq!(update.new_range, 0..0);
}
}

View File

@@ -29,7 +29,10 @@ pub mod search_history;
mod yarn;
use crate::git_store::GitStore;
pub use git_store::git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal};
pub use git_store::{
ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal},
};
use anyhow::{Context as _, Result, anyhow};
use buffer_store::{BufferStore, BufferStoreEvent};

View File

@@ -83,7 +83,6 @@ impl VsCodeDebugTaskDefinition {
}
}
/// blah
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VsCodeDebugTaskFile {

View File

@@ -115,6 +115,7 @@ pub struct TerminalView {
blinking_paused: bool,
blink_epoch: usize,
hover_target_tooltip: Option<String>,
hover_tooltip_update: Task<()>,
workspace_id: Option<WorkspaceId>,
show_breadcrumbs: bool,
block_below_cursor: Option<Rc<BlockProperties>>,
@@ -197,6 +198,7 @@ impl TerminalView {
blinking_paused: false,
blink_epoch: 0,
hover_target_tooltip: None,
hover_tooltip_update: Task::ready(()),
workspace_id,
show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
block_below_cursor: None,
@@ -844,7 +846,7 @@ fn subscribe_for_terminal_events(
let terminal_events_subscription = cx.subscribe_in(
terminal,
window,
move |this, _, event, window, cx| match event {
move |terminal_view, _, event, window, cx| match event {
Event::Wakeup => {
cx.notify();
cx.emit(Event::Wakeup);
@@ -853,7 +855,7 @@ fn subscribe_for_terminal_events(
}
Event::Bell => {
this.has_bell = true;
terminal_view.has_bell = true;
cx.emit(Event::Wakeup);
}
@@ -862,7 +864,7 @@ fn subscribe_for_terminal_events(
TerminalSettings::get_global(cx).blinking,
TerminalBlink::TerminalControlled
) {
this.blinking_terminal_enabled = *blinking;
terminal_view.blinking_terminal_enabled = *blinking;
}
}
@@ -871,25 +873,46 @@ fn subscribe_for_terminal_events(
}
Event::NewNavigationTarget(maybe_navigation_target) => {
this.hover_target_tooltip =
maybe_navigation_target
.as_ref()
.and_then(|navigation_target| match navigation_target {
MaybeNavigationTarget::Url(url) => Some(url.clone()),
MaybeNavigationTarget::PathLike(path_like_target) => {
let valid_files_to_open_task = possible_open_target(
&workspace,
&path_like_target.terminal_dir,
&path_like_target.maybe_path,
cx,
);
Some(match smol::block_on(valid_files_to_open_task)? {
OpenTarget::File(path, _) | OpenTarget::Worktree(path, _) => {
path.to_string(|path| path.to_string_lossy().to_string())
}
})
}
});
match maybe_navigation_target.as_ref() {
None => {
terminal_view.hover_target_tooltip = None;
terminal_view.hover_tooltip_update = Task::ready(());
}
Some(MaybeNavigationTarget::Url(url)) => {
terminal_view.hover_target_tooltip = Some(url.clone());
terminal_view.hover_tooltip_update = Task::ready(());
}
Some(MaybeNavigationTarget::PathLike(path_like_target)) => {
let valid_files_to_open_task = possible_open_target(
&workspace,
&path_like_target.terminal_dir,
&path_like_target.maybe_path,
cx,
);
terminal_view.hover_tooltip_update =
cx.spawn(async move |terminal_view, cx| {
let file_to_open = valid_files_to_open_task.await;
terminal_view
.update(cx, |terminal_view, _| match file_to_open {
Some(
OpenTarget::File(path, _)
| OpenTarget::Worktree(path, _),
) => {
terminal_view.hover_target_tooltip =
Some(path.to_string(|path| {
path.to_string_lossy().to_string()
}));
}
None => {
terminal_view.hover_target_tooltip = None;
}
})
.ok();
});
}
}
cx.notify()
}
@@ -897,7 +920,7 @@ fn subscribe_for_terminal_events(
MaybeNavigationTarget::Url(url) => cx.open_url(url),
MaybeNavigationTarget::PathLike(path_like_target) => {
if this.hover_target_tooltip.is_none() {
if terminal_view.hover_target_tooltip.is_none() {
return;
}
let task_workspace = workspace.clone();
@@ -1207,9 +1230,12 @@ fn possible_open_target(
let fs = workspace.read(cx).project().read(cx).fs().clone();
cx.background_spawn(async move {
for path_to_check in fs_paths_to_check {
if let Some(metadata) = fs.metadata(&path_to_check.path).await.ok().flatten() {
return Some(OpenTarget::File(path_to_check, metadata));
for mut path_to_check in fs_paths_to_check {
if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() {
if let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() {
path_to_check.path = fs_path_to_check;
return Some(OpenTarget::File(path_to_check, metadata));
}
}
}

View File

@@ -143,6 +143,11 @@ impl ThemeColors {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: orange().light().step_12(),
version_control_ignored: gray().light().step_12(),
version_control_conflict_ours_background: green().light().step_10().alpha(0.5),
version_control_conflict_theirs_background: blue().light().step_10().alpha(0.5),
version_control_conflict_ours_marker_background: green().light().step_10().alpha(0.7),
version_control_conflict_theirs_marker_background: blue().light().step_10().alpha(0.7),
version_control_conflict_divider_background: Hsla::default(),
}
}
@@ -258,6 +263,11 @@ impl ThemeColors {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: orange().dark().step_12(),
version_control_ignored: gray().dark().step_12(),
version_control_conflict_ours_background: green().dark().step_10().alpha(0.5),
version_control_conflict_theirs_background: blue().dark().step_10().alpha(0.5),
version_control_conflict_ours_marker_background: green().dark().step_10().alpha(0.7),
version_control_conflict_theirs_marker_background: blue().dark().step_10().alpha(0.7),
version_control_conflict_divider_background: Hsla::default(),
}
}
}

View File

@@ -201,6 +201,23 @@ pub(crate) fn zed_default_dark() -> Theme {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: crate::orange().light().step_12(),
version_control_ignored: crate::gray().light().step_12(),
version_control_conflict_ours_background: crate::green()
.light()
.step_12()
.alpha(0.5),
version_control_conflict_theirs_background: crate::blue()
.light()
.step_12()
.alpha(0.5),
version_control_conflict_ours_marker_background: crate::green()
.light()
.step_12()
.alpha(0.7),
version_control_conflict_theirs_marker_background: crate::blue()
.light()
.step_12()
.alpha(0.7),
version_control_conflict_divider_background: Hsla::default(),
},
status: StatusColors {
conflict: yellow,

View File

@@ -586,6 +586,26 @@ pub struct ThemeColorsContent {
/// Ignored version control color.
#[serde(rename = "version_control.ignored")]
pub version_control_ignored: Option<String>,
/// Background color for row highlights of "ours" regions in merge conflicts.
#[serde(rename = "version_control.conflict.ours_background")]
pub version_control_conflict_ours_background: Option<String>,
/// Background color for row highlights of "theirs" regions in merge conflicts.
#[serde(rename = "version_control.conflict.theirs_background")]
pub version_control_conflict_theirs_background: Option<String>,
/// Background color for row highlights of "ours" conflict markers in merge conflicts.
#[serde(rename = "version_control.conflict.ours_marker_background")]
pub version_control_conflict_ours_marker_background: Option<String>,
/// Background color for row highlights of "theirs" conflict markers in merge conflicts.
#[serde(rename = "version_control.conflict.theirs_marker_background")]
pub version_control_conflict_theirs_marker_background: Option<String>,
/// Background color for row highlights of the "ours"/"theirs" divider in merge conflicts.
#[serde(rename = "version_control.conflict.divider_background")]
pub version_control_conflict_divider_background: Option<String>,
}
impl ThemeColorsContent {
@@ -1037,6 +1057,26 @@ impl ThemeColorsContent {
.and_then(|color| try_parse_color(color).ok())
// Fall back to `conflict`, for backwards compatibility.
.or(status_colors.ignored),
version_control_conflict_ours_background: self
.version_control_conflict_ours_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_theirs_background: self
.version_control_conflict_theirs_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_ours_marker_background: self
.version_control_conflict_ours_marker_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_theirs_marker_background: self
.version_control_conflict_theirs_marker_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_conflict_divider_background: self
.version_control_conflict_divider_background
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
}
}
}

View File

@@ -261,6 +261,14 @@ pub struct ThemeColors {
pub version_control_conflict: Hsla,
/// Represents an ignored entry in version control systems.
pub version_control_ignored: Hsla,
/// Represents the "ours" region of a merge conflict.
pub version_control_conflict_ours_background: Hsla,
/// Represents the "theirs" region of a merge conflict.
pub version_control_conflict_theirs_background: Hsla,
pub version_control_conflict_ours_marker_background: Hsla,
pub version_control_conflict_theirs_marker_background: Hsla,
pub version_control_conflict_divider_background: Hsla,
}
#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)]

View File

@@ -30,7 +30,7 @@ itertools.workspace = true
language.workspace = true
log.workspace = true
multi_buffer.workspace = true
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", rev = "764dd270c642f77f10f3e19d05cc178a6cbe69f3", features = ["use_tokio"], optional = true }
picker.workspace = true
project.workspace = true
regex.workspace = true

View File

@@ -1500,7 +1500,7 @@ impl ShellExec {
editor.highlight_rows::<ShellExec>(
input_range.clone().unwrap(),
cx.theme().status().unreachable_background,
false,
Default::default(),
cx,
);

View File

@@ -61,4 +61,11 @@ impl WebSearchRegistry {
self.active_provider = Some(provider);
}
}
pub fn unregister_provider(&mut self, id: WebSearchProviderId) {
self.providers.remove(&id);
if self.active_provider.as_ref().map(|provider| provider.id()) == Some(id) {
self.active_provider = None;
}
}
}

View File

@@ -14,7 +14,6 @@ path = "src/web_search_providers.rs"
[dependencies]
anyhow.workspace = true
client.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true

View File

@@ -50,9 +50,11 @@ impl State {
}
}
pub const ZED_WEB_SEARCH_PROVIDER_ID: &'static str = "zed.dev";
impl WebSearchProvider for CloudWebSearchProvider {
fn id(&self) -> WebSearchProviderId {
WebSearchProviderId("zed.dev".into())
WebSearchProviderId(ZED_WEB_SEARCH_PROVIDER_ID.into())
}
fn search(&self, query: String, cx: &mut App) -> Task<Result<WebSearchResponse>> {

View File

@@ -1,10 +1,10 @@
mod cloud;
use client::Client;
use feature_flags::{FeatureFlagAppExt, ZedProWebSearchTool};
use gpui::{App, Context};
use language_model::LanguageModelRegistry;
use std::sync::Arc;
use web_search::WebSearchRegistry;
use web_search::{WebSearchProviderId, WebSearchRegistry};
pub fn init(client: Arc<Client>, cx: &mut App) {
let registry = WebSearchRegistry::global(cx);
@@ -18,18 +18,27 @@ fn register_web_search_providers(
client: Arc<Client>,
cx: &mut Context<WebSearchRegistry>,
) {
cx.observe_flag::<ZedProWebSearchTool, _>({
let client = client.clone();
move |is_enabled, cx| {
if is_enabled {
WebSearchRegistry::global(cx).update(cx, |registry, cx| {
registry.register_provider(
cx.subscribe(
&LanguageModelRegistry::global(cx),
move |this, registry, event, cx| match event {
language_model::Event::DefaultModelChanged => {
let using_zed_provider = registry
.read(cx)
.default_model()
.map_or(false, |default| default.is_provided_by_zed());
if using_zed_provider {
this.register_provider(
cloud::CloudWebSearchProvider::new(client.clone(), cx),
cx,
);
});
)
} else {
this.unregister_provider(WebSearchProviderId(
cloud::ZED_WEB_SEARCH_PROVIDER_ID.into(),
));
}
}
}
})
_ => {}
},
)
.detach();
}

View File

@@ -5879,7 +5879,8 @@ fn resize_bottom_dock(
window: &mut Window,
cx: &mut App,
) {
let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
let size =
new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE - workspace.bounds.top());
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
bottom_dock.resize_active_panel(Some(size), window, cx);
});

View File

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

View File

@@ -90,7 +90,7 @@ regex = { version = "1" }
regex-automata = { version = "0.4" }
regex-syntax = { version = "0.8" }
rust_decimal = { version = "1", default-features = false, features = ["maths", "serde", "std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["fs", "net"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
rustls = { version = "0.23", features = ["ring"] }
rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
@@ -205,7 +205,7 @@ regex = { version = "1" }
regex-automata = { version = "0.4" }
regex-syntax = { version = "0.8" }
rust_decimal = { version = "1", default-features = false, features = ["maths", "serde", "std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["fs", "net"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
rustls = { version = "0.23", features = ["ring"] }
rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
@@ -258,8 +258,8 @@ naga = { version = "23", features = ["msl-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal"] }
object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event", "mm", "param", "pipe", "process", "procfs", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "termios", "time"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
scopeguard = { version = "1" }
security-framework = { version = "3", features = ["OSX_10_14"] }
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
@@ -283,8 +283,8 @@ nix = { version = "0.29", features = ["fs", "pthread", "signal"] }
object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event", "mm", "param", "pipe", "process", "procfs", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "termios", "time"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
scopeguard = { version = "1" }
security-framework = { version = "3", features = ["OSX_10_14"] }
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
@@ -306,8 +306,8 @@ naga = { version = "23", features = ["msl-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal"] }
object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event", "mm", "param", "pipe", "process", "procfs", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "termios", "time"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
scopeguard = { version = "1" }
security-framework = { version = "3", features = ["OSX_10_14"] }
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
@@ -331,8 +331,8 @@ nix = { version = "0.29", features = ["fs", "pthread", "signal"] }
object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event", "mm", "param", "pipe", "process", "procfs", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "termios", "time"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
scopeguard = { version = "1" }
security-framework = { version = "3", features = ["OSX_10_14"] }
security-framework-sys = { version = "2", features = ["OSX_10_14"] }
@@ -356,7 +356,8 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std",
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
linux-raw-sys = { version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
@@ -367,8 +368,8 @@ proc-macro2 = { version = "1", features = ["span-locations"] }
quote = { version = "1" }
rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event", "mm", "param", "pipe", "process", "procfs", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "termios", "time"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
scopeguard = { version = "1" }
smallvec = { version = "1", default-features = false, features = ["write"] }
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
@@ -396,7 +397,8 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std",
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
linux-raw-sys = { version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
@@ -406,8 +408,8 @@ object = { version = "0.36", default-features = false, features = ["archive", "r
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event", "mm", "param", "pipe", "process", "procfs", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "termios", "time"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
scopeguard = { version = "1" }
smallvec = { version = "1", default-features = false, features = ["write"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
@@ -434,7 +436,8 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std",
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
linux-raw-sys = { version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
@@ -445,8 +448,8 @@ proc-macro2 = { version = "1", features = ["span-locations"] }
quote = { version = "1" }
rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event", "mm", "param", "pipe", "process", "procfs", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "termios", "time"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
scopeguard = { version = "1" }
smallvec = { version = "1", default-features = false, features = ["write"] }
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
@@ -474,7 +477,8 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std",
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
linux-raw-sys = { version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
@@ -484,8 +488,8 @@ object = { version = "0.36", default-features = false, features = ["archive", "r
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event", "mm", "param", "pipe", "process", "procfs", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "termios", "time"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
scopeguard = { version = "1" }
smallvec = { version = "1", default-features = false, features = ["write"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
@@ -506,7 +510,7 @@ hyper-rustls = { version = "0.27", default-features = false, features = ["http1"
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
scopeguard = { version = "1" }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
tokio-socks = { version = "0.5", features = ["futures-io"] }
@@ -514,7 +518,7 @@ tokio-stream = { version = "0.1", features = ["fs"] }
winapi = { version = "0.3", default-features = false, features = ["cfg", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "in6addr", "inaddr", "knownfolders", "minwinbase", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "sysinfoapi", "winbase", "windef", "winerror", "winioctl"] }
windows-core = { version = "0.61" }
windows-numerics = { version = "0.2" }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_UI_Shell"] }
@@ -529,7 +533,7 @@ itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
scopeguard = { version = "1" }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
tokio-socks = { version = "0.5", features = ["futures-io"] }
@@ -537,7 +541,7 @@ tokio-stream = { version = "0.1", features = ["fs"] }
winapi = { version = "0.3", default-features = false, features = ["cfg", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "in6addr", "inaddr", "knownfolders", "minwinbase", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "sysinfoapi", "winbase", "windef", "winerror", "winioctl"] }
windows-core = { version = "0.61" }
windows-numerics = { version = "0.2" }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_UI_Shell"] }
@@ -557,7 +561,8 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std",
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
linux-raw-sys = { version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
@@ -568,8 +573,8 @@ proc-macro2 = { version = "1", features = ["span-locations"] }
quote = { version = "1" }
rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event", "mm", "param", "pipe", "process", "procfs", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "termios", "time"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
scopeguard = { version = "1" }
smallvec = { version = "1", default-features = false, features = ["write"] }
syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] }
@@ -597,7 +602,8 @@ gimli = { version = "0.31", default-features = false, features = ["read", "std",
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
inout = { version = "0.1", default-features = false, features = ["block-padding"] }
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
linux-raw-sys = { version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
@@ -607,8 +613,8 @@ object = { version = "0.36", default-features = false, features = ["archive", "r
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["event", "mm", "param", "pipe", "process", "procfs", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "termios", "time"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] }
rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] }
scopeguard = { version = "1" }
smallvec = { version = "1", default-features = false, features = ["write"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }