Skip to content

Commit

Permalink
Proposed changes editor features (#18373)
Browse files Browse the repository at this point in the history
This PR adds some more functionality to the Proposed Changes Editor
view, which we'll be using in
#18240 for allowing the
assistant to propose changes to a set of buffers.

* Add an `Apply All` button, and fully implement applying of changes to
the base buffer
* Make the proposed changes editor searchable
* Fix a bug in branch buffers' diff state management

Release Notes:

- N/A
  • Loading branch information
maxbrunsfeld authored Sep 25, 2024
1 parent 3161aed commit 6167688
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 76 deletions.
4 changes: 3 additions & 1 deletion crates/editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ use language::{
};
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges;
use proposed_changes_editor::{ProposedChangesBuffer, ProposedChangesEditor};
pub use proposed_changes_editor::{
ProposedChangesBuffer, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
use similar::{ChangeTag, TextDiff};
use task::{ResolvedTask, TaskTemplate, TaskVariables};

Expand Down
84 changes: 82 additions & 2 deletions crates/editor/src/proposed_changes_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ use language::{Buffer, BufferEvent, Capability};
use multi_buffer::{ExcerptRange, MultiBuffer};
use project::Project;
use smol::stream::StreamExt;
use std::{ops::Range, time::Duration};
use std::{any::TypeId, ops::Range, time::Duration};
use text::ToOffset;
use ui::prelude::*;
use workspace::Item;
use workspace::{
searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView,
};

pub struct ProposedChangesEditor {
editor: View<Editor>,
Expand All @@ -23,6 +26,10 @@ pub struct ProposedChangesBuffer<T> {
pub ranges: Vec<Range<T>>,
}

pub struct ProposedChangesEditorToolbar {
current_editor: Option<View<ProposedChangesEditor>>,
}

impl ProposedChangesEditor {
pub fn new<T: ToOffset>(
buffers: Vec<ProposedChangesBuffer<T>>,
Expand Down Expand Up @@ -96,6 +103,17 @@ impl ProposedChangesEditor {
self.recalculate_diffs_tx.unbounded_send(buffer).ok();
}
}

fn apply_all_changes(&self, cx: &mut ViewContext<Self>) {
let buffers = self.editor.read(cx).buffer.read(cx).all_buffers();
for branch_buffer in buffers {
if let Some(base_buffer) = branch_buffer.read(cx).diff_base_buffer() {
base_buffer.update(cx, |base_buffer, cx| {
base_buffer.merge(&branch_buffer, None, cx)
});
}
}
}
}

impl Render for ProposedChangesEditor {
Expand All @@ -122,4 +140,66 @@ impl Item for ProposedChangesEditor {
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
Some("Proposed changes".into())
}

fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}

fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a View<Self>,
_: &'a AppContext,
) -> Option<gpui::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
}
}
}

impl ProposedChangesEditorToolbar {
pub fn new() -> Self {
Self {
current_editor: None,
}
}

fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
if self.current_editor.is_some() {
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
}
}

impl Render for ProposedChangesEditorToolbar {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let editor = self.current_editor.clone();
Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
if let Some(editor) = &editor {
editor.update(cx, |editor, cx| {
editor.apply_all_changes(cx);
});
}
})
}
}

impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}

impl ToolbarItemView for ProposedChangesEditorToolbar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>,
_cx: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
self.current_editor =
active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
self.get_toolbar_item_location()
}
}
130 changes: 96 additions & 34 deletions crates/language/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ pub type BufferRow = u32;
#[derive(Clone)]
enum BufferDiffBase {
Git(Rope),
PastBufferVersion(Model<Buffer>, BufferSnapshot),
PastBufferVersion {
buffer: Model<Buffer>,
rope: Rope,
operations_to_ignore: Vec<clock::Lamport>,
},
}

/// An in-memory representation of a source code file, including its text,
Expand Down Expand Up @@ -795,19 +799,15 @@ impl Buffer {
let this = cx.handle();
cx.new_model(|cx| {
let mut branch = Self {
diff_base: Some(BufferDiffBase::PastBufferVersion(
this.clone(),
self.snapshot(),
)),
diff_base: Some(BufferDiffBase::PastBufferVersion {
buffer: this.clone(),
rope: self.as_rope().clone(),
operations_to_ignore: Vec::new(),
}),
language: self.language.clone(),
has_conflict: self.has_conflict,
has_unsaved_edits: Cell::new(self.has_unsaved_edits.get_mut().clone()),
_subscriptions: vec![cx.subscribe(&this, |branch: &mut Self, _, event, cx| {
if let BufferEvent::Operation { operation, .. } = event {
branch.apply_ops([operation.clone()], cx);
branch.diff_base_version += 1;
}
})],
_subscriptions: vec![cx.subscribe(&this, Self::on_base_buffer_event)],
..Self::build(
self.text.branch(),
None,
Expand All @@ -823,18 +823,74 @@ impl Buffer {
})
}

pub fn merge(&mut self, branch: &Model<Self>, cx: &mut ModelContext<Self>) {
let branch = branch.read(cx);
let edits = branch
.edits_since::<usize>(&self.version)
.map(|edit| {
(
edit.old,
branch.text_for_range(edit.new).collect::<String>(),
/// Applies all of the changes in `branch` buffer that intersect the given `range`
/// to this buffer.
pub fn merge(
&mut self,
branch: &Model<Self>,
range: Option<Range<Anchor>>,
cx: &mut ModelContext<Self>,
) {
let edits = branch.read_with(cx, |branch, _| {
branch
.edits_since_in_range::<usize>(
&self.version,
range.unwrap_or(Anchor::MIN..Anchor::MAX),
)
})
.collect::<Vec<_>>();
self.edit(edits, None, cx);
.map(|edit| {
(
edit.old,
branch.text_for_range(edit.new).collect::<String>(),
)
})
.collect::<Vec<_>>()
});
let operation = self.edit(edits, None, cx);

// Prevent this operation from being reapplied to the branch.
branch.update(cx, |branch, cx| {
if let Some(BufferDiffBase::PastBufferVersion {
operations_to_ignore,
..
}) = &mut branch.diff_base
{
operations_to_ignore.extend(operation);
}
cx.emit(BufferEvent::Edited)
});
}

fn on_base_buffer_event(
&mut self,
_: Model<Buffer>,
event: &BufferEvent,
cx: &mut ModelContext<Self>,
) {
if let BufferEvent::Operation { operation, .. } = event {
if let Some(BufferDiffBase::PastBufferVersion {
operations_to_ignore,
..
}) = &mut self.diff_base
{
let mut is_ignored = false;
if let Operation::Buffer(text::Operation::Edit(buffer_operation)) = &operation {
operations_to_ignore.retain(|operation_to_ignore| {
match buffer_operation.timestamp.cmp(&operation_to_ignore) {
Ordering::Less => true,
Ordering::Equal => {
is_ignored = true;
false
}
Ordering::Greater => false,
}
});
}
if !is_ignored {
self.apply_ops([operation.clone()], cx);
self.diff_base_version += 1;
}
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -1017,9 +1073,8 @@ impl Buffer {
/// Returns the current diff base, see [Buffer::set_diff_base].
pub fn diff_base(&self) -> Option<&Rope> {
match self.diff_base.as_ref()? {
BufferDiffBase::Git(rope) => Some(rope),
BufferDiffBase::PastBufferVersion(_, buffer_snapshot) => {
Some(buffer_snapshot.as_rope())
BufferDiffBase::Git(rope) | BufferDiffBase::PastBufferVersion { rope, .. } => {
Some(rope)
}
}
}
Expand Down Expand Up @@ -1050,29 +1105,36 @@ impl Buffer {
self.diff_base_version
}

pub fn diff_base_buffer(&self) -> Option<Model<Self>> {
match self.diff_base.as_ref()? {
BufferDiffBase::Git(_) => None,
BufferDiffBase::PastBufferVersion { buffer, .. } => Some(buffer.clone()),
}
}

/// Recomputes the diff.
pub fn recalculate_diff(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<()>> {
let diff_base_rope = match self.diff_base.as_mut()? {
let diff_base_rope = match self.diff_base.as_ref()? {
BufferDiffBase::Git(rope) => rope.clone(),
BufferDiffBase::PastBufferVersion(base_buffer, base_buffer_snapshot) => {
let new_base_snapshot = base_buffer.read(cx).snapshot();
*base_buffer_snapshot = new_base_snapshot;
base_buffer_snapshot.as_rope().clone()
}
BufferDiffBase::PastBufferVersion { buffer, .. } => buffer.read(cx).as_rope().clone(),
};
let snapshot = self.snapshot();

let snapshot = self.snapshot();
let mut diff = self.git_diff.clone();
let diff = cx.background_executor().spawn(async move {
diff.update(&diff_base_rope, &snapshot).await;
diff
(diff, diff_base_rope)
});

Some(cx.spawn(|this, mut cx| async move {
let buffer_diff = diff.await;
let (buffer_diff, diff_base_rope) = diff.await;
this.update(&mut cx, |this, cx| {
this.git_diff = buffer_diff;
this.non_text_state_update_count += 1;
if let Some(BufferDiffBase::PastBufferVersion { rope, .. }) = &mut this.diff_base {
*rope = diff_base_rope;
cx.emit(BufferEvent::DiffBaseChanged);
}
cx.emit(BufferEvent::DiffUpdated);
})
.ok();
Expand Down
Loading

0 comments on commit 6167688

Please sign in to comment.