diff --git a/Cargo.lock b/Cargo.lock index 9d0b121666..6e8e64edb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -453,7 +453,6 @@ dependencies = [ "assistant_slash_command", "assistant_tool", "async-watch", - "buffer_diff", "chrono", "client", "clock", @@ -692,7 +691,6 @@ name = "assistant_tool" version = "0.1.0" dependencies = [ "anyhow", - "buffer_diff", "clock", "collections", "derive_more", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index b818ca31d4..df79bf77a4 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -25,7 +25,6 @@ assistant_settings.workspace = true assistant_slash_command.workspace = true assistant_tool.workspace = true async-watch.workspace = true -buffer_diff.workspace = true chrono.workspace = true client.workspace = true clock.workspace = true @@ -83,7 +82,6 @@ workspace.workspace = true zed_actions.workspace = true [dev-dependencies] -buffer_diff = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] } indoc.workspace = true diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 51142ac4e4..5224b097cc 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,6 +1,5 @@ mod active_thread; mod assistant_configuration; -mod assistant_diff; mod assistant_model_selector; mod assistant_panel; mod buffer_codegen; @@ -37,7 +36,6 @@ pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate} pub use crate::inline_assistant::InlineAssistant; pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent}; pub use crate::thread_store::ThreadStore; -pub use assistant_diff::AssistantDiff; actions!( assistant2, diff --git a/crates/assistant2/src/assistant_diff.rs b/crates/assistant2/src/assistant_diff.rs deleted file mode 100644 index 855b7a8d79..0000000000 --- a/crates/assistant2/src/assistant_diff.rs +++ /dev/null @@ -1,625 +0,0 @@ -use crate::{Thread, ThreadEvent}; -use anyhow::Result; -use buffer_diff::DiffHunkStatus; -use collections::HashSet; -use editor::{Editor, EditorEvent, MultiBuffer}; -use futures::future; -use gpui::{ - prelude::*, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, - SharedString, Subscription, Task, WeakEntity, Window, -}; -use language::{Capability, OffsetRangeExt}; -use multi_buffer::PathKey; -use project::{Project, ProjectPath}; -use std::{ - any::{Any, TypeId}, - ops::Range, - sync::Arc, -}; -use ui::{prelude::*, IconButtonShape}; -use util::TryFutureExt; -use workspace::{ - item::{BreadcrumbText, ItemEvent, TabContentParams}, - searchable::SearchableItemHandle, - Item, ItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace, -}; - -pub struct AssistantDiff { - multibuffer: Entity, - editor: Entity, - thread: Entity, - focus_handle: FocusHandle, - workspace: WeakEntity, - title: SharedString, - _subscriptions: Vec, -} - -impl AssistantDiff { - pub fn deploy( - thread: Entity, - workspace: WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Result<()> { - let existing_diff = workspace.update(cx, |workspace, cx| { - workspace - .items_of_type::(cx) - .find(|diff| diff.read(cx).thread == thread) - })?; - if let Some(existing_diff) = existing_diff { - workspace.update(cx, |workspace, cx| { - workspace.activate_item(&existing_diff, true, true, window, cx); - }) - } else { - let assistant_diff = - cx.new(|cx| AssistantDiff::new(thread.clone(), workspace.clone(), window, cx)); - workspace.update(cx, |workspace, cx| { - workspace.add_item_to_center(Box::new(assistant_diff), window, cx); - }) - } - } - - pub fn new( - thread: Entity, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - let project = thread.read(cx).project().clone(); - let render_diff_hunk_controls = Arc::new({ - let assistant_diff = cx.entity(); - move |row, - status: &DiffHunkStatus, - hunk_range, - is_created_file, - line_height, - _editor: &Entity, - cx: &mut App| { - render_diff_hunk_controls( - row, - status, - hunk_range, - is_created_file, - line_height, - &assistant_diff, - cx, - ) - } - }); - let editor = cx.new(|cx| { - let mut editor = - Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); - editor.disable_inline_diagnostics(); - editor.set_expand_all_diff_hunks(cx); - editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx); - editor - }); - - let action_log = thread.read(cx).action_log().clone(); - let mut this = Self { - _subscriptions: vec![ - cx.observe_in(&action_log, window, |this, _action_log, window, cx| { - this.update_excerpts(window, cx) - }), - cx.subscribe(&thread, |this, _thread, event, cx| { - this.handle_thread_event(event, cx) - }), - ], - title: SharedString::default(), - multibuffer, - editor, - thread, - focus_handle, - workspace, - }; - this.update_excerpts(window, cx); - this.update_title(cx); - this - } - - fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context) { - let thread = self.thread.read(cx); - let unreviewed_buffers = thread.action_log().read(cx).unreviewed_buffers(); - let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::>(); - - for (buffer, tracked) in unreviewed_buffers { - let Some(file) = buffer.read(cx).file().cloned() else { - continue; - }; - - let path_key = PathKey::namespaced("", file.full_path(cx).into()); - paths_to_delete.remove(&path_key); - - let snapshot = buffer.read(cx).snapshot(); - let diff = tracked.diff().read(cx); - let diff_hunk_ranges = diff - .hunks_intersecting_range( - language::Anchor::MIN..language::Anchor::MAX, - &snapshot, - cx, - ) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) - .collect::>(); - - let was_empty = self.multibuffer.update(cx, |multibuffer, cx| { - let was_empty = multibuffer.is_empty(); - multibuffer.set_excerpts_for_path( - path_key.clone(), - buffer, - diff_hunk_ranges, - editor::DEFAULT_MULTIBUFFER_CONTEXT, - cx, - ); - multibuffer.add_diff(tracked.diff().clone(), cx); - was_empty - }); - - self.editor.update(cx, |editor, cx| { - if was_empty { - editor.change_selections(None, window, cx, |selections| { - // TODO select the very beginning (possibly inside a deletion) - selections.select_ranges([0..0]) - }); - } - }); - } - - self.multibuffer.update(cx, |multibuffer, cx| { - for path in paths_to_delete { - multibuffer.remove_excerpts_for_path(path, cx); - } - }); - - if self.multibuffer.read(cx).is_empty() - && self - .editor - .read(cx) - .focus_handle(cx) - .contains_focused(window, cx) - { - self.focus_handle.focus(window); - } else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() { - self.editor.update(cx, |editor, cx| { - editor.focus_handle(cx).focus(window); - }); - } - } - - fn update_title(&mut self, cx: &mut Context) { - let new_title = self - .thread - .read(cx) - .summary() - .unwrap_or("Assistant Changes".into()); - if new_title != self.title { - self.title = new_title; - cx.emit(EditorEvent::TitleChanged); - } - } - - fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context) { - match event { - ThreadEvent::SummaryChanged => self.update_title(cx), - _ => {} - } - } - - fn review_diff_hunks( - &mut self, - hunk_ranges: Vec>, - accept: bool, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.multibuffer.read(cx).snapshot(cx); - let diff_hunks_in_ranges = self - .editor - .read(cx) - .diff_hunks_in_ranges(&hunk_ranges, &snapshot) - .collect::>(); - - let mut tasks = Vec::new(); - for hunk in diff_hunks_in_ranges { - let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id); - if let Some(buffer) = buffer { - let task = self.thread.update(cx, |thread, cx| { - thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx) - }); - tasks.push(task.log_err()); - } - } - - cx.spawn_in(window, async move |this, cx| { - future::join_all(tasks).await; - this.update_in(cx, |this, window, cx| this.update_excerpts(window, cx)) - }) - .detach_and_log_err(cx); - } -} - -impl EventEmitter for AssistantDiff {} - -impl Focusable for AssistantDiff { - fn focus_handle(&self, cx: &App) -> FocusHandle { - if self.multibuffer.read(cx).is_empty() { - self.focus_handle.clone() - } else { - self.editor.focus_handle(cx) - } - } -} - -impl Item for AssistantDiff { - type Event = EditorEvent; - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(Icon::new(IconName::ZedAssistant).color(Color::Muted)) - } - - fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { - Editor::to_item_events(event, f) - } - - fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { - self.editor - .update(cx, |editor, cx| editor.deactivated(window, cx)); - } - - fn navigate( - &mut self, - data: Box, - window: &mut Window, - cx: &mut Context, - ) -> bool { - self.editor - .update(cx, |editor, cx| editor.navigate(data, window, cx)) - } - - fn tab_tooltip_text(&self, _: &App) -> Option { - Some("Project Diff".into()) - } - - fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { - let summary = self - .thread - .read(cx) - .summary() - .unwrap_or("Assistant Changes".into()); - Label::new(format!("Review: {}", summary)) - .color(if params.selected { - Color::Default - } else { - Color::Muted - }) - .into_any_element() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Project Diff Opened") - } - - fn as_searchable(&self, _: &Entity) -> Option> { - Some(Box::new(self.editor.clone())) - } - - fn for_each_project_item( - &self, - cx: &App, - f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), - ) { - self.editor.for_each_project_item(cx, f) - } - - fn is_singleton(&self, _: &App) -> bool { - false - } - - fn set_nav_history( - &mut self, - nav_history: ItemNavHistory, - _: &mut Window, - cx: &mut Context, - ) { - self.editor.update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }); - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Option> - where - Self: Sized, - { - Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx))) - } - - fn is_dirty(&self, cx: &App) -> bool { - self.multibuffer.read(cx).is_dirty(cx) - } - - fn has_conflict(&self, cx: &App) -> bool { - self.multibuffer.read(cx).has_conflict(cx) - } - - fn can_save(&self, _: &App) -> bool { - true - } - - fn save( - &mut self, - format: bool, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.editor.save(format, project, window, cx) - } - - fn save_as( - &mut self, - _: Entity, - _: ProjectPath, - _window: &mut Window, - _: &mut Context, - ) -> Task> { - unreachable!() - } - - fn reload( - &mut self, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.editor.reload(project, window, cx) - } - - fn act_as_type<'a>( - &'a self, - type_id: TypeId, - self_handle: &'a Entity, - _: &'a App, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.to_any()) - } else if type_id == TypeId::of::() { - Some(self.editor.to_any()) - } else { - None - } - } - - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.editor.breadcrumbs(theme, cx) - } - - fn added_to_workspace( - &mut self, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) { - self.editor.update(cx, |editor, cx| { - editor.added_to_workspace(workspace, window, cx) - }); - } -} - -impl Render for AssistantDiff { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let is_empty = self.multibuffer.read(cx).is_empty(); - div() - .track_focus(&self.focus_handle) - .key_context(if is_empty { - "EmptyPane" - } else { - "AssistantDiff" - }) - .bg(cx.theme().colors().editor_background) - .flex() - .items_center() - .justify_center() - .size_full() - .when(is_empty, |el| el.child("No changes to review")) - .when(!is_empty, |el| el.child(self.editor.clone())) - } -} - -fn render_diff_hunk_controls( - row: u32, - status: &DiffHunkStatus, - hunk_range: Range, - is_created_file: bool, - line_height: Pixels, - assistant_diff: &Entity, - cx: &mut App, -) -> AnyElement { - let editor = assistant_diff.read(cx).editor.clone(); - h_flex() - .h(line_height) - .mr_1() - .gap_1() - .px_0p5() - .pb_1() - .border_x_1() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .rounded_b_lg() - .bg(cx.theme().colors().editor_background) - .gap_1() - .occlude() - .shadow_md() - .children(if status.has_secondary_hunk() { - vec![ - Button::new(("stage", row as u64), "Accept") - .alpha(if status.is_pending() { 0.66 } else { 1.0 }) - // TODO: add tooltip - // .tooltip({ - // let focus_handle = editor.focus_handle(cx); - // move |window, cx| { - // Tooltip::for_action_in( - // "Stage Hunk", - // &::git::ToggleStaged, - // &focus_handle, - // window, - // cx, - // ) - // } - // }) - .on_click({ - let assistant_diff = assistant_diff.clone(); - move |_event, window, cx| { - assistant_diff.update(cx, |diff, cx| { - diff.review_diff_hunks( - vec![hunk_range.start..hunk_range.start], - true, - window, - cx, - ); - }); - } - }), - Button::new("undo", "Undo") - // TODO: add tooltip - // .tooltip({ - // let focus_handle = editor.focus_handle(cx); - // move |window, cx| { - // Tooltip::for_action_in("Undo Hunk", &::git::Undo, &focus_handle, window, cx) - // } - // }) - .on_click({ - let _editor = editor.clone(); - move |_event, _window, _cx| { - // editor.update(cx, |editor, cx| { - // let snapshot = editor.snapshot(window, cx); - // let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); - // editor.undo_hunks_in_ranges(vec![point..point], window, cx); - // }); - } - }) - .disabled(is_created_file), - ] - } else { - vec![Button::new(("review", row as u64), "Review") - .alpha(if status.is_pending() { 0.66 } else { 1.0 }) - // TODO: add tooltip - // .tooltip({ - // let focus_handle = editor.focus_handle(cx); - // move |window, cx| { - // Tooltip::for_action_in( - // "Review", - // &::git::ToggleStaged, - // &focus_handle, - // window, - // cx, - // ) - // } - // }) - .on_click({ - let assistant_diff = assistant_diff.clone(); - move |_event, window, cx| { - assistant_diff.update(cx, |diff, cx| { - diff.review_diff_hunks( - vec![hunk_range.start..hunk_range.start], - false, - window, - cx, - ); - }); - } - })] - }) - .when( - !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(), - |el| { - el.child( - IconButton::new(("next-hunk", row as u64), IconName::ArrowDown) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - // .disabled(!has_multiple_hunks) - // TODO: add tooltip - // .tooltip({ - // let focus_handle = editor.focus_handle(cx); - // move |window, cx| { - // Tooltip::for_action_in( - // "Next Hunk", - // &GoToHunk, - // &focus_handle, - // window, - // cx, - // ) - // } - // }) - .on_click({ - let _editor = editor.clone(); - move |_event, _window, _cx| { - // TODO: wire this up - // editor.update(cx, |editor, cx| { - // let snapshot = editor.snapshot(window, cx); - // let position = - // hunk_range.end.to_point(&snapshot.buffer_snapshot); - // editor.go_to_hunk_before_or_after_position( - // &snapshot, - // position, - // Direction::Next, - // window, - // cx, - // ); - // editor.expand_selected_diff_hunks(cx); - // }); - } - }), - ) - .child( - IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - // .disabled(!has_multiple_hunks) - // TODO: add tooltip - // .tooltip({ - // let focus_handle = editor.focus_handle(cx); - // move |window, cx| { - // Tooltip::for_action_in( - // "Previous Hunk", - // &GoToPreviousHunk, - // &focus_handle, - // window, - // cx, - // ) - // } - // }) - .on_click({ - let _editor = editor.clone(); - move |_event, _window, _cx| { - // TODO: wire this up - // editor.update(cx, |editor, cx| { - // let snapshot = editor.snapshot(window, cx); - // let point = - // hunk_range.start.to_point(&snapshot.buffer_snapshot); - // editor.go_to_hunk_before_or_after_position( - // &snapshot, - // point, - // Direction::Prev, - // window, - // cx, - // ); - // editor.expand_selected_diff_hunks(cx); - // }); - } - }), - ) - }, - ) - .into_any_element() -} diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 0ffe75e205..a095e8bb3e 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -20,7 +20,6 @@ use ui::{ prelude::*, ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip, }; -use util::ResultExt; use vim_mode_setting::VimModeSetting; use workspace::notifications::{NotificationId, NotifyTaskExt}; use workspace::{Toast, Workspace}; @@ -32,7 +31,7 @@ use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; use crate::thread::{RequestKind, Thread}; use crate::thread_store::ThreadStore; use crate::tool_selector::ToolSelector; -use crate::{AssistantDiff, Chat, ChatMode, RemoveAllContext, ToggleContextPicker}; +use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker}; pub struct MessageEditor { thread: Entity, @@ -314,10 +313,6 @@ impl MessageEditor { }) .detach_and_notify_err(window, cx); } - - fn handle_review_click(&self, window: &mut Window, cx: &mut Context) { - AssistantDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err(); - } } impl Focusable for MessageEditor { @@ -352,9 +347,8 @@ impl Render for MessageEditor { px(64.) }; - let action_log = self.thread.read(cx).action_log(); - let unreviewed_buffers = action_log.read(cx).unreviewed_buffers(); - let unreviewed_buffers_count = unreviewed_buffers.len(); + let changed_buffers = self.thread.read(cx).scripting_changed_buffers(cx); + let changed_buffers_count = changed_buffers.len(); v_flex() .size_full() @@ -416,7 +410,7 @@ impl Render for MessageEditor { ), ) }) - .when(unreviewed_buffers_count > 0, |parent| { + .when(changed_buffers_count > 0, |parent| { parent.child( v_flex() .mx_2() @@ -427,130 +421,93 @@ impl Render for MessageEditor { .rounded_t_md() .child( h_flex() + .gap_2() .p_2() - .justify_between() .child( - h_flex() - .gap_2() - .child( - Disclosure::new( - "edits-disclosure", - self.edits_expanded, - ) - .on_click( - cx.listener(|this, _ev, _window, cx| { - this.edits_expanded = !this.edits_expanded; - cx.notify(); - }), - ), - ) - .child( - Label::new("Edits") - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .child( - Label::new("•") - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .child( - Label::new(format!( - "{} {}", - unreviewed_buffers_count, - if unreviewed_buffers_count == 1 { - "file" - } else { - "files" - } - )) - .size(LabelSize::XSmall) - .color(Color::Muted), - ), + Disclosure::new("edits-disclosure", self.edits_expanded) + .on_click(cx.listener(|this, _ev, _window, cx| { + this.edits_expanded = !this.edits_expanded; + cx.notify(); + })), ) .child( - Button::new("review", "Review") - .label_size(LabelSize::XSmall) - .on_click(cx.listener(|this, _, window, cx| { - this.handle_review_click(window, cx) - })), + Label::new("Edits") + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + .child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted)) + .child( + Label::new(format!( + "{} {}", + changed_buffers_count, + if changed_buffers_count == 1 { + "file" + } else { + "files" + } + )) + .size(LabelSize::XSmall) + .color(Color::Muted), ), ) .when(self.edits_expanded, |parent| { parent.child( v_flex().bg(cx.theme().colors().editor_background).children( - unreviewed_buffers.into_iter().enumerate().flat_map( - |(index, (buffer, tracked))| { - let file = buffer.read(cx).file()?; - let path = file.path(); + changed_buffers.enumerate().flat_map(|(index, buffer)| { + let file = buffer.read(cx).file()?; + let path = file.path(); - let parent_label = path.parent().and_then(|parent| { - let parent_str = parent.to_string_lossy(); + let parent_label = path.parent().and_then(|parent| { + let parent_str = parent.to_string_lossy(); - if parent_str.is_empty() { - None - } else { - Some( - Label::new(format!( - "{}{}", - parent_str, - std::path::MAIN_SEPARATOR_STR - )) - .color(Color::Muted) - .size(LabelSize::Small), - ) - } - }); - - let name_label = path.file_name().map(|name| { - Label::new(name.to_string_lossy().to_string()) - .size(LabelSize::Small) - }); - - let file_icon = FileIcons::get_icon(&path, cx) - .map(Icon::from_path) - .unwrap_or_else(|| Icon::new(IconName::File)); - - let element = div() - .p_2() - .when( - index + 1 < unreviewed_buffers_count, - |parent| { - parent - .border_color( - cx.theme().colors().border, - ) - .border_b_1() - }, + if parent_str.is_empty() { + None + } else { + Some( + Label::new(format!( + "{}{}", + parent_str, + std::path::MAIN_SEPARATOR_STR + )) + .color(Color::Muted) + .size(LabelSize::Small), ) - .child( - h_flex() - .gap_2() - .child(file_icon) - .child( - // TODO: handle overflow - h_flex() - .children(parent_label) - .children(name_label), - ) - // TODO: show lines changed - .child( - Label::new("+").color(Color::Created), - ) - .child( - Label::new("-").color(Color::Deleted), - ) - .when(!tracked.needs_review(), |parent| { - parent.child( - Icon::new(IconName::Check) - .color(Color::Success), - ) - }), - ); + } + }); - Some(element) - }, - ), + let name_label = path.file_name().map(|name| { + Label::new(name.to_string_lossy().to_string()) + .size(LabelSize::Small) + }); + + let file_icon = FileIcons::get_icon(&path, cx) + .map(Icon::from_path) + .unwrap_or_else(|| Icon::new(IconName::File)); + + let element = div() + .p_2() + .when(index + 1 < changed_buffers_count, |parent| { + parent + .border_color(cx.theme().colors().border) + .border_b_1() + }) + .child( + h_flex() + .gap_2() + .child(file_icon) + .child( + // TODO: handle overflow + h_flex() + .children(parent_label) + .children(name_label), + ) + // TODO: show lines changed + .child(Label::new("+").color(Color::Created)) + .child(Label::new("-").color(Color::Deleted)), + ); + + Some(element) + }), ), ) }), diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 7f7cda3951..a89dba5660 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -1,6 +1,5 @@ use std::fmt::Write as _; use std::io::Write; -use std::ops::Range; use std::sync::Arc; use anyhow::{Context as _, Result}; @@ -976,10 +975,6 @@ impl Thread { }) } - pub fn project(&self) -> &Entity { - &self.project - } - /// Create a snapshot of the current project state including git information and unsaved buffers. fn project_snapshot( project: Entity, @@ -1128,18 +1123,6 @@ impl Thread { Ok(String::from_utf8_lossy(&markdown).to_string()) } - pub fn review_edits_in_range( - &mut self, - buffer: Entity, - buffer_range: Range, - accept: bool, - cx: &mut Context, - ) -> Task> { - self.action_log.update(cx, |action_log, cx| { - action_log.review_edits_in_range(buffer, buffer_range, accept, cx) - }) - } - pub fn action_log(&self) -> &Entity { &self.action_log } diff --git a/crates/assistant_tool/Cargo.toml b/crates/assistant_tool/Cargo.toml index b091c8ca72..040a906bf3 100644 --- a/crates/assistant_tool/Cargo.toml +++ b/crates/assistant_tool/Cargo.toml @@ -13,7 +13,6 @@ path = "src/assistant_tool.rs" [dependencies] anyhow.workspace = true -buffer_diff.workspace = true collections.workspace = true clock.workspace = true derive_more.workspace = true @@ -24,12 +23,3 @@ parking_lot.workspace = true project.workspace = true serde.workspace = true serde_json.workspace = true - -[dev-dependencies] -buffer_diff = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -clock = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -language_model = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } diff --git a/crates/assistant_tool/src/action_log.rs b/crates/assistant_tool/src/action_log.rs deleted file mode 100644 index 362b211d29..0000000000 --- a/crates/assistant_tool/src/action_log.rs +++ /dev/null @@ -1,398 +0,0 @@ -use anyhow::{anyhow, Result}; -use buffer_diff::BufferDiff; -use collections::{BTreeMap, HashMap, HashSet}; -use gpui::{App, AppContext, Context, Entity, Task}; -use language::{Buffer, OffsetRangeExt, ToOffset}; -use std::{future::Future, ops::Range}; - -/// Tracks actions performed by tools in a thread -#[derive(Debug)] -pub struct ActionLog { - /// Buffers that user manually added to the context, and whose content has - /// changed since the model last saw them. - stale_buffers_in_context: HashSet>, - /// Buffers that we want to notify the model about when they change. - tracked_buffers: BTreeMap, TrackedBuffer>, -} - -#[derive(Debug, Clone)] -pub struct TrackedBuffer { - buffer: Entity, - unreviewed_edit_ids: Vec, - accepted_edit_ids: Vec, - version: clock::Global, - diff: Entity, - secondary_diff: Entity, -} - -impl TrackedBuffer { - pub fn needs_review(&self) -> bool { - !self.unreviewed_edit_ids.is_empty() - } - - pub fn diff(&self) -> &Entity { - &self.diff - } - - fn update_diff(&mut self, cx: &mut App) -> impl 'static + Future { - let edits_to_undo = self - .unreviewed_edit_ids - .iter() - .chain(&self.accepted_edit_ids) - .map(|edit_id| (*edit_id, u32::MAX)) - .collect::>(); - let buffer_without_edits = self.buffer.update(cx, |buffer, cx| buffer.branch(cx)); - buffer_without_edits.update(cx, |buffer, cx| { - buffer.undo_operations(edits_to_undo, cx); - }); - let primary_diff_update = self.diff.update(cx, |diff, cx| { - diff.set_base_text( - buffer_without_edits, - self.buffer.read(cx).text_snapshot(), - cx, - ) - }); - - let unreviewed_edits_to_undo = self - .unreviewed_edit_ids - .iter() - .map(|edit_id| (*edit_id, u32::MAX)) - .collect::>(); - let buffer_without_unreviewed_edits = - self.buffer.update(cx, |buffer, cx| buffer.branch(cx)); - buffer_without_unreviewed_edits.update(cx, |buffer, cx| { - buffer.undo_operations(unreviewed_edits_to_undo, cx); - }); - let secondary_diff_update = self.secondary_diff.update(cx, |diff, cx| { - diff.set_base_text( - buffer_without_unreviewed_edits.clone(), - self.buffer.read(cx).text_snapshot(), - cx, - ) - }); - - async move { - _ = primary_diff_update.await; - _ = secondary_diff_update.await; - } - } -} - -impl ActionLog { - /// Creates a new, empty action log. - pub fn new() -> Self { - Self { - stale_buffers_in_context: HashSet::default(), - tracked_buffers: BTreeMap::default(), - } - } - - fn track_buffer( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> &mut TrackedBuffer { - let tracked_buffer = self - .tracked_buffers - .entry(buffer.clone()) - .or_insert_with(|| { - let text_snapshot = buffer.read(cx).text_snapshot(); - let unreviewed_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); - let diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&text_snapshot, cx); - diff.set_secondary_diff(unreviewed_diff.clone()); - diff - }); - TrackedBuffer { - buffer: buffer.clone(), - unreviewed_edit_ids: Vec::new(), - accepted_edit_ids: Vec::new(), - version: buffer.read(cx).version(), - diff, - secondary_diff: unreviewed_diff, - } - }); - tracked_buffer.version = buffer.read(cx).version(); - tracked_buffer - } - - /// Track a buffer as read, so we can notify the model about user edits. - pub fn buffer_read(&mut self, buffer: Entity, cx: &mut Context) { - self.track_buffer(buffer, cx); - } - - /// Mark a buffer as edited, so we can refresh it in the context - pub fn buffer_edited( - &mut self, - buffer: Entity, - edit_ids: Vec, - cx: &mut Context, - ) -> Task> { - self.stale_buffers_in_context.insert(buffer.clone()); - - let tracked_buffer = self.track_buffer(buffer.clone(), cx); - tracked_buffer - .unreviewed_edit_ids - .extend(edit_ids.iter().copied()); - let update = tracked_buffer.update_diff(cx); - cx.spawn(async move |this, cx| { - update.await; - this.update(cx, |_this, cx| cx.notify())?; - Ok(()) - }) - } - - /// Accepts edits in a given range within a buffer. - pub fn review_edits_in_range( - &mut self, - buffer: Entity, - buffer_range: Range, - accept: bool, - cx: &mut Context, - ) -> Task> { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return Task::ready(Err(anyhow!("buffer not found"))); - }; - - let buffer = buffer.read(cx); - let buffer_range = buffer_range.to_offset(buffer); - - let source; - let destination; - if accept { - source = &mut tracked_buffer.unreviewed_edit_ids; - destination = &mut tracked_buffer.accepted_edit_ids; - } else { - source = &mut tracked_buffer.accepted_edit_ids; - destination = &mut tracked_buffer.unreviewed_edit_ids; - } - - source.retain(|edit_id| { - for range in buffer.edited_ranges_for_edit_ids::([edit_id]) { - if buffer_range.end >= range.start && buffer_range.start <= range.end { - destination.push(*edit_id); - return false; - } - } - true - }); - - let update = tracked_buffer.update_diff(cx); - cx.spawn(async move |this, cx| { - update.await; - this.update(cx, |_this, cx| cx.notify())?; - Ok(()) - }) - } - - /// Returns the set of buffers that contain changes that haven't been reviewed by the user. - pub fn unreviewed_buffers(&self) -> BTreeMap, TrackedBuffer> { - self.tracked_buffers - .iter() - .map(|(buffer, tracked)| (buffer.clone(), tracked.clone())) - .collect() - } - - /// Iterate over buffers changed since last read or edited by the model - pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator> { - self.tracked_buffers - .iter() - .filter(|(buffer, tracked)| tracked.version != buffer.read(cx).version) - .map(|(buffer, _)| buffer) - } - - /// Takes and returns the set of buffers pending refresh, clearing internal state. - pub fn take_stale_buffers_in_context(&mut self) -> HashSet> { - std::mem::take(&mut self.stale_buffers_in_context) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use buffer_diff::DiffHunkStatusKind; - use gpui::TestAppContext; - use language::Point; - - #[gpui::test] - async fn test_edit_review(cx: &mut TestAppContext) { - let action_log = cx.new(|_| ActionLog::new()); - let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx)); - - let edit1 = buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx) - .unwrap() - }); - let edit2 = buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx) - .unwrap() - }); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndEf\nghi\njkl\nmnO" - ); - - action_log - .update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), vec![edit1, edit2], cx) - }) - .await - .unwrap(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - review_status: ReviewStatus::Unreviewed, - diff_status: DiffHunkStatusKind::Modified, - }, - HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - review_status: ReviewStatus::Unreviewed, - diff_status: DiffHunkStatusKind::Modified, - } - ], - )] - ); - - action_log - .update(cx, |log, cx| { - log.review_edits_in_range( - buffer.clone(), - Point::new(3, 0)..Point::new(4, 3), - true, - cx, - ) - }) - .await - .unwrap(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - review_status: ReviewStatus::Unreviewed, - diff_status: DiffHunkStatusKind::Modified, - }, - HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - review_status: ReviewStatus::Reviewed, - diff_status: DiffHunkStatusKind::Modified, - } - ], - )] - ); - - action_log - .update(cx, |log, cx| { - log.review_edits_in_range( - buffer.clone(), - Point::new(3, 0)..Point::new(4, 3), - false, - cx, - ) - }) - .await - .unwrap(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - review_status: ReviewStatus::Unreviewed, - diff_status: DiffHunkStatusKind::Modified, - }, - HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - review_status: ReviewStatus::Unreviewed, - diff_status: DiffHunkStatusKind::Modified, - } - ], - )] - ); - - action_log - .update(cx, |log, cx| { - log.review_edits_in_range( - buffer.clone(), - Point::new(0, 0)..Point::new(4, 3), - true, - cx, - ) - }) - .await - .unwrap(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - review_status: ReviewStatus::Reviewed, - diff_status: DiffHunkStatusKind::Modified, - }, - HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - review_status: ReviewStatus::Reviewed, - diff_status: DiffHunkStatusKind::Modified, - } - ], - )] - ); - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct HunkStatus { - range: Range, - review_status: ReviewStatus, - diff_status: DiffHunkStatusKind, - } - - #[derive(Copy, Clone, Debug, PartialEq, Eq)] - enum ReviewStatus { - Unreviewed, - Reviewed, - } - - fn unreviewed_hunks( - action_log: &Entity, - cx: &TestAppContext, - ) -> Vec<(Entity, Vec)> { - cx.read(|cx| { - action_log - .read(cx) - .unreviewed_buffers() - .into_iter() - .map(|(buffer, tracked_buffer)| { - let snapshot = buffer.read(cx).snapshot(); - ( - buffer, - tracked_buffer - .diff - .read(cx) - .hunks(&snapshot, cx) - .map(|hunk| HunkStatus { - review_status: if hunk.status().has_secondary_hunk() { - ReviewStatus::Unreviewed - } else { - ReviewStatus::Reviewed - }, - diff_status: hunk.status().kind, - range: hunk.range, - }) - .collect(), - ) - }) - .collect() - }) - } -} diff --git a/crates/assistant_tool/src/assistant_tool.rs b/crates/assistant_tool/src/assistant_tool.rs index 875479d8b6..22564bc37f 100644 --- a/crates/assistant_tool/src/assistant_tool.rs +++ b/crates/assistant_tool/src/assistant_tool.rs @@ -1,14 +1,16 @@ -mod action_log; mod tool_registry; mod tool_working_set; -use anyhow::Result; -use gpui::{App, Entity, SharedString, Task}; -use language_model::LanguageModelRequestMessage; -use project::Project; use std::sync::Arc; -pub use crate::action_log::*; +use anyhow::Result; +use collections::{HashMap, HashSet}; +use gpui::Context; +use gpui::{App, Entity, SharedString, Task}; +use language::Buffer; +use language_model::LanguageModelRequestMessage; +use project::Project; + pub use crate::tool_registry::*; pub use crate::tool_working_set::*; @@ -52,3 +54,57 @@ pub trait Tool: 'static + Send + Sync { cx: &mut App, ) -> Task>; } + +/// Tracks actions performed by tools in a thread +#[derive(Debug)] +pub struct ActionLog { + /// Buffers that user manually added to the context, and whose content has + /// changed since the model last saw them. + stale_buffers_in_context: HashSet>, + /// Buffers that we want to notify the model about when they change. + tracked_buffers: HashMap, TrackedBuffer>, +} + +#[derive(Debug, Default)] +struct TrackedBuffer { + version: clock::Global, +} + +impl ActionLog { + /// Creates a new, empty action log. + pub fn new() -> Self { + Self { + stale_buffers_in_context: HashSet::default(), + tracked_buffers: HashMap::default(), + } + } + + /// Track a buffer as read, so we can notify the model about user edits. + pub fn buffer_read(&mut self, buffer: Entity, cx: &mut Context) { + let tracked_buffer = self.tracked_buffers.entry(buffer.clone()).or_default(); + tracked_buffer.version = buffer.read(cx).version(); + } + + /// Mark a buffer as edited, so we can refresh it in the context + pub fn buffer_edited(&mut self, buffers: HashSet>, cx: &mut Context) { + for buffer in &buffers { + let tracked_buffer = self.tracked_buffers.entry(buffer.clone()).or_default(); + tracked_buffer.version = buffer.read(cx).version(); + } + + self.stale_buffers_in_context.extend(buffers); + } + + /// Iterate over buffers changed since last read or edited by the model + pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator> { + self.tracked_buffers + .iter() + .filter(|(buffer, tracked)| tracked.version != buffer.read(cx).version) + .map(|(buffer, _)| buffer) + } + + /// Takes and returns the set of buffers pending refresh, clearing internal state. + pub fn take_stale_buffers_in_context(&mut self) -> HashSet> { + std::mem::take(&mut self.stale_buffers_in_context) + } +} diff --git a/crates/assistant_tools/src/edit_files_tool.rs b/crates/assistant_tools/src/edit_files_tool.rs index f4bf394f30..be0beb2d52 100644 --- a/crates/assistant_tools/src/edit_files_tool.rs +++ b/crates/assistant_tools/src/edit_files_tool.rs @@ -274,17 +274,7 @@ impl EditToolRequest { self.bad_searches.push(invalid_replace); } DiffResult::Diff(diff) => { - let edit_ids = buffer.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.apply_diff(diff, cx); - let transaction = buffer.finalize_last_transaction(); - transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone()) - })?; - self.action_log - .update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), edit_ids, cx) - })? - .await?; + let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?; write!(&mut self.output, "\n\n{}", source)?; self.changed_buffers.insert(buffer); @@ -332,6 +322,10 @@ impl EditToolRequest { .await?; } + self.action_log + .update(cx, |log, cx| log.buffer_edited(self.changed_buffers, cx)) + .log_err(); + let errors = self.parser.errors(); if errors.is_empty() && self.bad_searches.is_empty() { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6f247d2768..786d6c60d7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -182,8 +182,8 @@ use theme::{ ThemeColors, ThemeSettings, }; use ui::{ - h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconButtonShape, IconName, - IconSize, Key, Tooltip, + h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize, Key, + Tooltip, }; use util::{maybe, post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ @@ -221,18 +221,6 @@ pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction"; pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict"; pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4; -pub type RenderDiffHunkControlsFn = Arc< - dyn Fn( - u32, - &DiffHunkStatus, - Range, - bool, - Pixels, - &Entity, - &mut App, - ) -> AnyElement, ->; - const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers { alt: true, shift: true, @@ -752,7 +740,6 @@ pub struct Editor { show_git_blame_inline_delay_task: Option>, git_blame_inline_tooltip: Option>, git_blame_inline_enabled: bool, - render_diff_hunk_controls: RenderDiffHunkControlsFn, serialize_dirty_buffers: bool, show_selection_menu: Option, blame: Option>, @@ -1487,7 +1474,6 @@ impl Editor { show_git_blame_inline_delay_task: None, git_blame_inline_tooltip: None, git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), - render_diff_hunk_controls: Arc::new(render_diff_hunk_controls), serialize_dirty_buffers: ProjectSettings::get_global(cx) .session .restore_unsaved_buffers, @@ -14491,15 +14477,6 @@ impl Editor { self.stage_or_unstage_diff_hunks(stage, ranges, cx); } - pub fn set_render_diff_hunk_controls( - &mut self, - render_diff_hunk_controls: RenderDiffHunkControlsFn, - cx: &mut Context, - ) { - self.render_diff_hunk_controls = render_diff_hunk_controls; - cx.notify(); - } - pub fn stage_and_next( &mut self, _: &::git::StageAndNext, @@ -19588,187 +19565,3 @@ impl From for LineHighlight { } } } - -fn render_diff_hunk_controls( - row: u32, - status: &DiffHunkStatus, - hunk_range: Range, - is_created_file: bool, - line_height: Pixels, - editor: &Entity, - cx: &mut App, -) -> AnyElement { - h_flex() - .h(line_height) - .mr_1() - .gap_1() - .px_0p5() - .pb_1() - .border_x_1() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .rounded_b_lg() - .bg(cx.theme().colors().editor_background) - .gap_1() - .occlude() - .shadow_md() - .child(if status.has_secondary_hunk() { - Button::new(("stage", row as u64), "Stage") - .alpha(if status.is_pending() { 0.66 } else { 1.0 }) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Stage Hunk", - &::git::ToggleStaged, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, _window, cx| { - editor.update(cx, |editor, cx| { - editor.stage_or_unstage_diff_hunks( - true, - vec![hunk_range.start..hunk_range.start], - cx, - ); - }); - } - }) - } else { - Button::new(("unstage", row as u64), "Unstage") - .alpha(if status.is_pending() { 0.66 } else { 1.0 }) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Unstage Hunk", - &::git::ToggleStaged, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, _window, cx| { - editor.update(cx, |editor, cx| { - editor.stage_or_unstage_diff_hunks( - false, - vec![hunk_range.start..hunk_range.start], - cx, - ); - }); - } - }) - }) - .child( - Button::new("restore", "Restore") - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Restore Hunk", - &::git::Restore, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); - editor.restore_hunks_in_ranges(vec![point..point], window, cx); - }); - } - }) - .disabled(is_created_file), - ) - .when( - !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(), - |el| { - el.child( - IconButton::new(("next-hunk", row as u64), IconName::ArrowDown) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - // .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Next Hunk", - &GoToHunk, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let position = - hunk_range.end.to_point(&snapshot.buffer_snapshot); - editor.go_to_hunk_before_or_after_position( - &snapshot, - position, - Direction::Next, - window, - cx, - ); - editor.expand_selected_diff_hunks(cx); - }); - } - }), - ) - .child( - IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - // .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Previous Hunk", - &GoToPreviousHunk, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let point = - hunk_range.start.to_point(&snapshot.buffer_snapshot); - editor.go_to_hunk_before_or_after_position( - &snapshot, - point, - Direction::Prev, - window, - cx, - ); - editor.expand_selected_diff_hunks(cx); - }); - } - }), - ) - }, - ) - .into_any_element() -} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f122aaa9d8..5947b48ba9 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -18,12 +18,12 @@ use crate::{ scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair}, BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode, - EditorSettings, EditorSnapshot, EditorStyle, FocusedBlock, GutterDimensions, HalfPageDown, - HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, - LineDown, LineHighlight, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, - SelectPhase, SelectedTextHighlight, Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, - ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, - GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, + EditorSettings, EditorSnapshot, EditorStyle, FocusedBlock, GoToHunk, GoToPreviousHunk, + GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason, + InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, OpenExcerpts, PageDown, PageUp, + Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap, + StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, + FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; @@ -42,6 +42,7 @@ use gpui::{ ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, Window, }; +use inline_completion::Direction; use itertools::Itertools; use language::{ language_settings::{ @@ -74,7 +75,10 @@ use std::{ use sum_tree::Bias; use text::BufferId; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; -use ui::{h_flex, prelude::*, ButtonLike, ContextMenu, KeyBinding, Tooltip, POPOVER_Y_PADDING}; +use ui::{ + h_flex, prelude::*, ButtonLike, ContextMenu, IconButtonShape, KeyBinding, Tooltip, + POPOVER_Y_PADDING, +}; use unicode_segmentation::UnicodeSegmentation; use util::{debug_panic, RangeExt, ResultExt}; use workspace::{item::Item, notifications::NotifyTaskExt}; @@ -3982,7 +3986,6 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> Vec { - let render_diff_hunk_controls = editor.read(cx).render_diff_hunk_controls.clone(); let point_for_position = position_map.point_for_position(window.mouse_position()); let mut controls = vec![]; @@ -4025,7 +4028,7 @@ impl EditorElement { + text_hitbox.bounds.top() - scroll_pixel_position.y; - let mut element = render_diff_hunk_controls( + let mut element = diff_hunk_controls( display_row_range.start.0, status, multi_buffer_range.clone(), @@ -8928,3 +8931,187 @@ mod tests { .collect() } } + +fn diff_hunk_controls( + row: u32, + status: &DiffHunkStatus, + hunk_range: Range, + is_created_file: bool, + line_height: Pixels, + editor: &Entity, + cx: &mut App, +) -> AnyElement { + h_flex() + .h(line_height) + .mr_1() + .gap_1() + .px_0p5() + .pb_1() + .border_x_1() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .rounded_b_lg() + .bg(cx.theme().colors().editor_background) + .gap_1() + .occlude() + .shadow_md() + .child(if status.has_secondary_hunk() { + Button::new(("stage", row as u64), "Stage") + .alpha(if status.is_pending() { 0.66 } else { 1.0 }) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Stage Hunk", + &::git::ToggleStaged, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, _window, cx| { + editor.update(cx, |editor, cx| { + editor.stage_or_unstage_diff_hunks( + true, + vec![hunk_range.start..hunk_range.start], + cx, + ); + }); + } + }) + } else { + Button::new(("unstage", row as u64), "Unstage") + .alpha(if status.is_pending() { 0.66 } else { 1.0 }) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Unstage Hunk", + &::git::ToggleStaged, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, _window, cx| { + editor.update(cx, |editor, cx| { + editor.stage_or_unstage_diff_hunks( + false, + vec![hunk_range.start..hunk_range.start], + cx, + ); + }); + } + }) + }) + .child( + Button::new("restore", "Restore") + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Restore Hunk", + &::git::Restore, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); + editor.restore_hunks_in_ranges(vec![point..point], window, cx); + }); + } + }) + .disabled(is_created_file), + ) + .when( + !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(), + |el| { + el.child( + IconButton::new(("next-hunk", row as u64), IconName::ArrowDown) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + // .disabled(!has_multiple_hunks) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Next Hunk", + &GoToHunk, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let position = + hunk_range.end.to_point(&snapshot.buffer_snapshot); + editor.go_to_hunk_before_or_after_position( + &snapshot, + position, + Direction::Next, + window, + cx, + ); + editor.expand_selected_diff_hunks(cx); + }); + } + }), + ) + .child( + IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + // .disabled(!has_multiple_hunks) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |window, cx| { + Tooltip::for_action_in( + "Previous Hunk", + &GoToPreviousHunk, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + move |_event, window, cx| { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(window, cx); + let point = + hunk_range.start.to_point(&snapshot.buffer_snapshot); + editor.go_to_hunk_before_or_after_position( + &snapshot, + point, + Direction::Prev, + window, + cx, + ); + editor.expand_selected_diff_hunks(cx); + }); + } + }), + ) + }, + ) + .into_any_element() +} diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 7f4d371d72..21fa3b3940 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1498,9 +1498,9 @@ impl Buffer { .flat_map(|transaction| self.edited_ranges_for_transaction(transaction)) } - pub fn edited_ranges_for_edit_ids<'a, D>( + pub fn edited_ranges_for_transaction<'a, D>( &'a self, - edit_ids: impl IntoIterator, + transaction: &'a Transaction, ) -> impl 'a + Iterator> where D: TextDimension, @@ -1508,7 +1508,7 @@ impl Buffer { // get fragment ranges let mut cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None); let offset_ranges = self - .fragment_ids_for_edits(edit_ids.into_iter()) + .fragment_ids_for_edits(transaction.edit_ids.iter()) .into_iter() .filter_map(move |fragment_id| { cursor.seek_forward(&Some(fragment_id), Bias::Left, &None); @@ -1547,16 +1547,6 @@ impl Buffer { }) } - pub fn edited_ranges_for_transaction<'a, D>( - &'a self, - transaction: &'a Transaction, - ) -> impl 'a + Iterator> - where - D: TextDimension, - { - self.edited_ranges_for_edit_ids(&transaction.edit_ids) - } - pub fn subscribe(&mut self) -> Subscription { self.subscriptions.subscribe() }