Allow reviewing of agent changes without Git (#27668)
Release Notes: - N/A
This commit is contained in:
parent
8a307e7b89
commit
94ed0b7767
27 changed files with 2271 additions and 1095 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -451,6 +451,7 @@ dependencies = [
|
||||||
"assistant_slash_command",
|
"assistant_slash_command",
|
||||||
"assistant_tool",
|
"assistant_tool",
|
||||||
"async-watch",
|
"async-watch",
|
||||||
|
"buffer_diff",
|
||||||
"chrono",
|
"chrono",
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
|
@ -466,7 +467,6 @@ dependencies = [
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"git",
|
"git",
|
||||||
"git_ui",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"heed",
|
"heed",
|
||||||
"html_to_markdown",
|
"html_to_markdown",
|
||||||
|
@ -496,7 +496,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
|
||||||
"smol",
|
"smol",
|
||||||
"streaming_diff",
|
"streaming_diff",
|
||||||
"telemetry",
|
"telemetry",
|
||||||
|
@ -692,6 +691,8 @@ name = "assistant_tool"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-watch",
|
||||||
|
"buffer_diff",
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
|
@ -703,6 +704,9 @@ dependencies = [
|
||||||
"project",
|
"project",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"settings",
|
||||||
|
"text",
|
||||||
|
"util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -712,6 +716,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assistant_tool",
|
"assistant_tool",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"feature_flags",
|
"feature_flags",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
|
|
|
@ -126,7 +126,6 @@
|
||||||
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
|
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||||
"ctrl-alt-space": "editor::ShowCharacterPalette",
|
"ctrl-alt-space": "editor::ShowCharacterPalette",
|
||||||
"ctrl-;": "editor::ToggleLineNumbers",
|
"ctrl-;": "editor::ToggleLineNumbers",
|
||||||
"ctrl-k ctrl-r": "git::Restore",
|
|
||||||
"ctrl-'": "editor::ToggleSelectedDiffHunks",
|
"ctrl-'": "editor::ToggleSelectedDiffHunks",
|
||||||
"ctrl-\"": "editor::ExpandAllDiffHunks",
|
"ctrl-\"": "editor::ExpandAllDiffHunks",
|
||||||
"ctrl-i": "editor::ShowSignatureHelp",
|
"ctrl-i": "editor::ShowSignatureHelp",
|
||||||
|
@ -138,6 +137,22 @@
|
||||||
"shift-f9": "editor::EditLogBreakpoint"
|
"shift-f9": "editor::EditLogBreakpoint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && !assistant_diff",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-k ctrl-r": "git::Restore",
|
||||||
|
"ctrl-alt-y": "git::ToggleStaged",
|
||||||
|
"alt-y": "git::StageAndNext",
|
||||||
|
"alt-shift-y": "git::UnstageAndNext"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "AssistantDiff",
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-y": "assistant2::ToggleKeep",
|
||||||
|
"ctrl-k ctrl-r": "assistant2::Reject"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
@ -382,9 +397,6 @@
|
||||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||||
"ctrl-shift-v": "markdown::OpenPreview",
|
"ctrl-shift-v": "markdown::OpenPreview",
|
||||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
||||||
"ctrl-alt-y": "git::ToggleStaged",
|
|
||||||
"alt-y": "git::StageAndNext",
|
|
||||||
"alt-shift-y": "git::UnstageAndNext",
|
|
||||||
"alt-.": "editor::GoToHunk",
|
"alt-.": "editor::GoToHunk",
|
||||||
"alt-,": "editor::GoToPreviousHunk"
|
"alt-,": "editor::GoToPreviousHunk"
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,10 +147,6 @@
|
||||||
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
|
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||||
"ctrl-cmd-space": "editor::ShowCharacterPalette",
|
"ctrl-cmd-space": "editor::ShowCharacterPalette",
|
||||||
"cmd-;": "editor::ToggleLineNumbers",
|
"cmd-;": "editor::ToggleLineNumbers",
|
||||||
"cmd-alt-z": "git::Restore",
|
|
||||||
"cmd-alt-y": "git::ToggleStaged",
|
|
||||||
"cmd-y": "git::StageAndNext",
|
|
||||||
"cmd-shift-y": "git::UnstageAndNext",
|
|
||||||
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
||||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||||
|
@ -231,6 +227,24 @@
|
||||||
"ctrl-alt-enter": "repl::RunInPlace"
|
"ctrl-alt-enter": "repl::RunInPlace"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && !assistant_diff",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"cmd-alt-z": "git::Restore",
|
||||||
|
"cmd-alt-y": "git::ToggleStaged",
|
||||||
|
"cmd-y": "git::StageAndNext",
|
||||||
|
"cmd-shift-y": "git::UnstageAndNext"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "AssistantDiff",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"cmd-y": "assistant2::ToggleKeep",
|
||||||
|
"cmd-alt-z": "assistant2::Reject"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "AssistantPanel",
|
"context": "AssistantPanel",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|
|
@ -25,6 +25,7 @@ assistant_settings.workspace = true
|
||||||
assistant_slash_command.workspace = true
|
assistant_slash_command.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
async-watch.workspace = true
|
async-watch.workspace = true
|
||||||
|
buffer_diff.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
|
@ -40,7 +41,6 @@ fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
git_ui.workspace = true
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
heed.workspace = true
|
heed.workspace = true
|
||||||
html_to_markdown.workspace = true
|
html_to_markdown.workspace = true
|
||||||
|
@ -68,7 +68,6 @@ rope.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
smallvec.workspace = true
|
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
streaming_diff.workspace = true
|
streaming_diff.workspace = true
|
||||||
telemetry.workspace = true
|
telemetry.workspace = true
|
||||||
|
@ -87,6 +86,7 @@ workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
gpui = { workspace = true, "features" = ["test-support"] }
|
gpui = { workspace = true, "features" = ["test-support"] }
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mod active_thread;
|
mod active_thread;
|
||||||
mod assistant_configuration;
|
mod assistant_configuration;
|
||||||
|
mod assistant_diff;
|
||||||
mod assistant_model_selector;
|
mod assistant_model_selector;
|
||||||
mod assistant_panel;
|
mod assistant_panel;
|
||||||
mod buffer_codegen;
|
mod buffer_codegen;
|
||||||
|
@ -37,6 +38,7 @@ pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate}
|
||||||
pub use crate::inline_assistant::InlineAssistant;
|
pub use crate::inline_assistant::InlineAssistant;
|
||||||
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
|
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
|
||||||
pub use crate::thread_store::ThreadStore;
|
pub use crate::thread_store::ThreadStore;
|
||||||
|
pub use assistant_diff::AssistantDiff;
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
assistant2,
|
assistant2,
|
||||||
|
@ -61,7 +63,9 @@ actions!(
|
||||||
FocusRight,
|
FocusRight,
|
||||||
RemoveFocusedContext,
|
RemoveFocusedContext,
|
||||||
AcceptSuggestedContext,
|
AcceptSuggestedContext,
|
||||||
OpenActiveThreadAsMarkdown
|
OpenActiveThreadAsMarkdown,
|
||||||
|
ToggleKeep,
|
||||||
|
Reject
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
665
crates/assistant2/src/assistant_diff.rs
Normal file
665
crates/assistant2/src/assistant_diff.rs
Normal file
|
@ -0,0 +1,665 @@
|
||||||
|
use crate::{Thread, ThreadEvent, ToggleKeep};
|
||||||
|
use anyhow::Result;
|
||||||
|
use buffer_diff::DiffHunkStatus;
|
||||||
|
use collections::HashSet;
|
||||||
|
use editor::{
|
||||||
|
actions::{GoToHunk, GoToPreviousHunk},
|
||||||
|
Direction, Editor, EditorEvent, MultiBuffer, ToPoint,
|
||||||
|
};
|
||||||
|
use gpui::{
|
||||||
|
prelude::*, AnyElement, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
|
SharedString, Subscription, Task, WeakEntity, Window,
|
||||||
|
};
|
||||||
|
use language::{Capability, DiskState, OffsetRangeExt};
|
||||||
|
use multi_buffer::PathKey;
|
||||||
|
use project::{Project, ProjectPath};
|
||||||
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
ops::Range,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||||
|
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.register_addon(AssistantDiffAddon);
|
||||||
|
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 changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
|
||||||
|
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
for (buffer, changed) in changed_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 = changed.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, is_excerpt_newly_added) =
|
||||||
|
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||||
|
let was_empty = multibuffer.is_empty();
|
||||||
|
let is_excerpt_newly_added = multibuffer.set_excerpts_for_path(
|
||||||
|
path_key.clone(),
|
||||||
|
buffer.clone(),
|
||||||
|
diff_hunk_ranges,
|
||||||
|
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
multibuffer.add_diff(changed.diff.clone(), cx);
|
||||||
|
(was_empty, is_excerpt_newly_added)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
if was_empty {
|
||||||
|
editor.change_selections(None, window, cx, |selections| {
|
||||||
|
selections.select_ranges([0..0])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_excerpt_newly_added
|
||||||
|
&& buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.map_or(false, |file| file.disk_state() == DiskState::Deleted)
|
||||||
|
{
|
||||||
|
editor.fold_buffer(snapshot.text.remote_id(), cx)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 toggle_keep(&mut self, _: &crate::ToggleKeep, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let ranges = self
|
||||||
|
.editor
|
||||||
|
.read(cx)
|
||||||
|
.selections
|
||||||
|
.disjoint_anchor_ranges()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||||
|
let diff_hunks_in_ranges = self
|
||||||
|
.editor
|
||||||
|
.read(cx)
|
||||||
|
.diff_hunks_in_ranges(&ranges, &snapshot)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for hunk in diff_hunks_in_ranges {
|
||||||
|
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||||
|
if let Some(buffer) = buffer {
|
||||||
|
self.thread.update(cx, |thread, cx| {
|
||||||
|
let accept = hunk.status().has_secondary_hunk();
|
||||||
|
thread.review_edits_in_range(buffer, hunk.buffer_range, accept, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reject(&mut self, _: &crate::Reject, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let ranges = self
|
||||||
|
.editor
|
||||||
|
.update(cx, |editor, cx| editor.selections.ranges(cx));
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.restore_hunks_in_ranges(ranges, window, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn review_diff_hunks(
|
||||||
|
&mut self,
|
||||||
|
hunk_ranges: Vec<Range<editor::Anchor>>,
|
||||||
|
accept: bool,
|
||||||
|
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<_>>();
|
||||||
|
|
||||||
|
for hunk in diff_hunks_in_ranges {
|
||||||
|
let buffer = self.multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||||
|
if let Some(buffer) = buffer {
|
||||||
|
self.thread.update(cx, |thread, cx| {
|
||||||
|
thread.review_edits_in_range(buffer, hunk.buffer_range, accept, 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("Assistant 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("Assistant 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"
|
||||||
|
})
|
||||||
|
.on_action(cx.listener(Self::toggle_keep))
|
||||||
|
.on_action(cx.listener(Self::reject))
|
||||||
|
.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(("keep", row as u64), "Keep")
|
||||||
|
.tooltip({
|
||||||
|
let focus_handle = editor.focus_handle(cx);
|
||||||
|
move |window, cx| {
|
||||||
|
Tooltip::for_action_in(
|
||||||
|
"Keep Hunk",
|
||||||
|
&crate::ToggleKeep,
|
||||||
|
&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,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Button::new("reject", "Reject")
|
||||||
|
.tooltip({
|
||||||
|
let focus_handle = editor.focus_handle(cx);
|
||||||
|
move |window, cx| {
|
||||||
|
Tooltip::for_action_in(
|
||||||
|
"Reject Hunk",
|
||||||
|
&crate::Reject,
|
||||||
|
&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),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![Button::new(("review", row as u64), "Review")
|
||||||
|
.tooltip({
|
||||||
|
let focus_handle = editor.focus_handle(cx);
|
||||||
|
move |window, cx| {
|
||||||
|
Tooltip::for_action_in("Review", &ToggleKeep, &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,
|
||||||
|
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)
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AssistantDiffAddon;
|
||||||
|
|
||||||
|
impl editor::Addon for AssistantDiffAddon {
|
||||||
|
fn to_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extend_key_context(&self, key_context: &mut gpui::KeyContext, _: &App) {
|
||||||
|
key_context.add("assistant_diff");
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,10 @@ use std::sync::Arc;
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::actions::MoveUp;
|
use editor::actions::MoveUp;
|
||||||
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
|
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
|
||||||
|
use file_icons::FileIcons;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use git::ExpandCommitEditor;
|
|
||||||
use git_ui::git_panel;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
point, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||||
WeakEntity,
|
WeakEntity,
|
||||||
};
|
};
|
||||||
use language_model::LanguageModelRegistry;
|
use language_model::LanguageModelRegistry;
|
||||||
|
@ -17,8 +16,10 @@ use settings::Settings;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
|
prelude::*, ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle,
|
||||||
|
Tooltip,
|
||||||
};
|
};
|
||||||
|
use util::ResultExt;
|
||||||
use vim_mode_setting::VimModeSetting;
|
use vim_mode_setting::VimModeSetting;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -30,7 +31,8 @@ use crate::profile_selector::ProfileSelector;
|
||||||
use crate::thread::{RequestKind, Thread};
|
use crate::thread::{RequestKind, Thread};
|
||||||
use crate::thread_store::ThreadStore;
|
use crate::thread_store::ThreadStore;
|
||||||
use crate::{
|
use crate::{
|
||||||
Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker, ToggleProfileSelector,
|
AssistantDiff, Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker,
|
||||||
|
ToggleProfileSelector,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct MessageEditor {
|
pub struct MessageEditor {
|
||||||
|
@ -46,6 +48,7 @@ pub struct MessageEditor {
|
||||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||||
model_selector: Entity<AssistantModelSelector>,
|
model_selector: Entity<AssistantModelSelector>,
|
||||||
profile_selector: Entity<ProfileSelector>,
|
profile_selector: Entity<ProfileSelector>,
|
||||||
|
edits_expanded: bool,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +140,7 @@ impl MessageEditor {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
edits_expanded: false,
|
||||||
profile_selector: cx
|
profile_selector: cx
|
||||||
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
|
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
|
@ -236,6 +240,9 @@ impl MessageEditor {
|
||||||
thread
|
thread
|
||||||
.update(cx, |thread, cx| {
|
.update(cx, |thread, cx| {
|
||||||
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
|
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
|
||||||
|
thread.action_log().update(cx, |action_log, cx| {
|
||||||
|
action_log.clear_reviewed_changes(cx);
|
||||||
|
});
|
||||||
thread.insert_user_message(user_message, context, checkpoint, cx);
|
thread.insert_user_message(user_message, context, checkpoint, cx);
|
||||||
thread.send_to_model(model, request_kind, cx);
|
thread.send_to_model(model, request_kind, cx);
|
||||||
})
|
})
|
||||||
|
@ -282,6 +289,10 @@ impl MessageEditor {
|
||||||
self.context_strip.focus_handle(cx).focus(window);
|
self.context_strip.focus_handle(cx).focus(window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
impl Focusable for MessageEditor {
|
||||||
|
@ -298,7 +309,6 @@ impl Render for MessageEditor {
|
||||||
let focus_handle = self.editor.focus_handle(cx);
|
let focus_handle = self.editor.focus_handle(cx);
|
||||||
let inline_context_picker = self.inline_context_picker.clone();
|
let inline_context_picker = self.inline_context_picker.clone();
|
||||||
|
|
||||||
let empty_thread = self.thread.read(cx).is_empty();
|
|
||||||
let is_generating = self.thread.read(cx).is_generating();
|
let is_generating = self.thread.read(cx).is_generating();
|
||||||
let is_model_selected = self.is_model_selected(cx);
|
let is_model_selected = self.is_model_selected(cx);
|
||||||
let is_editor_empty = self.is_editor_empty(cx);
|
let is_editor_empty = self.is_editor_empty(cx);
|
||||||
|
@ -318,30 +328,10 @@ impl Render for MessageEditor {
|
||||||
px(64.)
|
px(64.)
|
||||||
};
|
};
|
||||||
|
|
||||||
let project = self.thread.read(cx).project();
|
let action_log = self.thread.read(cx).action_log();
|
||||||
let changed_files = if let Some(repository) = project.read(cx).active_repository(cx) {
|
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||||
repository.read(cx).cached_status().count()
|
let changed_buffers_count = changed_buffers.len();
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let border_color = cx.theme().colors().border;
|
|
||||||
let active_color = cx.theme().colors().element_selected;
|
|
||||||
let editor_bg_color = cx.theme().colors().editor_background;
|
let editor_bg_color = cx.theme().colors().editor_background;
|
||||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
|
||||||
|
|
||||||
let edit_files_container = || {
|
|
||||||
h_flex()
|
|
||||||
.mx_2()
|
|
||||||
.py_1()
|
|
||||||
.pl_2p5()
|
|
||||||
.pr_1()
|
|
||||||
.bg(bg_edit_files_disclosure)
|
|
||||||
.border_1()
|
|
||||||
.border_color(border_color)
|
|
||||||
.justify_between()
|
|
||||||
.flex_wrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
@ -403,169 +393,150 @@ impl Render for MessageEditor {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(
|
.when(changed_buffers_count > 0, |parent| {
|
||||||
changed_files > 0 && !is_generating && !empty_thread,
|
parent.child(
|
||||||
|parent| {
|
v_flex()
|
||||||
parent.child(
|
.mx_2()
|
||||||
edit_files_container()
|
.bg(cx.theme().colors().element_background)
|
||||||
.border_b_0()
|
.border_1()
|
||||||
.rounded_t_md()
|
.border_b_0()
|
||||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
.border_color(cx.theme().colors().border)
|
||||||
color: gpui::black().opacity(0.15),
|
.rounded_t_md()
|
||||||
offset: point(px(1.), px(-1.)),
|
.child(
|
||||||
blur_radius: px(3.),
|
h_flex()
|
||||||
spread_radius: px(0.),
|
.p_2()
|
||||||
}])
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Label::new("Edits").size(LabelSize::XSmall))
|
.child(
|
||||||
.child(div().size_1().rounded_full().bg(border_color))
|
Disclosure::new(
|
||||||
.child(
|
"edits-disclosure",
|
||||||
Label::new(format!(
|
self.edits_expanded,
|
||||||
"{} {}",
|
)
|
||||||
changed_files,
|
.on_click(
|
||||||
if changed_files == 1 { "file" } else { "files" }
|
cx.listener(|this, _ev, _window, cx| {
|
||||||
))
|
this.edits_expanded = !this.edits_expanded;
|
||||||
.size(LabelSize::XSmall),
|
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!(
|
||||||
|
"{} {}",
|
||||||
|
changed_buffers_count,
|
||||||
|
if changed_buffers_count == 1 {
|
||||||
|
"file"
|
||||||
|
} else {
|
||||||
|
"files"
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("review", "Review")
|
||||||
|
.label_size(LabelSize::XSmall)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.handle_review_click(window, cx)
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when(self.edits_expanded, |parent| {
|
||||||
|
parent.child(
|
||||||
|
v_flex().bg(cx.theme().colors().editor_background).children(
|
||||||
|
changed_buffers.into_iter().enumerate().flat_map(
|
||||||
|
|(index, (buffer, changed))| {
|
||||||
|
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();
|
||||||
|
|
||||||
|
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 < 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),
|
||||||
|
)
|
||||||
|
.when(!changed.needs_review, |parent| {
|
||||||
|
parent.child(
|
||||||
|
Icon::new(IconName::Check)
|
||||||
|
.color(Color::Success),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(element)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
}),
|
||||||
h_flex()
|
)
|
||||||
.gap_1()
|
})
|
||||||
.child(
|
|
||||||
Button::new("panel", "Open Git Panel")
|
|
||||||
.label_size(LabelSize::XSmall)
|
|
||||||
.key_binding({
|
|
||||||
let focus_handle = focus_handle.clone();
|
|
||||||
KeyBinding::for_action_in(
|
|
||||||
&git_panel::ToggleFocus,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.map(|kb| kb.size(rems_from_px(10.)))
|
|
||||||
})
|
|
||||||
.on_click(|_ev, _window, cx| {
|
|
||||||
cx.defer(|cx| {
|
|
||||||
cx.dispatch_action(&git_panel::ToggleFocus)
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("review", "Review Diff")
|
|
||||||
.label_size(LabelSize::XSmall)
|
|
||||||
.key_binding({
|
|
||||||
let focus_handle = focus_handle.clone();
|
|
||||||
KeyBinding::for_action_in(
|
|
||||||
&git_ui::project_diff::Diff,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.map(|kb| kb.size(rems_from_px(10.)))
|
|
||||||
})
|
|
||||||
.on_click(|_event, _window, cx| {
|
|
||||||
cx.defer(|cx| {
|
|
||||||
cx.dispatch_action(&git_ui::project_diff::Diff)
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("commit", "Commit Changes")
|
|
||||||
.label_size(LabelSize::XSmall)
|
|
||||||
.key_binding({
|
|
||||||
let focus_handle = focus_handle.clone();
|
|
||||||
KeyBinding::for_action_in(
|
|
||||||
&ExpandCommitEditor,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.map(|kb| kb.size(rems_from_px(10.)))
|
|
||||||
})
|
|
||||||
.on_click(|_event, _window, cx| {
|
|
||||||
cx.defer(|cx| {
|
|
||||||
cx.dispatch_action(&ExpandCommitEditor)
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.when(
|
|
||||||
changed_files > 0 && !is_generating && empty_thread,
|
|
||||||
|parent| {
|
|
||||||
parent.child(
|
|
||||||
edit_files_container()
|
|
||||||
.mb_2()
|
|
||||||
.rounded_md()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(Label::new("Consider committing your changes before starting a fresh thread").size(LabelSize::XSmall))
|
|
||||||
.child(div().size_1().rounded_full().bg(border_color))
|
|
||||||
.child(
|
|
||||||
Label::new(format!(
|
|
||||||
"{} {}",
|
|
||||||
changed_files,
|
|
||||||
if changed_files == 1 { "file" } else { "files" }
|
|
||||||
))
|
|
||||||
.size(LabelSize::XSmall),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
Button::new("review", "Review Diff")
|
|
||||||
.label_size(LabelSize::XSmall)
|
|
||||||
.key_binding({
|
|
||||||
let focus_handle = focus_handle.clone();
|
|
||||||
KeyBinding::for_action_in(
|
|
||||||
&git_ui::project_diff::Diff,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.map(|kb| kb.size(rems_from_px(10.)))
|
|
||||||
})
|
|
||||||
.on_click(|_event, _window, cx| {
|
|
||||||
cx.defer(|cx| {
|
|
||||||
cx.dispatch_action(&git_ui::project_diff::Diff)
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("commit", "Commit Changes")
|
|
||||||
.label_size(LabelSize::XSmall)
|
|
||||||
.key_binding({
|
|
||||||
let focus_handle = focus_handle.clone();
|
|
||||||
KeyBinding::for_action_in(
|
|
||||||
&ExpandCommitEditor,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.map(|kb| kb.size(rems_from_px(10.)))
|
|
||||||
})
|
|
||||||
.on_click(|_event, _window, cx| {
|
|
||||||
cx.defer(|cx| {
|
|
||||||
cx.dispatch_action(&ExpandCommitEditor)
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("MessageEditor")
|
.key_context("MessageEditor")
|
||||||
.on_action(cx.listener(Self::chat))
|
.on_action(cx.listener(Self::chat))
|
||||||
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
|
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
|
||||||
this.profile_selector.read(cx).menu_handle().toggle(window, cx);
|
this.profile_selector
|
||||||
|
.read(cx)
|
||||||
|
.menu_handle()
|
||||||
|
.toggle(window, cx);
|
||||||
}))
|
}))
|
||||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||||
this.model_selector
|
this.model_selector
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
|
@ -1529,6 +1530,18 @@ impl Thread {
|
||||||
Ok(String::from_utf8_lossy(&markdown).to_string())
|
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>,
|
||||||
|
) {
|
||||||
|
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> {
|
pub fn action_log(&self) -> &Entity<ActionLog> {
|
||||||
&self.action_log
|
&self.action_log
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ path = "src/assistant_tool.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
async-watch.workspace = true
|
||||||
|
buffer_diff.workspace = true
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
|
@ -24,3 +26,16 @@ parking_lot.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
text.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"] }
|
||||||
|
settings = { workspace = true, features = ["test-support"] }
|
||||||
|
text = { workspace = true, features = ["test-support"] }
|
||||||
|
util = { workspace = true, features = ["test-support"] }
|
||||||
|
|
968
crates/assistant_tool/src/action_log.rs
Normal file
968
crates/assistant_tool/src/action_log.rs
Normal file
|
@ -0,0 +1,968 @@
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
use buffer_diff::BufferDiff;
|
||||||
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
|
||||||
|
use language::{
|
||||||
|
Buffer, BufferEvent, DiskState, OffsetRangeExt, Operation, TextBufferSnapshot, ToOffset,
|
||||||
|
};
|
||||||
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
|
/// Tracks actions performed by tools in a thread
|
||||||
|
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>,
|
||||||
|
/// Has the model edited a file since it last checked diagnostics?
|
||||||
|
edited_since_project_diagnostics_check: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionLog {
|
||||||
|
/// Creates a new, empty action log.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
stale_buffers_in_context: HashSet::default(),
|
||||||
|
tracked_buffers: BTreeMap::default(),
|
||||||
|
edited_since_project_diagnostics_check: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_reviewed_changes(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.tracked_buffers
|
||||||
|
.retain(|_buffer, tracked_buffer| match &mut tracked_buffer.change {
|
||||||
|
Change::Edited {
|
||||||
|
accepted_edit_ids, ..
|
||||||
|
} => {
|
||||||
|
accepted_edit_ids.clear();
|
||||||
|
tracked_buffer.schedule_diff_update();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Change::Deleted { reviewed, .. } => !*reviewed,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notifies a diagnostics check
|
||||||
|
pub fn checked_project_diagnostics(&mut self) {
|
||||||
|
self.edited_since_project_diagnostics_check = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if any files have been edited since the last project diagnostics check
|
||||||
|
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
|
||||||
|
self.edited_since_project_diagnostics_check
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_buffer(
|
||||||
|
&mut self,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
created: bool,
|
||||||
|
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
|
||||||
|
});
|
||||||
|
let (diff_update_tx, diff_update_rx) = async_watch::channel(());
|
||||||
|
TrackedBuffer {
|
||||||
|
buffer: buffer.clone(),
|
||||||
|
change: Change::Edited {
|
||||||
|
unreviewed_edit_ids: HashSet::default(),
|
||||||
|
accepted_edit_ids: HashSet::default(),
|
||||||
|
initial_content: if created {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(text_snapshot.clone())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
version: buffer.read(cx).version(),
|
||||||
|
diff,
|
||||||
|
secondary_diff: unreviewed_diff,
|
||||||
|
diff_update: diff_update_tx,
|
||||||
|
_maintain_diff: cx.spawn({
|
||||||
|
let buffer = buffer.clone();
|
||||||
|
async move |this, cx| {
|
||||||
|
Self::maintain_diff(this, buffer, diff_update_rx, cx)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tracked_buffer.version = buffer.read(cx).version();
|
||||||
|
tracked_buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_buffer_event(
|
||||||
|
&mut self,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
event: &BufferEvent,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
BufferEvent::Operation { operation, .. } => {
|
||||||
|
self.handle_buffer_operation(buffer, operation, cx)
|
||||||
|
}
|
||||||
|
BufferEvent::FileHandleChanged => {
|
||||||
|
self.handle_buffer_file_changed(buffer, cx);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_buffer_operation(
|
||||||
|
&mut self,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
operation: &Operation,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Operation::Buffer(text::Operation::Edit(operation)) = operation else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Change::Edited {
|
||||||
|
unreviewed_edit_ids,
|
||||||
|
accepted_edit_ids,
|
||||||
|
..
|
||||||
|
} = &mut tracked_buffer.change
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if unreviewed_edit_ids.contains(&operation.timestamp)
|
||||||
|
|| accepted_edit_ids.contains(&operation.timestamp)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let operation_edit_ranges = buffer
|
||||||
|
.edited_ranges_for_edit_ids::<usize>([&operation.timestamp])
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let intersects_unreviewed_edits = ranges_intersect(
|
||||||
|
operation_edit_ranges.iter().cloned(),
|
||||||
|
buffer.edited_ranges_for_edit_ids::<usize>(unreviewed_edit_ids.iter()),
|
||||||
|
);
|
||||||
|
let mut intersected_accepted_edits = HashSet::default();
|
||||||
|
for accepted_edit_id in accepted_edit_ids.iter() {
|
||||||
|
let intersects_accepted_edit = ranges_intersect(
|
||||||
|
operation_edit_ranges.iter().cloned(),
|
||||||
|
buffer.edited_ranges_for_edit_ids::<usize>([accepted_edit_id]),
|
||||||
|
);
|
||||||
|
if intersects_accepted_edit {
|
||||||
|
intersected_accepted_edits.insert(*accepted_edit_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the buffer operation overlaps with any tracked edits, mark it as unreviewed.
|
||||||
|
// If it intersects an already-accepted id, mark that edit as unreviewed again.
|
||||||
|
if intersects_unreviewed_edits || !intersected_accepted_edits.is_empty() {
|
||||||
|
unreviewed_edit_ids.insert(operation.timestamp);
|
||||||
|
for accepted_edit_id in intersected_accepted_edits {
|
||||||
|
unreviewed_edit_ids.insert(accepted_edit_id);
|
||||||
|
accepted_edit_ids.remove(&accepted_edit_id);
|
||||||
|
}
|
||||||
|
tracked_buffer.schedule_diff_update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_buffer_file_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||||
|
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match tracked_buffer.change {
|
||||||
|
Change::Deleted { .. } => {
|
||||||
|
if buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.map_or(false, |file| file.disk_state() != DiskState::Deleted)
|
||||||
|
{
|
||||||
|
// If the buffer had been deleted by a tool, but it got
|
||||||
|
// resurrected externally, we want to clear the changes we
|
||||||
|
// were tracking and reset the buffer's state.
|
||||||
|
tracked_buffer.change = Change::Edited {
|
||||||
|
unreviewed_edit_ids: HashSet::default(),
|
||||||
|
accepted_edit_ids: HashSet::default(),
|
||||||
|
initial_content: Some(buffer.read(cx).text_snapshot()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
tracked_buffer.schedule_diff_update();
|
||||||
|
}
|
||||||
|
Change::Edited { .. } => {
|
||||||
|
if buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.map_or(false, |file| file.disk_state() == DiskState::Deleted)
|
||||||
|
{
|
||||||
|
// If the buffer had been edited by a tool, but it got
|
||||||
|
// deleted externally, we want to stop tracking it.
|
||||||
|
self.tracked_buffers.remove(&buffer);
|
||||||
|
} else {
|
||||||
|
tracked_buffer.schedule_diff_update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn maintain_diff(
|
||||||
|
this: WeakEntity<Self>,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
mut diff_update: async_watch::Receiver<()>,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Result<()> {
|
||||||
|
while let Some(_) = diff_update.recv().await.ok() {
|
||||||
|
let update = this.update(cx, |this, cx| {
|
||||||
|
let tracked_buffer = this
|
||||||
|
.tracked_buffers
|
||||||
|
.get_mut(&buffer)
|
||||||
|
.context("buffer not tracked")?;
|
||||||
|
anyhow::Ok(tracked_buffer.update_diff(cx))
|
||||||
|
})??;
|
||||||
|
update.await;
|
||||||
|
this.update(cx, |_this, cx| cx.notify())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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, false, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track a buffer as read, so we can notify the model about user edits.
|
||||||
|
pub fn will_create_buffer(
|
||||||
|
&mut self,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
edit_id: Option<clock::Lamport>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.track_buffer(buffer.clone(), true, cx);
|
||||||
|
self.buffer_edited(buffer, edit_id.into_iter().collect(), cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark a buffer as edited, so we can refresh it in the context
|
||||||
|
pub fn buffer_edited(
|
||||||
|
&mut self,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
mut edit_ids: Vec<clock::Lamport>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.edited_since_project_diagnostics_check = true;
|
||||||
|
self.stale_buffers_in_context.insert(buffer.clone());
|
||||||
|
|
||||||
|
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
|
||||||
|
|
||||||
|
match &mut tracked_buffer.change {
|
||||||
|
Change::Edited {
|
||||||
|
unreviewed_edit_ids,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
unreviewed_edit_ids.extend(edit_ids.iter().copied());
|
||||||
|
}
|
||||||
|
Change::Deleted {
|
||||||
|
deleted_content,
|
||||||
|
deletion_id,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
edit_ids.extend(*deletion_id);
|
||||||
|
tracked_buffer.change = Change::Edited {
|
||||||
|
unreviewed_edit_ids: edit_ids.into_iter().collect(),
|
||||||
|
accepted_edit_ids: HashSet::default(),
|
||||||
|
initial_content: Some(deleted_content.clone()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracked_buffer.schedule_diff_update();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||||
|
let tracked_buffer = self.track_buffer(buffer.clone(), false, cx);
|
||||||
|
if let Change::Edited {
|
||||||
|
initial_content, ..
|
||||||
|
} = &tracked_buffer.change
|
||||||
|
{
|
||||||
|
if let Some(initial_content) = initial_content {
|
||||||
|
let deletion_id = buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
|
||||||
|
tracked_buffer.change = Change::Deleted {
|
||||||
|
reviewed: false,
|
||||||
|
deleted_content: initial_content.clone(),
|
||||||
|
deletion_id,
|
||||||
|
};
|
||||||
|
tracked_buffer.schedule_diff_update();
|
||||||
|
} else {
|
||||||
|
self.tracked_buffers.remove(&buffer);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
) {
|
||||||
|
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let buffer_range = buffer_range.to_offset(buffer);
|
||||||
|
|
||||||
|
match &mut tracked_buffer.change {
|
||||||
|
Change::Deleted { reviewed, .. } => {
|
||||||
|
*reviewed = accept;
|
||||||
|
}
|
||||||
|
Change::Edited {
|
||||||
|
unreviewed_edit_ids,
|
||||||
|
accepted_edit_ids,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let (source, destination) = if accept {
|
||||||
|
(unreviewed_edit_ids, accepted_edit_ids)
|
||||||
|
} else {
|
||||||
|
(accepted_edit_ids, 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.insert(*edit_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracked_buffer.schedule_diff_update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the set of buffers that contain changes that haven't been reviewed by the user.
|
||||||
|
pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, ChangedBuffer> {
|
||||||
|
self.tracked_buffers
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, tracked)| tracked.has_changes(cx))
|
||||||
|
.map(|(buffer, tracked)| {
|
||||||
|
(
|
||||||
|
buffer.clone(),
|
||||||
|
ChangedBuffer {
|
||||||
|
diff: tracked.diff.clone(),
|
||||||
|
needs_review: match &tracked.change {
|
||||||
|
Change::Edited {
|
||||||
|
unreviewed_edit_ids,
|
||||||
|
..
|
||||||
|
} => !unreviewed_edit_ids.is_empty(),
|
||||||
|
Change::Deleted { reviewed, .. } => !reviewed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ranges_intersect(
|
||||||
|
ranges_a: impl IntoIterator<Item = Range<usize>>,
|
||||||
|
ranges_b: impl IntoIterator<Item = Range<usize>>,
|
||||||
|
) -> bool {
|
||||||
|
let mut ranges_a_iter = ranges_a.into_iter().peekable();
|
||||||
|
let mut ranges_b_iter = ranges_b.into_iter().peekable();
|
||||||
|
while let (Some(range_a), Some(range_b)) = (ranges_a_iter.peek(), ranges_b_iter.peek()) {
|
||||||
|
if range_a.end < range_b.start {
|
||||||
|
ranges_a_iter.next();
|
||||||
|
} else if range_b.end < range_a.start {
|
||||||
|
ranges_b_iter.next();
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackedBuffer {
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
change: Change,
|
||||||
|
version: clock::Global,
|
||||||
|
diff: Entity<BufferDiff>,
|
||||||
|
secondary_diff: Entity<BufferDiff>,
|
||||||
|
diff_update: async_watch::Sender<()>,
|
||||||
|
_maintain_diff: Task<()>,
|
||||||
|
_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Change {
|
||||||
|
Edited {
|
||||||
|
unreviewed_edit_ids: HashSet<clock::Lamport>,
|
||||||
|
accepted_edit_ids: HashSet<clock::Lamport>,
|
||||||
|
initial_content: Option<TextBufferSnapshot>,
|
||||||
|
},
|
||||||
|
Deleted {
|
||||||
|
reviewed: bool,
|
||||||
|
deleted_content: TextBufferSnapshot,
|
||||||
|
deletion_id: Option<clock::Lamport>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackedBuffer {
|
||||||
|
fn has_changes(&self, cx: &App) -> bool {
|
||||||
|
self.diff
|
||||||
|
.read(cx)
|
||||||
|
.hunks(&self.buffer.read(cx), cx)
|
||||||
|
.next()
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_diff_update(&self) {
|
||||||
|
self.diff_update.send(()).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_diff(&mut self, cx: &mut App) -> Task<()> {
|
||||||
|
match &self.change {
|
||||||
|
Change::Edited {
|
||||||
|
unreviewed_edit_ids,
|
||||||
|
accepted_edit_ids,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let edits_to_undo = unreviewed_edit_ids
|
||||||
|
.iter()
|
||||||
|
.chain(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 = 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,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
_ = primary_diff_update.await;
|
||||||
|
_ = secondary_diff_update.await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Change::Deleted {
|
||||||
|
reviewed,
|
||||||
|
deleted_content,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let reviewed = *reviewed;
|
||||||
|
let deleted_content = deleted_content.clone();
|
||||||
|
|
||||||
|
let primary_diff = self.diff.clone();
|
||||||
|
let secondary_diff = self.secondary_diff.clone();
|
||||||
|
let buffer_snapshot = self.buffer.read(cx).text_snapshot();
|
||||||
|
let language = self.buffer.read(cx).language().cloned();
|
||||||
|
let language_registry = self.buffer.read(cx).language_registry().clone();
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let base_text = Arc::new(deleted_content.text());
|
||||||
|
|
||||||
|
let primary_diff_snapshot = BufferDiff::update_diff(
|
||||||
|
primary_diff.clone(),
|
||||||
|
buffer_snapshot.clone(),
|
||||||
|
Some(base_text.clone()),
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
language.clone(),
|
||||||
|
language_registry.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let secondary_diff_snapshot = BufferDiff::update_diff(
|
||||||
|
secondary_diff.clone(),
|
||||||
|
buffer_snapshot.clone(),
|
||||||
|
if reviewed {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(base_text.clone())
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
language.clone(),
|
||||||
|
language_registry.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(primary_diff_snapshot) = primary_diff_snapshot {
|
||||||
|
primary_diff
|
||||||
|
.update(cx, |diff, cx| {
|
||||||
|
diff.set_snapshot(
|
||||||
|
&buffer_snapshot,
|
||||||
|
primary_diff_snapshot,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(secondary_diff_snapshot) = secondary_diff_snapshot {
|
||||||
|
secondary_diff
|
||||||
|
.update(cx, |diff, cx| {
|
||||||
|
diff.set_snapshot(
|
||||||
|
&buffer_snapshot,
|
||||||
|
secondary_diff_snapshot,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChangedBuffer {
|
||||||
|
pub diff: Entity<BufferDiff>,
|
||||||
|
pub needs_review: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use buffer_diff::DiffHunkStatusKind;
|
||||||
|
use gpui::TestAppContext;
|
||||||
|
use language::Point;
|
||||||
|
use project::{FakeFs, Fs, Project, RemoveOptions};
|
||||||
|
use serde_json::json;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use util::path;
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
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,
|
||||||
|
old_text: "def\n".into(),
|
||||||
|
},
|
||||||
|
HunkStatus {
|
||||||
|
range: Point::new(4, 0)..Point::new(4, 3),
|
||||||
|
review_status: ReviewStatus::Unreviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Modified,
|
||||||
|
old_text: "mno".into(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
|
||||||
|
action_log.update(cx, |log, cx| {
|
||||||
|
log.review_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), true, cx)
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
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,
|
||||||
|
old_text: "def\n".into(),
|
||||||
|
},
|
||||||
|
HunkStatus {
|
||||||
|
range: Point::new(4, 0)..Point::new(4, 3),
|
||||||
|
review_status: ReviewStatus::Reviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Modified,
|
||||||
|
old_text: "mno".into(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
|
||||||
|
action_log.update(cx, |log, cx| {
|
||||||
|
log.review_edits_in_range(
|
||||||
|
buffer.clone(),
|
||||||
|
Point::new(3, 0)..Point::new(4, 3),
|
||||||
|
false,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
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,
|
||||||
|
old_text: "def\n".into(),
|
||||||
|
},
|
||||||
|
HunkStatus {
|
||||||
|
range: Point::new(4, 0)..Point::new(4, 3),
|
||||||
|
review_status: ReviewStatus::Unreviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Modified,
|
||||||
|
old_text: "mno".into(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
|
||||||
|
action_log.update(cx, |log, cx| {
|
||||||
|
log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), true, cx)
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
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,
|
||||||
|
old_text: "def\n".into(),
|
||||||
|
},
|
||||||
|
HunkStatus {
|
||||||
|
range: Point::new(4, 0)..Point::new(4, 3),
|
||||||
|
review_status: ReviewStatus::Reviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Modified,
|
||||||
|
old_text: "mno".into(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_overlapping_user_edits(cx: &mut TestAppContext) {
|
||||||
|
let action_log = cx.new(|_| ActionLog::new());
|
||||||
|
let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl\nmno", cx));
|
||||||
|
|
||||||
|
let tool_edit = buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer
|
||||||
|
.edit(
|
||||||
|
[(Point::new(0, 2)..Point::new(2, 3), "C\nDEF\nGHI")],
|
||||||
|
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![tool_edit], cx)
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
unreviewed_hunks(&action_log, cx),
|
||||||
|
vec![(
|
||||||
|
buffer.clone(),
|
||||||
|
vec![HunkStatus {
|
||||||
|
range: Point::new(0, 0)..Point::new(3, 0),
|
||||||
|
review_status: ReviewStatus::Unreviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Modified,
|
||||||
|
old_text: "abc\ndef\nghi\n".into(),
|
||||||
|
}],
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
|
||||||
|
action_log.update(cx, |log, cx| {
|
||||||
|
log.review_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), true, cx)
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
unreviewed_hunks(&action_log, cx),
|
||||||
|
vec![(
|
||||||
|
buffer.clone(),
|
||||||
|
vec![HunkStatus {
|
||||||
|
range: Point::new(0, 0)..Point::new(3, 0),
|
||||||
|
review_status: ReviewStatus::Reviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Modified,
|
||||||
|
old_text: "abc\ndef\nghi\n".into(),
|
||||||
|
}],
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit([(Point::new(0, 2)..Point::new(0, 2), "X")], None, cx)
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
unreviewed_hunks(&action_log, cx),
|
||||||
|
vec![(
|
||||||
|
buffer.clone(),
|
||||||
|
vec![HunkStatus {
|
||||||
|
range: Point::new(0, 0)..Point::new(3, 0),
|
||||||
|
review_status: ReviewStatus::Unreviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Modified,
|
||||||
|
old_text: "abc\ndef\nghi\n".into(),
|
||||||
|
}],
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
|
||||||
|
action_log.update(cx, |log, cx| log.clear_reviewed_changes(cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
unreviewed_hunks(&action_log, cx),
|
||||||
|
vec![(
|
||||||
|
buffer.clone(),
|
||||||
|
vec![HunkStatus {
|
||||||
|
range: Point::new(0, 0)..Point::new(3, 0),
|
||||||
|
review_status: ReviewStatus::Unreviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Modified,
|
||||||
|
old_text: "abc\ndef\nghi\n".into(),
|
||||||
|
}],
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_deletion(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings_store);
|
||||||
|
language::init(cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
path!("/dir"),
|
||||||
|
json!({"file1": "lorem\n", "file2": "ipsum\n"}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||||
|
let file1_path = project
|
||||||
|
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
|
||||||
|
.unwrap();
|
||||||
|
let file2_path = project
|
||||||
|
.read_with(cx, |project, cx| project.find_project_path("dir/file2", cx))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let action_log = cx.new(|_| ActionLog::new());
|
||||||
|
let buffer1 = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_buffer(file1_path.clone(), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let buffer2 = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_buffer(file2_path.clone(), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx));
|
||||||
|
action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx));
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.delete_file(file1_path.clone(), false, cx)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.delete_file(file2_path.clone(), false, cx)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
unreviewed_hunks(&action_log, cx),
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
buffer1.clone(),
|
||||||
|
vec![HunkStatus {
|
||||||
|
range: Point::new(0, 0)..Point::new(0, 0),
|
||||||
|
review_status: ReviewStatus::Unreviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Deleted,
|
||||||
|
old_text: "lorem\n".into(),
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
buffer2.clone(),
|
||||||
|
vec![HunkStatus {
|
||||||
|
range: Point::new(0, 0)..Point::new(0, 0),
|
||||||
|
review_status: ReviewStatus::Unreviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Deleted,
|
||||||
|
old_text: "ipsum\n".into(),
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate file1 being recreated externally.
|
||||||
|
fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec())
|
||||||
|
.await;
|
||||||
|
let buffer2 = project
|
||||||
|
.update(cx, |project, cx| project.open_buffer(file2_path, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
// Simulate file2 being recreated by a tool.
|
||||||
|
let edit_id = buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx));
|
||||||
|
action_log.update(cx, |log, cx| {
|
||||||
|
log.will_create_buffer(buffer2.clone(), edit_id, cx)
|
||||||
|
});
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
unreviewed_hunks(&action_log, cx),
|
||||||
|
vec![(
|
||||||
|
buffer2.clone(),
|
||||||
|
vec![HunkStatus {
|
||||||
|
range: Point::new(0, 0)..Point::new(0, 5),
|
||||||
|
review_status: ReviewStatus::Unreviewed,
|
||||||
|
diff_status: DiffHunkStatusKind::Modified,
|
||||||
|
old_text: "ipsum\n".into(),
|
||||||
|
}],
|
||||||
|
)]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate file2 being deleted externally.
|
||||||
|
fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct HunkStatus {
|
||||||
|
range: Range<Point>,
|
||||||
|
review_status: ReviewStatus,
|
||||||
|
diff_status: DiffHunkStatusKind,
|
||||||
|
old_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
.changed_buffers(cx)
|
||||||
|
.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,
|
||||||
|
old_text: tracked_buffer
|
||||||
|
.diff
|
||||||
|
.read(cx)
|
||||||
|
.base_text()
|
||||||
|
.text_for_range(hunk.diff_base_byte_range)
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,19 @@
|
||||||
|
mod action_log;
|
||||||
mod tool_registry;
|
mod tool_registry;
|
||||||
mod tool_working_set;
|
mod tool_working_set;
|
||||||
|
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::fmt::Formatter;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::{HashMap, HashSet};
|
use gpui::{App, Entity, SharedString, Task};
|
||||||
use gpui::{App, Context, Entity, SharedString, Task};
|
|
||||||
use icons::IconName;
|
use icons::IconName;
|
||||||
use language::Buffer;
|
|
||||||
use language_model::LanguageModelRequestMessage;
|
use language_model::LanguageModelRequestMessage;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
|
||||||
|
pub use crate::action_log::*;
|
||||||
pub use crate::tool_registry::*;
|
pub use crate::tool_registry::*;
|
||||||
pub use crate::tool_working_set::*;
|
pub use crate::tool_working_set::*;
|
||||||
|
|
||||||
|
@ -71,71 +73,3 @@ impl Debug for dyn Tool {
|
||||||
f.debug_struct("Tool").field("name", &self.name()).finish()
|
f.debug_struct("Tool").field("name", &self.name()).finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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>,
|
|
||||||
/// Has the model edited a file since it last checked diagnostics?
|
|
||||||
edited_since_project_diagnostics_check: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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(),
|
|
||||||
edited_since_project_diagnostics_check: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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);
|
|
||||||
self.edited_since_project_diagnostics_check = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Notifies a diagnostics check
|
|
||||||
pub fn checked_project_diagnostics(&mut self) {
|
|
||||||
self.edited_since_project_diagnostics_check = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if any files have been edited since the last project diagnostics check
|
|
||||||
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
|
|
||||||
self.edited_since_project_diagnostics_check
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ path = "src/assistant_tools.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
|
clock.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
feature_flags.workspace = true
|
feature_flags.workspace = true
|
||||||
|
@ -38,6 +39,7 @@ worktree.workspace = true
|
||||||
open = { workspace = true }
|
open = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
clock = { workspace = true, features = ["test-support"] }
|
||||||
collections = { workspace = true, features = ["test-support"] }
|
collections = { workspace = true, features = ["test-support"] }
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
language = { workspace = true, features = ["test-support"] }
|
language = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -70,7 +70,7 @@ impl Tool for CreateFileTool {
|
||||||
input: serde_json::Value,
|
input: serde_json::Value,
|
||||||
_messages: &[LanguageModelRequestMessage],
|
_messages: &[LanguageModelRequestMessage],
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
_action_log: Entity<ActionLog>,
|
action_log: Entity<ActionLog>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<String>> {
|
) -> Task<Result<String>> {
|
||||||
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
|
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
|
||||||
|
@ -85,24 +85,20 @@ impl Tool for CreateFileTool {
|
||||||
let destination_path: Arc<str> = input.path.as_str().into();
|
let destination_path: Arc<str> = input.path.as_str().into();
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.create_entry(project_path.clone(), false, cx)
|
|
||||||
})?
|
|
||||||
.await
|
|
||||||
.map_err(|err| anyhow!("Unable to create {destination_path}: {err}"))?;
|
|
||||||
let buffer = project
|
let buffer = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.open_buffer(project_path.clone(), cx)
|
project.open_buffer(project_path.clone(), cx)
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
|
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
|
||||||
buffer.update(cx, |buffer, cx| {
|
let edit_id = buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx))?;
|
||||||
buffer.set_text(contents, cx);
|
|
||||||
|
action_log.update(cx, |action_log, cx| {
|
||||||
|
action_log.will_create_buffer(buffer.clone(), edit_id, cx)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
project
|
project
|
||||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||||
.await
|
.await
|
||||||
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
|
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant_tool::{ActionLog, Tool};
|
use assistant_tool::{ActionLog, Tool};
|
||||||
|
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||||
use gpui::{App, AppContext, Entity, Task};
|
use gpui::{App, AppContext, Entity, Task};
|
||||||
use language_model::LanguageModelRequestMessage;
|
use language_model::LanguageModelRequestMessage;
|
||||||
use project::Project;
|
use project::{Project, ProjectPath};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -60,28 +61,76 @@ impl Tool for DeletePathTool {
|
||||||
input: serde_json::Value,
|
input: serde_json::Value,
|
||||||
_messages: &[LanguageModelRequestMessage],
|
_messages: &[LanguageModelRequestMessage],
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
_action_log: Entity<ActionLog>,
|
action_log: Entity<ActionLog>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<String>> {
|
) -> Task<Result<String>> {
|
||||||
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
|
let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
|
||||||
Ok(input) => input.path,
|
Ok(input) => input.path,
|
||||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||||
};
|
};
|
||||||
|
let Some(project_path) = project.read(cx).find_project_path(&path_str, cx) else {
|
||||||
|
return Task::ready(Err(anyhow!(
|
||||||
|
"Couldn't delete {path_str} because that path isn't in this project."
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
match project
|
let Some(worktree) = project
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.find_project_path(&path_str, cx)
|
.worktree_for_id(project_path.worktree_id, cx)
|
||||||
.and_then(|path| project.update(cx, |project, cx| project.delete_file(path, false, cx)))
|
else {
|
||||||
{
|
return Task::ready(Err(anyhow!(
|
||||||
Some(deletion_task) => cx.background_spawn(async move {
|
"Couldn't delete {path_str} because that path isn't in this project."
|
||||||
match deletion_task.await {
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let worktree_snapshot = worktree.read(cx).snapshot();
|
||||||
|
let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
|
||||||
|
cx.background_spawn({
|
||||||
|
let project_path = project_path.clone();
|
||||||
|
async move {
|
||||||
|
for entry in
|
||||||
|
worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
|
||||||
|
{
|
||||||
|
if !entry.path.starts_with(&project_path.path) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
paths_tx
|
||||||
|
.send(ProjectPath {
|
||||||
|
worktree_id: project_path.worktree_id,
|
||||||
|
path: entry.path.clone(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
while let Some(path) = paths_rx.next().await {
|
||||||
|
if let Ok(buffer) = project
|
||||||
|
.update(cx, |project, cx| project.open_buffer(path, cx))?
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
action_log.update(cx, |action_log, cx| {
|
||||||
|
action_log.will_delete_buffer(buffer.clone(), cx)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let delete = project.update(cx, |project, cx| {
|
||||||
|
project.delete_file(project_path, false, cx)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match delete {
|
||||||
|
Some(deletion_task) => match deletion_task.await {
|
||||||
Ok(()) => Ok(format!("Deleted {path_str}")),
|
Ok(()) => Ok(format!("Deleted {path_str}")),
|
||||||
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
|
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
|
||||||
}
|
},
|
||||||
}),
|
None => Err(anyhow!(
|
||||||
None => Task::ready(Err(anyhow!(
|
"Couldn't delete {path_str} because that path isn't in this project."
|
||||||
"Couldn't delete {path_str} because that path isn't in this project."
|
)),
|
||||||
))),
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,6 +173,7 @@ enum EditorResponse {
|
||||||
struct AppliedAction {
|
struct AppliedAction {
|
||||||
source: String,
|
source: String,
|
||||||
buffer: Entity<language::Buffer>,
|
buffer: Entity<language::Buffer>,
|
||||||
|
edit_ids: Vec<clock::Lamport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -340,9 +341,18 @@ impl EditToolRequest {
|
||||||
self.push_search_error(error);
|
self.push_search_error(error);
|
||||||
}
|
}
|
||||||
DiffResult::Diff(diff) => {
|
DiffResult::Diff(diff) => {
|
||||||
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
|
let edit_ids = buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.finalize_last_transaction();
|
||||||
|
buffer.apply_diff(diff, false, cx);
|
||||||
|
let transaction = buffer.finalize_last_transaction();
|
||||||
|
transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone())
|
||||||
|
})?;
|
||||||
|
|
||||||
self.push_applied_action(AppliedAction { source, buffer });
|
self.push_applied_action(AppliedAction {
|
||||||
|
source,
|
||||||
|
buffer,
|
||||||
|
edit_ids,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,7 +474,10 @@ impl EditToolRequest {
|
||||||
let mut changed_buffers = HashSet::default();
|
let mut changed_buffers = HashSet::default();
|
||||||
|
|
||||||
for action in applied {
|
for action in applied {
|
||||||
changed_buffers.insert(action.buffer);
|
changed_buffers.insert(action.buffer.clone());
|
||||||
|
self.action_log.update(cx, |log, cx| {
|
||||||
|
log.buffer_edited(action.buffer, action.edit_ids, cx)
|
||||||
|
})?;
|
||||||
write!(&mut output, "\n\n{}", action.source)?;
|
write!(&mut output, "\n\n{}", action.source)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,10 +487,6 @@ impl EditToolRequest {
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.action_log
|
|
||||||
.update(cx, |log, cx| log.buffer_edited(changed_buffers.clone(), cx))
|
|
||||||
.log_err();
|
|
||||||
|
|
||||||
if !search_errors.is_empty() {
|
if !search_errors.is_empty() {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
|
|
|
@ -5,7 +5,7 @@ use language_model::LanguageModelRequestMessage;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashSet, path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
use ui::IconName;
|
use ui::IconName;
|
||||||
|
|
||||||
use crate::replace::replace_exact;
|
use crate::replace::replace_exact;
|
||||||
|
@ -189,20 +189,21 @@ impl Tool for FindReplaceFileTool {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Some(diff) = result {
|
if let Some(diff) = result {
|
||||||
buffer.update(cx, |buffer, cx| {
|
let edit_ids = buffer.update(cx, |buffer, cx| {
|
||||||
let _ = buffer.apply_diff(diff, cx);
|
buffer.finalize_last_transaction();
|
||||||
|
buffer.apply_diff(diff, false, cx);
|
||||||
|
let transaction = buffer.finalize_last_transaction();
|
||||||
|
transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
action_log.update(cx, |log, cx| {
|
||||||
|
log.buffer_edited(buffer.clone(), edit_ids, cx)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
project.save_buffer(buffer.clone(), cx)
|
project.save_buffer(buffer, cx)
|
||||||
})?.await?;
|
})?.await?;
|
||||||
|
|
||||||
action_log.update(cx, |log, cx| {
|
|
||||||
let mut buffers = HashSet::default();
|
|
||||||
buffers.insert(buffer);
|
|
||||||
log.buffer_edited(buffers, cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(format!("Edited {}", input.path.display()))
|
Ok(format!("Edited {}", input.path.display()))
|
||||||
} else {
|
} else {
|
||||||
let err = buffer.read_with(cx, |buffer, _cx| {
|
let err = buffer.read_with(cx, |buffer, _cx| {
|
||||||
|
|
|
@ -518,7 +518,7 @@ mod tests {
|
||||||
// Call replace_flexible and transform the result
|
// Call replace_flexible and transform the result
|
||||||
replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| {
|
replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| {
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
let _ = buffer.apply_diff(diff, cx);
|
let _ = buffer.apply_diff(diff, false, cx);
|
||||||
buffer.text()
|
buffer.text()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -185,8 +185,8 @@ use theme::{
|
||||||
ThemeColors, ThemeSettings,
|
ThemeColors, ThemeSettings,
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize, Key,
|
h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconButtonShape, IconName,
|
||||||
Tooltip,
|
IconSize, Key, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
|
use util::{maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -220,6 +220,18 @@ 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 EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
|
||||||
pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4;
|
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 {
|
const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers {
|
||||||
alt: true,
|
alt: true,
|
||||||
shift: true,
|
shift: true,
|
||||||
|
@ -752,6 +764,7 @@ pub struct Editor {
|
||||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||||
git_blame_inline_tooltip: Option<WeakEntity<crate::commit_tooltip::CommitTooltip>>,
|
git_blame_inline_tooltip: Option<WeakEntity<crate::commit_tooltip::CommitTooltip>>,
|
||||||
git_blame_inline_enabled: bool,
|
git_blame_inline_enabled: bool,
|
||||||
|
render_diff_hunk_controls: RenderDiffHunkControlsFn,
|
||||||
serialize_dirty_buffers: bool,
|
serialize_dirty_buffers: bool,
|
||||||
show_selection_menu: Option<bool>,
|
show_selection_menu: Option<bool>,
|
||||||
blame: Option<Entity<GitBlame>>,
|
blame: Option<Entity<GitBlame>>,
|
||||||
|
@ -1527,6 +1540,7 @@ impl Editor {
|
||||||
show_git_blame_inline_delay_task: None,
|
show_git_blame_inline_delay_task: None,
|
||||||
git_blame_inline_tooltip: None,
|
git_blame_inline_tooltip: None,
|
||||||
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
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)
|
serialize_dirty_buffers: ProjectSettings::get_global(cx)
|
||||||
.session
|
.session
|
||||||
.restore_unsaved_buffers,
|
.restore_unsaved_buffers,
|
||||||
|
@ -8399,7 +8413,7 @@ impl Editor {
|
||||||
self.restore_hunks_in_ranges(selections, window, cx);
|
self.restore_hunks_in_ranges(selections, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_hunks_in_ranges(
|
pub fn restore_hunks_in_ranges(
|
||||||
&mut self,
|
&mut self,
|
||||||
ranges: Vec<Range<Point>>,
|
ranges: Vec<Range<Point>>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -12623,7 +12637,7 @@ impl Editor {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn go_to_hunk_before_or_after_position(
|
pub fn go_to_hunk_before_or_after_position(
|
||||||
&mut self,
|
&mut self,
|
||||||
snapshot: &EditorSnapshot,
|
snapshot: &EditorSnapshot,
|
||||||
position: Point,
|
position: Point,
|
||||||
|
@ -14786,6 +14800,15 @@ impl Editor {
|
||||||
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
|
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(
|
pub fn stage_and_next(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &::git::StageAndNext,
|
_: &::git::StageAndNext,
|
||||||
|
@ -19913,3 +19936,187 @@ 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,13 +18,13 @@ use crate::{
|
||||||
scroll::scroll_amount::ScrollAmount,
|
scroll::scroll_amount::ScrollAmount,
|
||||||
BlockId, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk,
|
BlockId, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk,
|
||||||
DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,
|
DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,
|
||||||
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FocusedBlock, GoToHunk,
|
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FocusedBlock,
|
||||||
GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
|
GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason,
|
||||||
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
|
InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, OpenExcerpts, PageDown, PageUp,
|
||||||
OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight,
|
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
|
||||||
Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS,
|
StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
|
||||||
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS,
|
||||||
MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||||
};
|
};
|
||||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||||
use client::ParticipantIndex;
|
use client::ParticipantIndex;
|
||||||
|
@ -43,7 +43,6 @@ use gpui::{
|
||||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||||
Subscription, TextRun, TextStyleRefinement, Window,
|
Subscription, TextRun, TextStyleRefinement, Window,
|
||||||
};
|
};
|
||||||
use inline_completion::Direction;
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{
|
language_settings::{
|
||||||
|
@ -76,10 +75,7 @@ use std::{
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
use text::BufferId;
|
use text::BufferId;
|
||||||
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
|
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
|
||||||
use ui::{
|
use ui::{h_flex, prelude::*, ButtonLike, ContextMenu, KeyBinding, Tooltip, POPOVER_Y_PADDING};
|
||||||
h_flex, prelude::*, ButtonLike, ContextMenu, IconButtonShape, KeyBinding, Tooltip,
|
|
||||||
POPOVER_Y_PADDING,
|
|
||||||
};
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use util::{debug_panic, RangeExt, ResultExt};
|
use util::{debug_panic, RangeExt, ResultExt};
|
||||||
use workspace::{item::Item, notifications::NotifyTaskExt};
|
use workspace::{item::Item, notifications::NotifyTaskExt};
|
||||||
|
@ -3919,6 +3915,7 @@ impl EditorElement {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Vec<AnyElement> {
|
) -> 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 point_for_position = position_map.point_for_position(window.mouse_position());
|
||||||
|
|
||||||
let mut controls = vec![];
|
let mut controls = vec![];
|
||||||
|
@ -3961,7 +3958,7 @@ impl EditorElement {
|
||||||
+ text_hitbox.bounds.top()
|
+ text_hitbox.bounds.top()
|
||||||
- scroll_pixel_position.y;
|
- scroll_pixel_position.y;
|
||||||
|
|
||||||
let mut element = diff_hunk_controls(
|
let mut element = render_diff_hunk_controls(
|
||||||
display_row_range.start.0,
|
display_row_range.start.0,
|
||||||
status,
|
status,
|
||||||
multi_buffer_range.clone(),
|
multi_buffer_range.clone(),
|
||||||
|
@ -8882,187 +8879,3 @@ mod tests {
|
||||||
.collect()
|
.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()
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,8 +5,8 @@ use futures::future::{self, BoxFuture};
|
||||||
use git::{
|
use git::{
|
||||||
blame::Blame,
|
blame::Blame,
|
||||||
repository::{
|
repository::{
|
||||||
AskPassSession, Branch, CommitDetails, GitIndex, GitRepository, GitRepositoryCheckpoint,
|
AskPassSession, Branch, CommitDetails, GitRepository, GitRepositoryCheckpoint, PushOptions,
|
||||||
PushOptions, Remote, RepoPath, ResetMode,
|
Remote, RepoPath, ResetMode,
|
||||||
},
|
},
|
||||||
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
|
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
|
||||||
};
|
};
|
||||||
|
@ -81,15 +81,7 @@ impl FakeGitRepository {
|
||||||
impl GitRepository for FakeGitRepository {
|
impl GitRepository for FakeGitRepository {
|
||||||
fn reload_index(&self) {}
|
fn reload_index(&self) {}
|
||||||
|
|
||||||
fn load_index_text(
|
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
|
||||||
&self,
|
|
||||||
index: Option<GitIndex>,
|
|
||||||
path: RepoPath,
|
|
||||||
) -> BoxFuture<Option<String>> {
|
|
||||||
if index.is_some() {
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
|
|
||||||
async {
|
async {
|
||||||
self.with_state_async(false, move |state| {
|
self.with_state_async(false, move |state| {
|
||||||
state
|
state
|
||||||
|
@ -179,19 +171,6 @@ impl GitRepository for FakeGitRepository {
|
||||||
self.path()
|
self.path()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status(
|
|
||||||
&self,
|
|
||||||
index: Option<GitIndex>,
|
|
||||||
path_prefixes: &[RepoPath],
|
|
||||||
) -> BoxFuture<'static, Result<GitStatus>> {
|
|
||||||
if index.is_some() {
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = self.status_blocking(path_prefixes);
|
|
||||||
async move { status }.boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
||||||
let workdir_path = self.dot_git_path.parent().unwrap();
|
let workdir_path = self.dot_git_path.parent().unwrap();
|
||||||
|
|
||||||
|
@ -457,12 +436,4 @@ impl GitRepository for FakeGitRepository {
|
||||||
) -> BoxFuture<Result<String>> {
|
) -> BoxFuture<Result<String>> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_index(&self) -> BoxFuture<Result<GitIndex>> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_diff(&self, _index: GitIndex, _diff: String) -> BoxFuture<Result<()>> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::borrow::{Borrow, Cow};
|
use std::borrow::{Borrow, Cow};
|
||||||
use std::ffi::{OsStr, OsString};
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::future;
|
||||||
use std::path::Component;
|
use std::path::Component;
|
||||||
use std::process::{ExitStatus, Stdio};
|
use std::process::{ExitStatus, Stdio};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
@ -20,7 +21,6 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use std::{future, mem};
|
|
||||||
use sum_tree::MapSeekTarget;
|
use sum_tree::MapSeekTarget;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use util::command::{new_smol_command, new_std_command};
|
use util::command::{new_smol_command, new_std_command};
|
||||||
|
@ -161,8 +161,7 @@ pub trait GitRepository: Send + Sync {
|
||||||
/// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
|
/// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
|
||||||
///
|
///
|
||||||
/// Also returns `None` for symlinks.
|
/// Also returns `None` for symlinks.
|
||||||
fn load_index_text(&self, index: Option<GitIndex>, path: RepoPath)
|
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>>;
|
||||||
-> BoxFuture<Option<String>>;
|
|
||||||
|
|
||||||
/// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path.
|
/// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path.
|
||||||
///
|
///
|
||||||
|
@ -184,11 +183,6 @@ pub trait GitRepository: Send + Sync {
|
||||||
|
|
||||||
fn merge_head_shas(&self) -> Vec<String>;
|
fn merge_head_shas(&self) -> Vec<String>;
|
||||||
|
|
||||||
fn status(
|
|
||||||
&self,
|
|
||||||
index: Option<GitIndex>,
|
|
||||||
path_prefixes: &[RepoPath],
|
|
||||||
) -> BoxFuture<'static, Result<GitStatus>>;
|
|
||||||
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
|
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
|
||||||
|
|
||||||
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>;
|
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>;
|
||||||
|
@ -312,12 +306,6 @@ pub trait GitRepository: Send + Sync {
|
||||||
base_checkpoint: GitRepositoryCheckpoint,
|
base_checkpoint: GitRepositoryCheckpoint,
|
||||||
target_checkpoint: GitRepositoryCheckpoint,
|
target_checkpoint: GitRepositoryCheckpoint,
|
||||||
) -> BoxFuture<Result<String>>;
|
) -> BoxFuture<Result<String>>;
|
||||||
|
|
||||||
/// Creates a new index for the repository.
|
|
||||||
fn create_index(&self) -> BoxFuture<Result<GitIndex>>;
|
|
||||||
|
|
||||||
/// Applies a diff to the repository's index.
|
|
||||||
fn apply_diff(&self, index: GitIndex, diff: String) -> BoxFuture<Result<()>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum DiffType {
|
pub enum DiffType {
|
||||||
|
@ -374,11 +362,6 @@ pub struct GitRepositoryCheckpoint {
|
||||||
commit_sha: Oid,
|
commit_sha: Oid,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
|
||||||
pub struct GitIndex {
|
|
||||||
id: Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GitRepository for RealGitRepository {
|
impl GitRepository for RealGitRepository {
|
||||||
fn reload_index(&self) {
|
fn reload_index(&self) {
|
||||||
if let Ok(mut index) = self.repository.lock().index() {
|
if let Ok(mut index) = self.repository.lock().index() {
|
||||||
|
@ -484,82 +467,35 @@ impl GitRepository for RealGitRepository {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_index_text(
|
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
|
||||||
&self,
|
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
|
||||||
index: Option<GitIndex>,
|
const GIT_MODE_SYMLINK: u32 = 0o120000;
|
||||||
path: RepoPath,
|
|
||||||
) -> BoxFuture<Option<String>> {
|
let repo = self.repository.clone();
|
||||||
let working_directory = self.working_directory();
|
|
||||||
let git_binary_path = self.git_binary_path.clone();
|
|
||||||
let executor = self.executor.clone();
|
|
||||||
self.executor
|
self.executor
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
match check_path_to_repo_path_errors(&path) {
|
fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
|
||||||
Ok(_) => {}
|
// This check is required because index.get_path() unwraps internally :(
|
||||||
Err(err) => {
|
check_path_to_repo_path_errors(path)?;
|
||||||
log::error!("Error with repo path: {:?}", err);
|
|
||||||
return None;
|
let mut index = repo.index()?;
|
||||||
}
|
index.read(false)?;
|
||||||
|
|
||||||
|
const STAGE_NORMAL: i32 = 0;
|
||||||
|
let oid = match index.get_path(path, STAGE_NORMAL) {
|
||||||
|
Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
|
||||||
|
_ => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = repo.find_blob(oid)?.content().to_owned();
|
||||||
|
Ok(Some(String::from_utf8(content)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
let working_directory = match working_directory {
|
match logic(&repo.lock(), &path) {
|
||||||
Ok(dir) => dir,
|
Ok(value) => return value,
|
||||||
Err(err) => {
|
Err(err) => log::error!("Error loading index text: {:?}", err),
|
||||||
log::error!("Error getting working directory: {:?}", err);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut git = GitBinary::new(git_binary_path, working_directory, executor);
|
|
||||||
let text = git
|
|
||||||
.with_option_index(index, async |git| {
|
|
||||||
// First check if the file is a symlink using ls-files
|
|
||||||
let ls_files_output = git
|
|
||||||
.run(&[
|
|
||||||
OsStr::new("ls-files"),
|
|
||||||
OsStr::new("--stage"),
|
|
||||||
path.to_unix_style().as_ref(),
|
|
||||||
])
|
|
||||||
.await
|
|
||||||
.context("error running ls-files")?;
|
|
||||||
|
|
||||||
// Parse ls-files output to check if it's a symlink
|
|
||||||
// Format is: "100644 <sha> 0 <filename>" where 100644 is the mode
|
|
||||||
if ls_files_output.is_empty() {
|
|
||||||
return Ok(None); // File not in index
|
|
||||||
}
|
|
||||||
|
|
||||||
let parts: Vec<&str> = ls_files_output.split_whitespace().collect();
|
|
||||||
if parts.len() < 2 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"unexpected ls-files output format: {}",
|
|
||||||
ls_files_output
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a symlink (120000 mode)
|
|
||||||
if parts[0] == "120000" {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let sha = parts[1];
|
|
||||||
|
|
||||||
// Now get the content
|
|
||||||
Ok(Some(
|
|
||||||
git.run_raw(&["cat-file", "blob", sha])
|
|
||||||
.await
|
|
||||||
.context("error getting blob content")?,
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match text {
|
|
||||||
Ok(text) => text,
|
|
||||||
Err(error) => {
|
|
||||||
log::error!("Error getting text: {}", error);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
None
|
||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
@ -678,40 +614,6 @@ impl GitRepository for RealGitRepository {
|
||||||
shas
|
shas
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status(
|
|
||||||
&self,
|
|
||||||
index: Option<GitIndex>,
|
|
||||||
path_prefixes: &[RepoPath],
|
|
||||||
) -> BoxFuture<'static, Result<GitStatus>> {
|
|
||||||
let working_directory = self.working_directory();
|
|
||||||
let git_binary_path = self.git_binary_path.clone();
|
|
||||||
let executor = self.executor.clone();
|
|
||||||
let mut args = vec![
|
|
||||||
OsString::from("--no-optional-locks"),
|
|
||||||
OsString::from("status"),
|
|
||||||
OsString::from("--porcelain=v1"),
|
|
||||||
OsString::from("--untracked-files=all"),
|
|
||||||
OsString::from("--no-renames"),
|
|
||||||
OsString::from("-z"),
|
|
||||||
];
|
|
||||||
args.extend(path_prefixes.iter().map(|path_prefix| {
|
|
||||||
if path_prefix.0.as_ref() == Path::new("") {
|
|
||||||
Path::new(".").into()
|
|
||||||
} else {
|
|
||||||
path_prefix.as_os_str().into()
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
self.executor
|
|
||||||
.spawn(async move {
|
|
||||||
let working_directory = working_directory?;
|
|
||||||
let mut git = GitBinary::new(git_binary_path, working_directory, executor);
|
|
||||||
git.with_option_index(index, async |git| git.run(&args).await)
|
|
||||||
.await?
|
|
||||||
.parse()
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
||||||
let output = new_std_command(&self.git_binary_path)
|
let output = new_std_command(&self.git_binary_path)
|
||||||
.current_dir(self.working_directory()?)
|
.current_dir(self.working_directory()?)
|
||||||
|
@ -1319,41 +1221,6 @@ impl GitRepository for RealGitRepository {
|
||||||
})
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_index(&self) -> BoxFuture<Result<GitIndex>> {
|
|
||||||
let working_directory = self.working_directory();
|
|
||||||
let git_binary_path = self.git_binary_path.clone();
|
|
||||||
|
|
||||||
let executor = self.executor.clone();
|
|
||||||
self.executor
|
|
||||||
.spawn(async move {
|
|
||||||
let working_directory = working_directory?;
|
|
||||||
let mut git = GitBinary::new(git_binary_path, working_directory, executor);
|
|
||||||
let index = GitIndex { id: Uuid::new_v4() };
|
|
||||||
git.with_index(index, async move |git| git.run(&["add", "--all"]).await)
|
|
||||||
.await?;
|
|
||||||
Ok(index)
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_diff(&self, index: GitIndex, diff: String) -> BoxFuture<Result<()>> {
|
|
||||||
let working_directory = self.working_directory();
|
|
||||||
let git_binary_path = self.git_binary_path.clone();
|
|
||||||
|
|
||||||
let executor = self.executor.clone();
|
|
||||||
self.executor
|
|
||||||
.spawn(async move {
|
|
||||||
let working_directory = working_directory?;
|
|
||||||
let mut git = GitBinary::new(git_binary_path, working_directory, executor);
|
|
||||||
git.with_index(index, async move |git| {
|
|
||||||
git.run_with_stdin(&["apply", "--cached", "-"], diff).await
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.boxed()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
|
fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
|
||||||
|
@ -1407,7 +1274,7 @@ impl GitBinary {
|
||||||
&mut self,
|
&mut self,
|
||||||
f: impl AsyncFnOnce(&Self) -> Result<R>,
|
f: impl AsyncFnOnce(&Self) -> Result<R>,
|
||||||
) -> Result<R> {
|
) -> Result<R> {
|
||||||
let index_file_path = self.path_for_index(GitIndex { id: Uuid::new_v4() });
|
let index_file_path = self.path_for_index_id(Uuid::new_v4());
|
||||||
|
|
||||||
let delete_temp_index = util::defer({
|
let delete_temp_index = util::defer({
|
||||||
let index_file_path = index_file_path.clone();
|
let index_file_path = index_file_path.clone();
|
||||||
|
@ -1432,30 +1299,10 @@ impl GitBinary {
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn with_index<R>(
|
fn path_for_index_id(&self, id: Uuid) -> PathBuf {
|
||||||
&mut self,
|
|
||||||
index: GitIndex,
|
|
||||||
f: impl AsyncFnOnce(&Self) -> Result<R>,
|
|
||||||
) -> Result<R> {
|
|
||||||
self.with_option_index(Some(index), f).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn with_option_index<R>(
|
|
||||||
&mut self,
|
|
||||||
index: Option<GitIndex>,
|
|
||||||
f: impl AsyncFnOnce(&Self) -> Result<R>,
|
|
||||||
) -> Result<R> {
|
|
||||||
let new_index_path = index.map(|index| self.path_for_index(index));
|
|
||||||
let old_index_path = mem::replace(&mut self.index_file_path, new_index_path);
|
|
||||||
let result = f(self).await;
|
|
||||||
self.index_file_path = old_index_path;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn path_for_index(&self, index: GitIndex) -> PathBuf {
|
|
||||||
self.working_directory
|
self.working_directory
|
||||||
.join(".git")
|
.join(".git")
|
||||||
.join(format!("index-{}.tmp", index.id))
|
.join(format!("index-{}.tmp", id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run<S>(&self, args: impl IntoIterator<Item = S>) -> Result<String>
|
pub async fn run<S>(&self, args: impl IntoIterator<Item = S>) -> Result<String>
|
||||||
|
@ -1486,26 +1333,6 @@ impl GitBinary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_with_stdin(&self, args: &[&str], stdin: String) -> Result<String> {
|
|
||||||
let mut command = self.build_command(args);
|
|
||||||
command.stdin(Stdio::piped());
|
|
||||||
let mut child = command.spawn()?;
|
|
||||||
|
|
||||||
let mut child_stdin = child.stdin.take().context("failed to write to stdin")?;
|
|
||||||
child_stdin.write_all(stdin.as_bytes()).await?;
|
|
||||||
drop(child_stdin);
|
|
||||||
|
|
||||||
let output = child.output().await?;
|
|
||||||
if output.status.success() {
|
|
||||||
Ok(String::from_utf8(output.stdout)?.trim_end().to_string())
|
|
||||||
} else {
|
|
||||||
Err(anyhow!(GitBinaryCommandError {
|
|
||||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
|
||||||
status: output.status,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_command<S>(&self, args: impl IntoIterator<Item = S>) -> smol::process::Command
|
fn build_command<S>(&self, args: impl IntoIterator<Item = S>) -> smol::process::Command
|
||||||
where
|
where
|
||||||
S: AsRef<OsStr>,
|
S: AsRef<OsStr>,
|
||||||
|
@ -1787,9 +1614,8 @@ fn checkpoint_author_envs() -> HashMap<String, String> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::status::{FileStatus, StatusCode, TrackedStatus};
|
use crate::status::FileStatus;
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
use unindent::Unindent;
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_checkpoint_basic(cx: &mut TestAppContext) {
|
async fn test_checkpoint_basic(cx: &mut TestAppContext) {
|
||||||
|
@ -1969,7 +1795,7 @@ mod tests {
|
||||||
"content2"
|
"content2"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
repo.status(None, &[]).await.unwrap().entries.as_ref(),
|
repo.status_blocking(&[]).unwrap().entries.as_ref(),
|
||||||
&[
|
&[
|
||||||
(RepoPath::from_str("new_file1"), FileStatus::Untracked),
|
(RepoPath::from_str("new_file1"), FileStatus::Untracked),
|
||||||
(RepoPath::from_str("new_file2"), FileStatus::Untracked)
|
(RepoPath::from_str("new_file2"), FileStatus::Untracked)
|
||||||
|
@ -2008,90 +1834,6 @@ mod tests {
|
||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_secondary_indices(cx: &mut TestAppContext) {
|
|
||||||
cx.executor().allow_parking();
|
|
||||||
|
|
||||||
let repo_dir = tempfile::tempdir().unwrap();
|
|
||||||
git2::Repository::init(repo_dir.path()).unwrap();
|
|
||||||
let repo =
|
|
||||||
RealGitRepository::new(&repo_dir.path().join(".git"), None, cx.executor()).unwrap();
|
|
||||||
let index = repo.create_index().await.unwrap();
|
|
||||||
smol::fs::write(repo_dir.path().join("file1"), "file1\n")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
smol::fs::write(repo_dir.path().join("file2"), "file2\n")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let diff = r#"
|
|
||||||
diff --git a/file2 b/file2
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000..cbc4e2e
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/file2
|
|
||||||
@@ -0,0 +1 @@
|
|
||||||
+file2
|
|
||||||
"#
|
|
||||||
.unindent();
|
|
||||||
repo.apply_diff(index, diff.to_string()).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
repo.status(Some(index), &[])
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.entries
|
|
||||||
.as_ref(),
|
|
||||||
vec![
|
|
||||||
(RepoPath::from_str("file1"), FileStatus::Untracked),
|
|
||||||
(
|
|
||||||
RepoPath::from_str("file2"),
|
|
||||||
FileStatus::index(StatusCode::Added)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
repo.load_index_text(Some(index), RepoPath::from_str("file1"))
|
|
||||||
.await,
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
repo.load_index_text(Some(index), RepoPath::from_str("file2"))
|
|
||||||
.await,
|
|
||||||
Some("file2\n".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
smol::fs::write(repo_dir.path().join("file2"), "file2-changed\n")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
repo.status(Some(index), &[])
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.entries
|
|
||||||
.as_ref(),
|
|
||||||
vec![
|
|
||||||
(RepoPath::from_str("file1"), FileStatus::Untracked),
|
|
||||||
(
|
|
||||||
RepoPath::from_str("file2"),
|
|
||||||
FileStatus::Tracked(TrackedStatus {
|
|
||||||
worktree_status: StatusCode::Modified,
|
|
||||||
index_status: StatusCode::Added,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
repo.load_index_text(Some(index), RepoPath::from_str("file1"))
|
|
||||||
.await,
|
|
||||||
None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
repo.load_index_text(Some(index), RepoPath::from_str("file2"))
|
|
||||||
.await,
|
|
||||||
Some("file2\n".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_branches_parsing() {
|
fn test_branches_parsing() {
|
||||||
// suppress "help: octal escapes are not supported, `\0` is always null"
|
// suppress "help: octal escapes are not supported, `\0` is always null"
|
||||||
|
|
|
@ -1320,7 +1320,7 @@ impl Buffer {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
if this.version() == diff.base_version {
|
if this.version() == diff.base_version {
|
||||||
this.finalize_last_transaction();
|
this.finalize_last_transaction();
|
||||||
this.apply_diff(diff, cx);
|
this.apply_diff(diff, true, cx);
|
||||||
tx.send(this.finalize_last_transaction().cloned()).ok();
|
tx.send(this.finalize_last_transaction().cloned()).ok();
|
||||||
this.has_conflict = false;
|
this.has_conflict = false;
|
||||||
this.did_reload(this.version(), this.line_ending(), new_mtime, cx);
|
this.did_reload(this.version(), this.line_ending(), new_mtime, cx);
|
||||||
|
@ -1879,9 +1879,14 @@ impl Buffer {
|
||||||
/// Applies a diff to the buffer. If the buffer has changed since the given diff was
|
/// Applies a diff to the buffer. If the buffer has changed since the given diff was
|
||||||
/// calculated, then adjust the diff to account for those changes, and discard any
|
/// calculated, then adjust the diff to account for those changes, and discard any
|
||||||
/// parts of the diff that conflict with those changes.
|
/// parts of the diff that conflict with those changes.
|
||||||
pub fn apply_diff(&mut self, diff: Diff, cx: &mut Context<Self>) -> Option<TransactionId> {
|
///
|
||||||
// Check for any edits to the buffer that have occurred since this diff
|
/// If `atomic` is true, the diff will be applied as a single edit.
|
||||||
// was computed.
|
pub fn apply_diff(
|
||||||
|
&mut self,
|
||||||
|
diff: Diff,
|
||||||
|
atomic: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<TransactionId> {
|
||||||
let snapshot = self.snapshot();
|
let snapshot = self.snapshot();
|
||||||
let mut edits_since = snapshot.edits_since::<usize>(&diff.base_version).peekable();
|
let mut edits_since = snapshot.edits_since::<usize>(&diff.base_version).peekable();
|
||||||
let mut delta = 0;
|
let mut delta = 0;
|
||||||
|
@ -1911,7 +1916,17 @@ impl Buffer {
|
||||||
|
|
||||||
self.start_transaction();
|
self.start_transaction();
|
||||||
self.text.set_line_ending(diff.line_ending);
|
self.text.set_line_ending(diff.line_ending);
|
||||||
self.edit(adjusted_edits, None, cx);
|
if atomic {
|
||||||
|
self.edit(adjusted_edits, None, cx);
|
||||||
|
} else {
|
||||||
|
let mut delta = 0isize;
|
||||||
|
for (range, new_text) in adjusted_edits {
|
||||||
|
let adjusted_range =
|
||||||
|
(range.start as isize + delta) as usize..(range.end as isize + delta) as usize;
|
||||||
|
delta += new_text.len() as isize - range.len() as isize;
|
||||||
|
self.edit([(adjusted_range, new_text)], None, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.end_transaction(cx)
|
self.end_transaction(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -374,7 +374,7 @@ async fn test_apply_diff(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
let diff = buffer.update(cx, |b, cx| b.diff(text.clone(), cx)).await;
|
let diff = buffer.update(cx, |b, cx| b.diff(text.clone(), cx)).await;
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
buffer.apply_diff(diff, cx).unwrap();
|
buffer.apply_diff(diff, true, cx).unwrap();
|
||||||
assert_eq!(buffer.text(), text);
|
assert_eq!(buffer.text(), text);
|
||||||
let actual_offsets = anchors
|
let actual_offsets = anchors
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -388,7 +388,7 @@ async fn test_apply_diff(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
let diff = buffer.update(cx, |b, cx| b.diff(text.clone(), cx)).await;
|
let diff = buffer.update(cx, |b, cx| b.diff(text.clone(), cx)).await;
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
buffer.apply_diff(diff, cx).unwrap();
|
buffer.apply_diff(diff, true, cx).unwrap();
|
||||||
assert_eq!(buffer.text(), text);
|
assert_eq!(buffer.text(), text);
|
||||||
let actual_offsets = anchors
|
let actual_offsets = anchors
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -433,7 +433,7 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
|
||||||
let format_diff = format.await;
|
let format_diff = format.await;
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
let version_before_format = format_diff.base_version.clone();
|
let version_before_format = format_diff.base_version.clone();
|
||||||
buffer.apply_diff(format_diff, cx);
|
buffer.apply_diff(format_diff, true, cx);
|
||||||
|
|
||||||
// The outcome depends on the order of concurrent tasks.
|
// The outcome depends on the order of concurrent tasks.
|
||||||
//
|
//
|
||||||
|
|
|
@ -20,10 +20,10 @@ use git::{
|
||||||
blame::Blame,
|
blame::Blame,
|
||||||
parse_git_remote_url,
|
parse_git_remote_url,
|
||||||
repository::{
|
repository::{
|
||||||
Branch, CommitDetails, DiffType, GitIndex, GitRepository, GitRepositoryCheckpoint,
|
Branch, CommitDetails, DiffType, GitRepository, GitRepositoryCheckpoint, PushOptions,
|
||||||
PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode,
|
Remote, RemoteCommandOutput, RepoPath, ResetMode,
|
||||||
},
|
},
|
||||||
status::{FileStatus, GitStatus},
|
status::FileStatus,
|
||||||
BuildPermalinkParams, GitHostingProviderRegistry,
|
BuildPermalinkParams, GitHostingProviderRegistry,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -146,22 +146,6 @@ pub struct GitStoreCheckpoint {
|
||||||
checkpoints_by_work_dir_abs_path: HashMap<PathBuf, GitRepositoryCheckpoint>,
|
checkpoints_by_work_dir_abs_path: HashMap<PathBuf, GitRepositoryCheckpoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct GitStoreDiff {
|
|
||||||
diffs_by_work_dir_abs_path: HashMap<PathBuf, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct GitStoreIndex {
|
|
||||||
indices_by_work_dir_abs_path: HashMap<PathBuf, GitIndex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct GitStoreStatus {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
statuses_by_work_dir_abs_path: HashMap<PathBuf, GitStatus>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Repository {
|
pub struct Repository {
|
||||||
pub repository_entry: RepositoryEntry,
|
pub repository_entry: RepositoryEntry,
|
||||||
pub merge_message: Option<String>,
|
pub merge_message: Option<String>,
|
||||||
|
@ -755,113 +739,6 @@ impl GitStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diff_checkpoints(
|
|
||||||
&self,
|
|
||||||
base_checkpoint: GitStoreCheckpoint,
|
|
||||||
target_checkpoint: GitStoreCheckpoint,
|
|
||||||
cx: &App,
|
|
||||||
) -> Task<Result<GitStoreDiff>> {
|
|
||||||
let repositories_by_work_dir_abs_path = self
|
|
||||||
.repositories
|
|
||||||
.values()
|
|
||||||
.map(|repo| {
|
|
||||||
(
|
|
||||||
repo.read(cx)
|
|
||||||
.repository_entry
|
|
||||||
.work_directory_abs_path
|
|
||||||
.clone(),
|
|
||||||
repo,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
for (work_dir_abs_path, base_checkpoint) in base_checkpoint.checkpoints_by_work_dir_abs_path
|
|
||||||
{
|
|
||||||
if let Some(target_checkpoint) = target_checkpoint
|
|
||||||
.checkpoints_by_work_dir_abs_path
|
|
||||||
.get(&work_dir_abs_path)
|
|
||||||
.cloned()
|
|
||||||
{
|
|
||||||
if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
|
|
||||||
{
|
|
||||||
let diff = repository
|
|
||||||
.read(cx)
|
|
||||||
.diff_checkpoints(base_checkpoint, target_checkpoint);
|
|
||||||
tasks.push(async move {
|
|
||||||
let diff = diff.await??;
|
|
||||||
anyhow::Ok((work_dir_abs_path, diff))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let diffs_by_path = future::try_join_all(tasks).await?;
|
|
||||||
Ok(GitStoreDiff {
|
|
||||||
diffs_by_work_dir_abs_path: diffs_by_path.into_iter().collect(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_index(&self, cx: &App) -> Task<Result<GitStoreIndex>> {
|
|
||||||
let mut indices = Vec::new();
|
|
||||||
for repository in self.repositories.values() {
|
|
||||||
let repository = repository.read(cx);
|
|
||||||
let work_dir_abs_path = repository.repository_entry.work_directory_abs_path.clone();
|
|
||||||
let index = repository.create_index().map(|index| index?);
|
|
||||||
indices.push(async move {
|
|
||||||
let index = index.await?;
|
|
||||||
anyhow::Ok((work_dir_abs_path, index))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.background_executor().spawn(async move {
|
|
||||||
let indices = future::try_join_all(indices).await?;
|
|
||||||
Ok(GitStoreIndex {
|
|
||||||
indices_by_work_dir_abs_path: indices.into_iter().collect(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_diff(
|
|
||||||
&self,
|
|
||||||
mut index: GitStoreIndex,
|
|
||||||
diff: GitStoreDiff,
|
|
||||||
cx: &App,
|
|
||||||
) -> Task<Result<()>> {
|
|
||||||
let repositories_by_work_dir_abs_path = self
|
|
||||||
.repositories
|
|
||||||
.values()
|
|
||||||
.map(|repo| {
|
|
||||||
(
|
|
||||||
repo.read(cx)
|
|
||||||
.repository_entry
|
|
||||||
.work_directory_abs_path
|
|
||||||
.clone(),
|
|
||||||
repo,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
for (work_dir_abs_path, diff) in diff.diffs_by_work_dir_abs_path {
|
|
||||||
if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) {
|
|
||||||
if let Some(branch) = index
|
|
||||||
.indices_by_work_dir_abs_path
|
|
||||||
.remove(&work_dir_abs_path)
|
|
||||||
{
|
|
||||||
let apply = repository.read(cx).apply_diff(branch, diff);
|
|
||||||
tasks.push(async move { apply.await? });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
future::try_join_all(tasks).await?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Blames a buffer.
|
/// Blames a buffer.
|
||||||
pub fn blame_buffer(
|
pub fn blame_buffer(
|
||||||
&self,
|
&self,
|
||||||
|
@ -1406,7 +1283,7 @@ impl GitStore {
|
||||||
let index_text = if current_index_text.is_some() {
|
let index_text = if current_index_text.is_some() {
|
||||||
local_repo
|
local_repo
|
||||||
.repo()
|
.repo()
|
||||||
.load_index_text(None, relative_path.clone())
|
.load_index_text(relative_path.clone())
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1521,87 +1398,6 @@ impl GitStore {
|
||||||
Some(status.status)
|
Some(status.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status(&self, index: Option<GitStoreIndex>, cx: &App) -> Task<Result<GitStoreStatus>> {
|
|
||||||
let repositories_by_work_dir_abs_path = self
|
|
||||||
.repositories
|
|
||||||
.values()
|
|
||||||
.map(|repo| {
|
|
||||||
(
|
|
||||||
repo.read(cx)
|
|
||||||
.repository_entry
|
|
||||||
.work_directory_abs_path
|
|
||||||
.clone(),
|
|
||||||
repo,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
|
|
||||||
if let Some(index) = index {
|
|
||||||
// When we have an index, just check the repositories that are part of it
|
|
||||||
for (work_dir_abs_path, git_index) in index.indices_by_work_dir_abs_path {
|
|
||||||
if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
|
|
||||||
{
|
|
||||||
let status = repository.read(cx).status(Some(git_index));
|
|
||||||
tasks.push(
|
|
||||||
async move {
|
|
||||||
let status = status.await??;
|
|
||||||
anyhow::Ok((work_dir_abs_path, status))
|
|
||||||
}
|
|
||||||
.boxed(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Otherwise, check all repositories
|
|
||||||
for repository in self.repositories.values() {
|
|
||||||
let repository = repository.read(cx);
|
|
||||||
let work_dir_abs_path = repository.repository_entry.work_directory_abs_path.clone();
|
|
||||||
let status = repository.status(None);
|
|
||||||
tasks.push(
|
|
||||||
async move {
|
|
||||||
let status = status.await??;
|
|
||||||
anyhow::Ok((work_dir_abs_path, status))
|
|
||||||
}
|
|
||||||
.boxed(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.background_executor().spawn(async move {
|
|
||||||
let statuses = future::try_join_all(tasks).await?;
|
|
||||||
Ok(GitStoreStatus {
|
|
||||||
statuses_by_work_dir_abs_path: statuses.into_iter().collect(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_index_text(
|
|
||||||
&self,
|
|
||||||
index: Option<GitStoreIndex>,
|
|
||||||
buffer: &Entity<Buffer>,
|
|
||||||
cx: &App,
|
|
||||||
) -> Task<Option<String>> {
|
|
||||||
let Some((repository, path)) =
|
|
||||||
self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
|
|
||||||
else {
|
|
||||||
return Task::ready(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let git_index = index.and_then(|index| {
|
|
||||||
index
|
|
||||||
.indices_by_work_dir_abs_path
|
|
||||||
.get(&repository.read(cx).repository_entry.work_directory_abs_path)
|
|
||||||
.copied()
|
|
||||||
});
|
|
||||||
let text = repository.read(cx).load_index_text(git_index, path);
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let text = text.await;
|
|
||||||
text.ok().flatten()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn repository_and_path_for_buffer_id(
|
pub fn repository_and_path_for_buffer_id(
|
||||||
&self,
|
&self,
|
||||||
buffer_id: BufferId,
|
buffer_id: BufferId,
|
||||||
|
@ -2851,24 +2647,11 @@ impl Repository {
|
||||||
self.repository_entry.status()
|
self.repository_entry.status()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status(&self, index: Option<GitIndex>) -> oneshot::Receiver<Result<GitStatus>> {
|
pub fn load_index_text(&self, path: RepoPath) -> oneshot::Receiver<Option<String>> {
|
||||||
self.send_job(move |repo, _cx| async move {
|
|
||||||
match repo {
|
|
||||||
RepositoryState::Local(git_repository) => git_repository.status(index, &[]).await,
|
|
||||||
RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_index_text(
|
|
||||||
&self,
|
|
||||||
index: Option<GitIndex>,
|
|
||||||
path: RepoPath,
|
|
||||||
) -> oneshot::Receiver<Option<String>> {
|
|
||||||
self.send_job(move |repo, _cx| async move {
|
self.send_job(move |repo, _cx| async move {
|
||||||
match repo {
|
match repo {
|
||||||
RepositoryState::Local(git_repository) => {
|
RepositoryState::Local(git_repository) => {
|
||||||
git_repository.load_index_text(index, path).await
|
git_repository.load_index_text(path).await
|
||||||
}
|
}
|
||||||
RepositoryState::Remote { .. } => None,
|
RepositoryState::Remote { .. } => None,
|
||||||
}
|
}
|
||||||
|
@ -3779,26 +3562,6 @@ impl Repository {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_index(&self) -> oneshot::Receiver<Result<GitIndex>> {
|
|
||||||
self.send_job(move |repo, _cx| async move {
|
|
||||||
match repo {
|
|
||||||
RepositoryState::Local(git_repository) => git_repository.create_index().await,
|
|
||||||
RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_diff(&self, index: GitIndex, diff: String) -> oneshot::Receiver<Result<()>> {
|
|
||||||
self.send_job(move |repo, _cx| async move {
|
|
||||||
match repo {
|
|
||||||
RepositoryState::Local(git_repository) => {
|
|
||||||
git_repository.apply_diff(index, diff).await
|
|
||||||
}
|
|
||||||
RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_permalink_in_rust_registry_src(
|
fn get_permalink_in_rust_registry_src(
|
||||||
|
|
|
@ -1228,7 +1228,7 @@ impl LocalLspStore {
|
||||||
.await;
|
.await;
|
||||||
buffer.handle.update(cx, |buffer, cx| {
|
buffer.handle.update(cx, |buffer, cx| {
|
||||||
buffer.start_transaction();
|
buffer.start_transaction();
|
||||||
buffer.apply_diff(diff, cx);
|
buffer.apply_diff(diff, true, cx);
|
||||||
transaction_id_format =
|
transaction_id_format =
|
||||||
transaction_id_format.or(buffer.end_transaction(cx));
|
transaction_id_format.or(buffer.end_transaction(cx));
|
||||||
if let Some(transaction_id) = transaction_id_format {
|
if let Some(transaction_id) = transaction_id_format {
|
||||||
|
@ -1362,7 +1362,7 @@ impl LocalLspStore {
|
||||||
zlog::trace!(logger => "Applying changes");
|
zlog::trace!(logger => "Applying changes");
|
||||||
buffer.handle.update(cx, |buffer, cx| {
|
buffer.handle.update(cx, |buffer, cx| {
|
||||||
buffer.start_transaction();
|
buffer.start_transaction();
|
||||||
buffer.apply_diff(diff, cx);
|
buffer.apply_diff(diff, true, cx);
|
||||||
transaction_id_format =
|
transaction_id_format =
|
||||||
transaction_id_format.or(buffer.end_transaction(cx));
|
transaction_id_format.or(buffer.end_transaction(cx));
|
||||||
if let Some(transaction_id) = transaction_id_format {
|
if let Some(transaction_id) = transaction_id_format {
|
||||||
|
@ -1405,7 +1405,7 @@ impl LocalLspStore {
|
||||||
zlog::trace!(logger => "Applying changes");
|
zlog::trace!(logger => "Applying changes");
|
||||||
buffer.handle.update(cx, |buffer, cx| {
|
buffer.handle.update(cx, |buffer, cx| {
|
||||||
buffer.start_transaction();
|
buffer.start_transaction();
|
||||||
buffer.apply_diff(diff, cx);
|
buffer.apply_diff(diff, true, cx);
|
||||||
transaction_id_format =
|
transaction_id_format =
|
||||||
transaction_id_format.or(buffer.end_transaction(cx));
|
transaction_id_format.or(buffer.end_transaction(cx));
|
||||||
if let Some(transaction_id) = transaction_id_format {
|
if let Some(transaction_id) = transaction_id_format {
|
||||||
|
|
|
@ -1498,9 +1498,9 @@ impl Buffer {
|
||||||
.flat_map(|transaction| self.edited_ranges_for_transaction(transaction))
|
.flat_map(|transaction| self.edited_ranges_for_transaction(transaction))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn edited_ranges_for_transaction<'a, D>(
|
pub fn edited_ranges_for_edit_ids<'a, D>(
|
||||||
&'a self,
|
&'a self,
|
||||||
transaction: &'a Transaction,
|
edit_ids: impl IntoIterator<Item = &'a clock::Lamport>,
|
||||||
) -> impl 'a + Iterator<Item = Range<D>>
|
) -> impl 'a + Iterator<Item = Range<D>>
|
||||||
where
|
where
|
||||||
D: TextDimension,
|
D: TextDimension,
|
||||||
|
@ -1508,7 +1508,7 @@ impl Buffer {
|
||||||
// get fragment ranges
|
// get fragment ranges
|
||||||
let mut cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None);
|
let mut cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None);
|
||||||
let offset_ranges = self
|
let offset_ranges = self
|
||||||
.fragment_ids_for_edits(transaction.edit_ids.iter())
|
.fragment_ids_for_edits(edit_ids.into_iter())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(move |fragment_id| {
|
.filter_map(move |fragment_id| {
|
||||||
cursor.seek_forward(&Some(fragment_id), Bias::Left, &None);
|
cursor.seek_forward(&Some(fragment_id), Bias::Left, &None);
|
||||||
|
@ -1547,6 +1547,16 @@ 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 {
|
pub fn subscribe(&mut self) -> Subscription {
|
||||||
self.subscriptions.subscribe()
|
self.subscriptions.subscribe()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1041,10 +1041,7 @@ impl Worktree {
|
||||||
if let Some(git_repo) =
|
if let Some(git_repo) =
|
||||||
snapshot.git_repositories.get(&repo.work_directory_id)
|
snapshot.git_repositories.get(&repo.work_directory_id)
|
||||||
{
|
{
|
||||||
return Ok(git_repo
|
return Ok(git_repo.repo_ptr.load_index_text(repo_path).await);
|
||||||
.repo_ptr
|
|
||||||
.load_index_text(None, repo_path)
|
|
||||||
.await);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue