Revert "Start tracking edits performed by the agent" (#27077)
Reverts zed-industries/zed#27064
This commit is contained in:
parent
584a70ca5e
commit
3edf930007
13 changed files with 346 additions and 1425 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
thread: Entity<Thread>,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
title: SharedString,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl AssistantDiff {
|
||||
pub fn deploy(
|
||||
thread: Entity<Thread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Result<()> {
|
||||
let existing_diff = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.items_of_type::<AssistantDiff>(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<Thread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> 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<Editor>,
|
||||
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<Self>) {
|
||||
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::<HashSet<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
match event {
|
||||
ThreadEvent::SummaryChanged => self.update_title(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn review_diff_hunks(
|
||||
&mut self,
|
||||
hunk_ranges: Vec<Range<editor::Anchor>>,
|
||||
accept: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<EditorEvent> 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<Icon> {
|
||||
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>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Box<dyn Any>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
|
||||
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<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
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>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
});
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<Self>>
|
||||
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<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.save(format, project, window, cx)
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: Entity<Project>,
|
||||
_: ProjectPath,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.reload(project, window, cx)
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a Entity<Self>,
|
||||
_: &'a App,
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
|
||||
self.editor.breadcrumbs(theme, cx)
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
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<Self>) -> 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<editor::Anchor>,
|
||||
is_created_file: bool,
|
||||
line_height: Pixels,
|
||||
assistant_diff: &Entity<AssistantDiff>,
|
||||
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()
|
||||
}
|
|
@ -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<Thread>,
|
||||
|
@ -314,10 +313,6 @@ impl MessageEditor {
|
|||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
}
|
||||
|
||||
fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
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)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
|
|
|
@ -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<Project> {
|
||||
&self.project
|
||||
}
|
||||
|
||||
/// Create a snapshot of the current project state including git information and unsaved buffers.
|
||||
fn project_snapshot(
|
||||
project: Entity<Project>,
|
||||
|
@ -1128,18 +1123,6 @@ impl Thread {
|
|||
Ok(String::from_utf8_lossy(&markdown).to_string())
|
||||
}
|
||||
|
||||
pub fn review_edits_in_range(
|
||||
&mut self,
|
||||
buffer: Entity<language::Buffer>,
|
||||
buffer_range: Range<language::Anchor>,
|
||||
accept: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
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<ActionLog> {
|
||||
&self.action_log
|
||||
}
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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<Entity<Buffer>>,
|
||||
/// Buffers that we want to notify the model about when they change.
|
||||
tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TrackedBuffer {
|
||||
buffer: Entity<Buffer>,
|
||||
unreviewed_edit_ids: Vec<clock::Lamport>,
|
||||
accepted_edit_ids: Vec<clock::Lamport>,
|
||||
version: clock::Global,
|
||||
diff: Entity<BufferDiff>,
|
||||
secondary_diff: Entity<BufferDiff>,
|
||||
}
|
||||
|
||||
impl TrackedBuffer {
|
||||
pub fn needs_review(&self) -> bool {
|
||||
!self.unreviewed_edit_ids.is_empty()
|
||||
}
|
||||
|
||||
pub fn diff(&self) -> &Entity<BufferDiff> {
|
||||
&self.diff
|
||||
}
|
||||
|
||||
fn update_diff(&mut self, cx: &mut App) -> impl 'static + Future<Output = ()> {
|
||||
let edits_to_undo = self
|
||||
.unreviewed_edit_ids
|
||||
.iter()
|
||||
.chain(&self.accepted_edit_ids)
|
||||
.map(|edit_id| (*edit_id, u32::MAX))
|
||||
.collect::<HashMap<_, _>>();
|
||||
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::<HashMap<_, _>>();
|
||||
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<Buffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> &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<Buffer>, cx: &mut Context<Self>) {
|
||||
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<Buffer>,
|
||||
edit_ids: Vec<clock::Lamport>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
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<T: ToOffset>(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
buffer_range: Range<T>,
|
||||
accept: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
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::<usize>([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<Entity<Buffer>, 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<Item = &'a Entity<Buffer>> {
|
||||
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<Entity<Buffer>> {
|
||||
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<Point>,
|
||||
review_status: ReviewStatus,
|
||||
diff_status: DiffHunkStatusKind,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum ReviewStatus {
|
||||
Unreviewed,
|
||||
Reviewed,
|
||||
}
|
||||
|
||||
fn unreviewed_hunks(
|
||||
action_log: &Entity<ActionLog>,
|
||||
cx: &TestAppContext,
|
||||
) -> Vec<(Entity<Buffer>, Vec<HunkStatus>)> {
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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<Result<String>>;
|
||||
}
|
||||
|
||||
/// 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<Entity<Buffer>>,
|
||||
/// Buffers that we want to notify the model about when they change.
|
||||
tracked_buffers: HashMap<Entity<Buffer>, 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<Buffer>, cx: &mut Context<Self>) {
|
||||
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<Entity<Buffer>>, cx: &mut Context<Self>) {
|
||||
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<Item = &'a Entity<Buffer>> {
|
||||
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<Entity<Buffer>> {
|
||||
std::mem::take(&mut self.stale_buffers_in_context)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<Anchor>,
|
||||
bool,
|
||||
Pixels,
|
||||
&Entity<Editor>,
|
||||
&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<Task<()>>,
|
||||
git_blame_inline_tooltip: Option<WeakEntity<crate::commit_tooltip::CommitTooltip>>,
|
||||
git_blame_inline_enabled: bool,
|
||||
render_diff_hunk_controls: RenderDiffHunkControlsFn,
|
||||
serialize_dirty_buffers: bool,
|
||||
show_selection_menu: Option<bool>,
|
||||
blame: Option<Entity<GitBlame>>,
|
||||
|
@ -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>,
|
||||
) {
|
||||
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<Background> for LineHighlight {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_diff_hunk_controls(
|
||||
row: u32,
|
||||
status: &DiffHunkStatus,
|
||||
hunk_range: Range<Anchor>,
|
||||
is_created_file: bool,
|
||||
line_height: Pixels,
|
||||
editor: &Entity<Editor>,
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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<AnyElement> {
|
||||
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<Anchor>,
|
||||
is_created_file: bool,
|
||||
line_height: Pixels,
|
||||
editor: &Entity<Editor>,
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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<Item = &'a clock::Lamport>,
|
||||
transaction: &'a Transaction,
|
||||
) -> impl 'a + Iterator<Item = Range<D>>
|
||||
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<Item = Range<D>>
|
||||
where
|
||||
D: TextDimension,
|
||||
{
|
||||
self.edited_ranges_for_edit_ids(&transaction.edit_ids)
|
||||
}
|
||||
|
||||
pub fn subscribe(&mut self) -> Subscription {
|
||||
self.subscriptions.subscribe()
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue