mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-28 09:54:33 +00:00
Add the ability to propose changes to a set of buffers (#18170)
This PR introduces functionality for creating *branches* of buffers that can be used to preview and edit change sets that haven't yet been applied to the buffers themselves. Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <elliott.codes@gmail.com> Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
parent
e309fbda2a
commit
743feb98bc
20 changed files with 622 additions and 186 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -7055,7 +7055,6 @@ dependencies = [
|
|||
"ctor",
|
||||
"env_logger",
|
||||
"futures 0.3.30",
|
||||
"git",
|
||||
"gpui",
|
||||
"itertools 0.13.0",
|
||||
"language",
|
||||
|
|
|
@ -1006,9 +1006,12 @@ impl Context {
|
|||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
language::BufferEvent::Operation(operation) => cx.emit(ContextEvent::Operation(
|
||||
ContextOperation::BufferOperation(operation.clone()),
|
||||
)),
|
||||
language::BufferEvent::Operation {
|
||||
operation,
|
||||
is_local: true,
|
||||
} => cx.emit(ContextEvent::Operation(ContextOperation::BufferOperation(
|
||||
operation.clone(),
|
||||
))),
|
||||
language::BufferEvent::Edited => {
|
||||
self.count_remaining_tokens(cx);
|
||||
self.reparse(cx);
|
||||
|
|
|
@ -175,7 +175,10 @@ impl ChannelBuffer {
|
|||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
language::BufferEvent::Operation(operation) => {
|
||||
language::BufferEvent::Operation {
|
||||
operation,
|
||||
is_local: true,
|
||||
} => {
|
||||
if *ZED_ALWAYS_ACTIVE {
|
||||
if let language::Operation::UpdateSelections { selections, .. } = operation {
|
||||
if selections.is_empty() {
|
||||
|
|
|
@ -9,6 +9,8 @@ use std::{
|
|||
|
||||
pub use system_clock::*;
|
||||
|
||||
pub const LOCAL_BRANCH_REPLICA_ID: u16 = u16::MAX;
|
||||
|
||||
/// A unique identifier for each distributed node.
|
||||
pub type ReplicaId = u16;
|
||||
|
||||
|
@ -25,7 +27,10 @@ pub struct Lamport {
|
|||
|
||||
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock).
|
||||
#[derive(Clone, Default, Hash, Eq, PartialEq)]
|
||||
pub struct Global(SmallVec<[u32; 8]>);
|
||||
pub struct Global {
|
||||
values: SmallVec<[u32; 8]>,
|
||||
local_branch_value: u32,
|
||||
}
|
||||
|
||||
impl Global {
|
||||
pub fn new() -> Self {
|
||||
|
@ -33,41 +38,51 @@ impl Global {
|
|||
}
|
||||
|
||||
pub fn get(&self, replica_id: ReplicaId) -> Seq {
|
||||
self.0.get(replica_id as usize).copied().unwrap_or(0) as Seq
|
||||
if replica_id == LOCAL_BRANCH_REPLICA_ID {
|
||||
self.local_branch_value
|
||||
} else {
|
||||
self.values.get(replica_id as usize).copied().unwrap_or(0) as Seq
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observe(&mut self, timestamp: Lamport) {
|
||||
if timestamp.value > 0 {
|
||||
let new_len = timestamp.replica_id as usize + 1;
|
||||
if new_len > self.0.len() {
|
||||
self.0.resize(new_len, 0);
|
||||
}
|
||||
if timestamp.replica_id == LOCAL_BRANCH_REPLICA_ID {
|
||||
self.local_branch_value = cmp::max(self.local_branch_value, timestamp.value);
|
||||
} else {
|
||||
let new_len = timestamp.replica_id as usize + 1;
|
||||
if new_len > self.values.len() {
|
||||
self.values.resize(new_len, 0);
|
||||
}
|
||||
|
||||
let entry = &mut self.0[timestamp.replica_id as usize];
|
||||
*entry = cmp::max(*entry, timestamp.value);
|
||||
let entry = &mut self.values[timestamp.replica_id as usize];
|
||||
*entry = cmp::max(*entry, timestamp.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(&mut self, other: &Self) {
|
||||
if other.0.len() > self.0.len() {
|
||||
self.0.resize(other.0.len(), 0);
|
||||
if other.values.len() > self.values.len() {
|
||||
self.values.resize(other.values.len(), 0);
|
||||
}
|
||||
|
||||
for (left, right) in self.0.iter_mut().zip(&other.0) {
|
||||
for (left, right) in self.values.iter_mut().zip(&other.values) {
|
||||
*left = cmp::max(*left, *right);
|
||||
}
|
||||
|
||||
self.local_branch_value = cmp::max(self.local_branch_value, other.local_branch_value);
|
||||
}
|
||||
|
||||
pub fn meet(&mut self, other: &Self) {
|
||||
if other.0.len() > self.0.len() {
|
||||
self.0.resize(other.0.len(), 0);
|
||||
if other.values.len() > self.values.len() {
|
||||
self.values.resize(other.values.len(), 0);
|
||||
}
|
||||
|
||||
let mut new_len = 0;
|
||||
for (ix, (left, right)) in self
|
||||
.0
|
||||
.values
|
||||
.iter_mut()
|
||||
.zip(other.0.iter().chain(iter::repeat(&0)))
|
||||
.zip(other.values.iter().chain(iter::repeat(&0)))
|
||||
.enumerate()
|
||||
{
|
||||
if *left == 0 {
|
||||
|
@ -80,7 +95,8 @@ impl Global {
|
|||
new_len = ix + 1;
|
||||
}
|
||||
}
|
||||
self.0.resize(new_len, 0);
|
||||
self.values.resize(new_len, 0);
|
||||
self.local_branch_value = cmp::min(self.local_branch_value, other.local_branch_value);
|
||||
}
|
||||
|
||||
pub fn observed(&self, timestamp: Lamport) -> bool {
|
||||
|
@ -88,34 +104,44 @@ impl Global {
|
|||
}
|
||||
|
||||
pub fn observed_any(&self, other: &Self) -> bool {
|
||||
self.0
|
||||
self.values
|
||||
.iter()
|
||||
.zip(other.0.iter())
|
||||
.zip(other.values.iter())
|
||||
.any(|(left, right)| *right > 0 && left >= right)
|
||||
|| (other.local_branch_value > 0 && self.local_branch_value >= other.local_branch_value)
|
||||
}
|
||||
|
||||
pub fn observed_all(&self, other: &Self) -> bool {
|
||||
let mut rhs = other.0.iter();
|
||||
self.0.iter().all(|left| match rhs.next() {
|
||||
let mut rhs = other.values.iter();
|
||||
self.values.iter().all(|left| match rhs.next() {
|
||||
Some(right) => left >= right,
|
||||
None => true,
|
||||
}) && rhs.next().is_none()
|
||||
&& self.local_branch_value >= other.local_branch_value
|
||||
}
|
||||
|
||||
pub fn changed_since(&self, other: &Self) -> bool {
|
||||
self.0.len() > other.0.len()
|
||||
self.values.len() > other.values.len()
|
||||
|| self
|
||||
.0
|
||||
.values
|
||||
.iter()
|
||||
.zip(other.0.iter())
|
||||
.zip(other.values.iter())
|
||||
.any(|(left, right)| left > right)
|
||||
|| self.local_branch_value > other.local_branch_value
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = Lamport> + '_ {
|
||||
self.0.iter().enumerate().map(|(replica_id, seq)| Lamport {
|
||||
replica_id: replica_id as ReplicaId,
|
||||
value: *seq,
|
||||
})
|
||||
self.values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(replica_id, seq)| Lamport {
|
||||
replica_id: replica_id as ReplicaId,
|
||||
value: *seq,
|
||||
})
|
||||
.chain((self.local_branch_value > 0).then_some(Lamport {
|
||||
replica_id: LOCAL_BRANCH_REPLICA_ID,
|
||||
value: self.local_branch_value,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,6 +218,9 @@ impl fmt::Debug for Global {
|
|||
}
|
||||
write!(f, "{}: {}", timestamp.replica_id, timestamp.value)?;
|
||||
}
|
||||
if self.local_branch_value > 0 {
|
||||
write!(f, "<branch>: {}", self.local_branch_value)?;
|
||||
}
|
||||
write!(f, "}}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -273,6 +273,7 @@ gpui::actions!(
|
|||
NextScreen,
|
||||
OpenExcerpts,
|
||||
OpenExcerptsSplit,
|
||||
OpenProposedChangesEditor,
|
||||
OpenFile,
|
||||
OpenPermalinkToLine,
|
||||
OpenUrl,
|
||||
|
|
|
@ -35,6 +35,7 @@ mod lsp_ext;
|
|||
mod mouse_context_menu;
|
||||
pub mod movement;
|
||||
mod persistence;
|
||||
mod proposed_changes_editor;
|
||||
mod rust_analyzer_ext;
|
||||
pub mod scroll;
|
||||
mod selections_collection;
|
||||
|
@ -46,7 +47,7 @@ mod signature_help;
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
use ::git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use ::git::diff::DiffHunkStatus;
|
||||
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
||||
pub(crate) use actions::*;
|
||||
use aho_corasick::AhoCorasick;
|
||||
|
@ -98,6 +99,7 @@ use language::{
|
|||
};
|
||||
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
|
||||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
use proposed_changes_editor::{ProposedChangesBuffer, ProposedChangesEditor};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||
|
||||
|
@ -113,7 +115,9 @@ pub use multi_buffer::{
|
|||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
|
||||
ToPoint,
|
||||
};
|
||||
use multi_buffer::{ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16};
|
||||
use multi_buffer::{
|
||||
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
||||
|
@ -6152,7 +6156,7 @@ impl Editor {
|
|||
pub fn prepare_revert_change(
|
||||
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
|
||||
multi_buffer: &Model<MultiBuffer>,
|
||||
hunk: &DiffHunk<MultiBufferRow>,
|
||||
hunk: &MultiBufferDiffHunk,
|
||||
cx: &AppContext,
|
||||
) -> Option<()> {
|
||||
let buffer = multi_buffer.read(cx).buffer(hunk.buffer_id)?;
|
||||
|
@ -9338,7 +9342,7 @@ impl Editor {
|
|||
snapshot: &DisplaySnapshot,
|
||||
initial_point: Point,
|
||||
is_wrapped: bool,
|
||||
hunks: impl Iterator<Item = DiffHunk<MultiBufferRow>>,
|
||||
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> bool {
|
||||
let display_point = initial_point.to_display_point(snapshot);
|
||||
|
@ -11885,6 +11889,52 @@ impl Editor {
|
|||
self.searchable
|
||||
}
|
||||
|
||||
fn open_proposed_changes_editor(
|
||||
&mut self,
|
||||
_: &OpenProposedChangesEditor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace() else {
|
||||
cx.propagate();
|
||||
return;
|
||||
};
|
||||
|
||||
let buffer = self.buffer.read(cx);
|
||||
let mut new_selections_by_buffer = HashMap::default();
|
||||
for selection in self.selections.all::<usize>(cx) {
|
||||
for (buffer, mut range, _) in
|
||||
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
|
||||
{
|
||||
if selection.reversed {
|
||||
mem::swap(&mut range.start, &mut range.end);
|
||||
}
|
||||
let mut range = range.to_point(buffer.read(cx));
|
||||
range.start.column = 0;
|
||||
range.end.column = buffer.read(cx).line_len(range.end.row);
|
||||
new_selections_by_buffer
|
||||
.entry(buffer)
|
||||
.or_insert(Vec::new())
|
||||
.push(range)
|
||||
}
|
||||
}
|
||||
|
||||
let proposed_changes_buffers = new_selections_by_buffer
|
||||
.into_iter()
|
||||
.map(|(buffer, ranges)| ProposedChangesBuffer { buffer, ranges })
|
||||
.collect::<Vec<_>>();
|
||||
let proposed_changes_editor = cx.new_view(|cx| {
|
||||
ProposedChangesEditor::new(proposed_changes_buffers, self.project.clone(), cx)
|
||||
});
|
||||
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(proposed_changes_editor), true, true, None, cx);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn open_excerpts_in_split(&mut self, _: &OpenExcerptsSplit, cx: &mut ViewContext<Self>) {
|
||||
self.open_excerpts_common(true, cx)
|
||||
}
|
||||
|
@ -12399,7 +12449,7 @@ impl Editor {
|
|||
fn hunks_for_selections(
|
||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||
selections: &[Selection<Anchor>],
|
||||
) -> Vec<DiffHunk<MultiBufferRow>> {
|
||||
) -> Vec<MultiBufferDiffHunk> {
|
||||
let buffer_rows_for_selections = selections.iter().map(|selection| {
|
||||
let head = selection.head();
|
||||
let tail = selection.tail();
|
||||
|
@ -12418,7 +12468,7 @@ fn hunks_for_selections(
|
|||
pub fn hunks_for_rows(
|
||||
rows: impl Iterator<Item = Range<MultiBufferRow>>,
|
||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||
) -> Vec<DiffHunk<MultiBufferRow>> {
|
||||
) -> Vec<MultiBufferDiffHunk> {
|
||||
let mut hunks = Vec::new();
|
||||
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
|
||||
HashMap::default();
|
||||
|
@ -12430,14 +12480,14 @@ pub fn hunks_for_rows(
|
|||
// when the caret is just above or just below the deleted hunk.
|
||||
let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed;
|
||||
let related_to_selection = if allow_adjacent {
|
||||
hunk.associated_range.overlaps(&query_rows)
|
||||
|| hunk.associated_range.start == query_rows.end
|
||||
|| hunk.associated_range.end == query_rows.start
|
||||
hunk.row_range.overlaps(&query_rows)
|
||||
|| hunk.row_range.start == query_rows.end
|
||||
|| hunk.row_range.end == query_rows.start
|
||||
} else {
|
||||
// `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected)
|
||||
// `hunk.associated_range` is exclusive (e.g. [2..3] means 2nd row is selected)
|
||||
hunk.associated_range.overlaps(&selected_multi_buffer_rows)
|
||||
|| selected_multi_buffer_rows.end == hunk.associated_range.start
|
||||
// `hunk.row_range` is exclusive (e.g. [2..3] means 2nd row is selected)
|
||||
hunk.row_range.overlaps(&selected_multi_buffer_rows)
|
||||
|| selected_multi_buffer_rows.end == hunk.row_range.start
|
||||
};
|
||||
if related_to_selection {
|
||||
if !processed_buffer_rows
|
||||
|
@ -13738,10 +13788,10 @@ impl RowRangeExt for Range<DisplayRow> {
|
|||
}
|
||||
}
|
||||
|
||||
fn hunk_status(hunk: &DiffHunk<MultiBufferRow>) -> DiffHunkStatus {
|
||||
fn hunk_status(hunk: &MultiBufferDiffHunk) -> DiffHunkStatus {
|
||||
if hunk.diff_base_byte_range.is_empty() {
|
||||
DiffHunkStatus::Added
|
||||
} else if hunk.associated_range.is_empty() {
|
||||
} else if hunk.row_range.is_empty() {
|
||||
DiffHunkStatus::Removed
|
||||
} else {
|
||||
DiffHunkStatus::Modified
|
||||
|
|
|
@ -346,6 +346,7 @@ impl EditorElement {
|
|||
register_action(view, cx, Editor::toggle_code_actions);
|
||||
register_action(view, cx, Editor::open_excerpts);
|
||||
register_action(view, cx, Editor::open_excerpts_in_split);
|
||||
register_action(view, cx, Editor::open_proposed_changes_editor);
|
||||
register_action(view, cx, Editor::toggle_soft_wrap);
|
||||
register_action(view, cx, Editor::toggle_tab_bar);
|
||||
register_action(view, cx, Editor::toggle_line_numbers);
|
||||
|
@ -3710,11 +3711,11 @@ impl EditorElement {
|
|||
)
|
||||
.map(|hunk| {
|
||||
let start_display_row =
|
||||
MultiBufferPoint::new(hunk.associated_range.start.0, 0)
|
||||
MultiBufferPoint::new(hunk.row_range.start.0, 0)
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
.row();
|
||||
let mut end_display_row =
|
||||
MultiBufferPoint::new(hunk.associated_range.end.0, 0)
|
||||
MultiBufferPoint::new(hunk.row_range.end.0, 0)
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
.row();
|
||||
if end_display_row != start_display_row {
|
||||
|
|
|
@ -2,9 +2,9 @@ pub mod blame;
|
|||
|
||||
use std::ops::Range;
|
||||
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use git::diff::DiffHunkStatus;
|
||||
use language::Point;
|
||||
use multi_buffer::{Anchor, MultiBufferRow};
|
||||
use multi_buffer::{Anchor, MultiBufferDiffHunk};
|
||||
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
|
@ -49,25 +49,25 @@ impl DisplayDiffHunk {
|
|||
}
|
||||
|
||||
pub fn diff_hunk_to_display(
|
||||
hunk: &DiffHunk<MultiBufferRow>,
|
||||
hunk: &MultiBufferDiffHunk,
|
||||
snapshot: &DisplaySnapshot,
|
||||
) -> DisplayDiffHunk {
|
||||
let hunk_start_point = Point::new(hunk.associated_range.start.0, 0);
|
||||
let hunk_start_point_sub = Point::new(hunk.associated_range.start.0.saturating_sub(1), 0);
|
||||
let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
|
||||
let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0);
|
||||
let hunk_end_point_sub = Point::new(
|
||||
hunk.associated_range
|
||||
hunk.row_range
|
||||
.end
|
||||
.0
|
||||
.saturating_sub(1)
|
||||
.max(hunk.associated_range.start.0),
|
||||
.max(hunk.row_range.start.0),
|
||||
0,
|
||||
);
|
||||
|
||||
let status = hunk_status(hunk);
|
||||
let is_removal = status == DiffHunkStatus::Removed;
|
||||
|
||||
let folds_start = Point::new(hunk.associated_range.start.0.saturating_sub(2), 0);
|
||||
let folds_end = Point::new(hunk.associated_range.end.0 + 2, 0);
|
||||
let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0);
|
||||
let folds_end = Point::new(hunk.row_range.end.0 + 2, 0);
|
||||
let folds_range = folds_start..folds_end;
|
||||
|
||||
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
|
||||
|
@ -87,7 +87,7 @@ pub fn diff_hunk_to_display(
|
|||
} else {
|
||||
let start = hunk_start_point.to_display_point(snapshot).row();
|
||||
|
||||
let hunk_end_row = hunk.associated_range.end.max(hunk.associated_range.start);
|
||||
let hunk_end_row = hunk.row_range.end.max(hunk.row_range.start);
|
||||
let hunk_end_point = Point::new(hunk_end_row.0, 0);
|
||||
|
||||
let multi_buffer_start = snapshot.buffer_snapshot.anchor_after(hunk_start_point);
|
||||
|
@ -288,7 +288,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
snapshot
|
||||
.git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12))
|
||||
.map(|hunk| (hunk_status(&hunk), hunk.associated_range))
|
||||
.map(|hunk| (hunk_status(&hunk), hunk.row_range))
|
||||
.collect::<Vec<_>>(),
|
||||
&expected,
|
||||
);
|
||||
|
@ -296,7 +296,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
snapshot
|
||||
.git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12))
|
||||
.map(|hunk| (hunk_status(&hunk), hunk.associated_range))
|
||||
.map(|hunk| (hunk_status(&hunk), hunk.row_range))
|
||||
.collect::<Vec<_>>(),
|
||||
expected
|
||||
.iter()
|
||||
|
|
|
@ -4,11 +4,12 @@ use std::{
|
|||
};
|
||||
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use git::diff::DiffHunkStatus;
|
||||
use gpui::{Action, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View};
|
||||
use language::Buffer;
|
||||
use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
|
||||
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
|
||||
MultiBufferSnapshot, ToPoint,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use text::{BufferId, Point};
|
||||
|
@ -190,9 +191,9 @@ impl Editor {
|
|||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
|
||||
.filter(|hunk| {
|
||||
let hunk_display_row_range = Point::new(hunk.associated_range.start.0, 0)
|
||||
let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0)
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
..Point::new(hunk.associated_range.end.0, 0)
|
||||
..Point::new(hunk.row_range.end.0, 0)
|
||||
.to_display_point(&snapshot.display_snapshot);
|
||||
let row_range_end =
|
||||
display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row());
|
||||
|
@ -203,7 +204,7 @@ impl Editor {
|
|||
|
||||
fn toggle_hunks_expanded(
|
||||
&mut self,
|
||||
hunks_to_toggle: Vec<DiffHunk<MultiBufferRow>>,
|
||||
hunks_to_toggle: Vec<MultiBufferDiffHunk>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let previous_toggle_task = self.expanded_hunks.hunk_update_tasks.remove(&None);
|
||||
|
@ -274,8 +275,8 @@ impl Editor {
|
|||
});
|
||||
for remaining_hunk in hunks_to_toggle {
|
||||
let remaining_hunk_point_range =
|
||||
Point::new(remaining_hunk.associated_range.start.0, 0)
|
||||
..Point::new(remaining_hunk.associated_range.end.0, 0);
|
||||
Point::new(remaining_hunk.row_range.start.0, 0)
|
||||
..Point::new(remaining_hunk.row_range.end.0, 0);
|
||||
hunks_to_expand.push(HoveredHunk {
|
||||
status: hunk_status(&remaining_hunk),
|
||||
multi_buffer_range: remaining_hunk_point_range
|
||||
|
@ -705,7 +706,7 @@ impl Editor {
|
|||
fn to_diff_hunk(
|
||||
hovered_hunk: &HoveredHunk,
|
||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||
) -> Option<DiffHunk<MultiBufferRow>> {
|
||||
) -> Option<MultiBufferDiffHunk> {
|
||||
let buffer_id = hovered_hunk
|
||||
.multi_buffer_range
|
||||
.start
|
||||
|
@ -716,9 +717,8 @@ fn to_diff_hunk(
|
|||
let point_range = hovered_hunk
|
||||
.multi_buffer_range
|
||||
.to_point(multi_buffer_snapshot);
|
||||
Some(DiffHunk {
|
||||
associated_range: MultiBufferRow(point_range.start.row)
|
||||
..MultiBufferRow(point_range.end.row),
|
||||
Some(MultiBufferDiffHunk {
|
||||
row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
|
||||
buffer_id,
|
||||
buffer_range,
|
||||
diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
|
||||
|
@ -868,7 +868,7 @@ fn editor_with_deleted_text(
|
|||
fn buffer_diff_hunk(
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
row_range: Range<Point>,
|
||||
) -> Option<DiffHunk<MultiBufferRow>> {
|
||||
) -> Option<MultiBufferDiffHunk> {
|
||||
let mut hunks = buffer_snapshot.git_diff_hunks_in_range(
|
||||
MultiBufferRow(row_range.start.row)..MultiBufferRow(row_range.end.row),
|
||||
);
|
||||
|
|
125
crates/editor/src/proposed_changes_editor.rs
Normal file
125
crates/editor/src/proposed_changes_editor.rs
Normal file
|
@ -0,0 +1,125 @@
|
|||
use crate::{Editor, EditorEvent};
|
||||
use collections::HashSet;
|
||||
use futures::{channel::mpsc, future::join_all};
|
||||
use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, Task, View};
|
||||
use language::{Buffer, BufferEvent, Capability};
|
||||
use multi_buffer::{ExcerptRange, MultiBuffer};
|
||||
use project::Project;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{ops::Range, time::Duration};
|
||||
use text::ToOffset;
|
||||
use ui::prelude::*;
|
||||
use workspace::Item;
|
||||
|
||||
pub struct ProposedChangesEditor {
|
||||
editor: View<Editor>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_recalculate_diffs_task: Task<Option<()>>,
|
||||
recalculate_diffs_tx: mpsc::UnboundedSender<Model<Buffer>>,
|
||||
}
|
||||
|
||||
pub struct ProposedChangesBuffer<T> {
|
||||
pub buffer: Model<Buffer>,
|
||||
pub ranges: Vec<Range<T>>,
|
||||
}
|
||||
|
||||
impl ProposedChangesEditor {
|
||||
pub fn new<T: ToOffset>(
|
||||
buffers: Vec<ProposedChangesBuffer<T>>,
|
||||
project: Option<Model<Project>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut subscriptions = Vec::new();
|
||||
let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
|
||||
for buffer in buffers {
|
||||
let branch_buffer = buffer.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
subscriptions.push(cx.subscribe(&branch_buffer, Self::on_buffer_event));
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts(
|
||||
branch_buffer,
|
||||
buffer.ranges.into_iter().map(|range| ExcerptRange {
|
||||
context: range,
|
||||
primary: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
|
||||
|
||||
Self {
|
||||
editor: cx
|
||||
.new_view(|cx| Editor::for_multibuffer(multibuffer.clone(), project, true, cx)),
|
||||
recalculate_diffs_tx,
|
||||
_recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
|
||||
let mut buffers_to_diff = HashSet::default();
|
||||
while let Some(buffer) = recalculate_diffs_rx.next().await {
|
||||
buffers_to_diff.insert(buffer);
|
||||
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(250))
|
||||
.await;
|
||||
let mut had_further_changes = false;
|
||||
while let Ok(next_buffer) = recalculate_diffs_rx.try_next() {
|
||||
buffers_to_diff.insert(next_buffer?);
|
||||
had_further_changes = true;
|
||||
}
|
||||
if !had_further_changes {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
join_all(buffers_to_diff.drain().filter_map(|buffer| {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
|
||||
.ok()?
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
None
|
||||
}),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_buffer_event(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
event: &BufferEvent,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let BufferEvent::Edited = event {
|
||||
self.recalculate_diffs_tx.unbounded_send(buffer).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProposedChangesEditor {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.editor.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ProposedChangesEditor {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
|
||||
|
||||
impl Item for ProposedChangesEditor {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn tab_icon(&self, _cx: &ui::WindowContext) -> Option<Icon> {
|
||||
Some(Icon::new(IconName::Pencil))
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
||||
Some("Proposed changes".into())
|
||||
}
|
||||
}
|
|
@ -108,16 +108,16 @@ pub fn editor_hunks(
|
|||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
|
||||
.map(|hunk| {
|
||||
let display_range = Point::new(hunk.associated_range.start.0, 0)
|
||||
let display_range = Point::new(hunk.row_range.start.0, 0)
|
||||
.to_display_point(snapshot)
|
||||
.row()
|
||||
..Point::new(hunk.associated_range.end.0, 0)
|
||||
..Point::new(hunk.row_range.end.0, 0)
|
||||
.to_display_point(snapshot)
|
||||
.row();
|
||||
let (_, buffer, _) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.excerpt_containing(Point::new(hunk.associated_range.start.0, 0), cx)
|
||||
.excerpt_containing(Point::new(hunk.row_range.start.0, 0), cx)
|
||||
.expect("no excerpt for expanded buffer's hunk start");
|
||||
let diff_base = buffer
|
||||
.read(cx)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use rope::Rope;
|
||||
use std::{iter, ops::Range};
|
||||
use sum_tree::SumTree;
|
||||
use text::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, Point};
|
||||
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
|
||||
|
||||
pub use git2 as libgit;
|
||||
use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
|
||||
|
@ -13,29 +13,30 @@ pub enum DiffHunkStatus {
|
|||
Removed,
|
||||
}
|
||||
|
||||
/// A diff hunk, representing a range of consequent lines in a singleton buffer, associated with a generic range.
|
||||
/// A diff hunk resolved to rows in the buffer.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DiffHunk<T> {
|
||||
/// E.g. a range in multibuffer, that has an excerpt added, singleton buffer for which has this diff hunk.
|
||||
/// Consider a singleton buffer with 10 lines, all of them are modified — so a corresponding diff hunk would have a range 0..10.
|
||||
/// And a multibuffer with the excerpt of lines 2-6 from the singleton buffer.
|
||||
/// If the multibuffer is searched for diff hunks, the associated range would be multibuffer rows, corresponding to rows 2..6 from the singleton buffer.
|
||||
/// But the hunk range would be 0..10, same for any other excerpts from the same singleton buffer.
|
||||
pub associated_range: Range<T>,
|
||||
/// Singleton buffer ID this hunk belongs to.
|
||||
pub buffer_id: BufferId,
|
||||
/// A consequent range of lines in the singleton buffer, that were changed and produced this diff hunk.
|
||||
pub struct DiffHunk {
|
||||
/// The buffer range, expressed in terms of rows.
|
||||
pub row_range: Range<u32>,
|
||||
/// The range in the buffer to which this hunk corresponds.
|
||||
pub buffer_range: Range<Anchor>,
|
||||
/// Original singleton buffer text before the change, that was instead of the `buffer_range`.
|
||||
/// The range in the buffer's diff base text to which this hunk corresponds.
|
||||
pub diff_base_byte_range: Range<usize>,
|
||||
}
|
||||
|
||||
impl sum_tree::Item for DiffHunk<Anchor> {
|
||||
/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
|
||||
#[derive(Debug, Clone)]
|
||||
struct InternalDiffHunk {
|
||||
buffer_range: Range<Anchor>,
|
||||
diff_base_byte_range: Range<usize>,
|
||||
}
|
||||
|
||||
impl sum_tree::Item for InternalDiffHunk {
|
||||
type Summary = DiffHunkSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
DiffHunkSummary {
|
||||
buffer_range: self.associated_range.clone(),
|
||||
buffer_range: self.buffer_range.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +65,7 @@ impl sum_tree::Summary for DiffHunkSummary {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct BufferDiff {
|
||||
last_buffer_version: Option<clock::Global>,
|
||||
tree: SumTree<DiffHunk<Anchor>>,
|
||||
tree: SumTree<InternalDiffHunk>,
|
||||
}
|
||||
|
||||
impl BufferDiff {
|
||||
|
@ -79,11 +80,12 @@ impl BufferDiff {
|
|||
self.tree.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn hunks_in_row_range<'a>(
|
||||
&'a self,
|
||||
range: Range<u32>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let start = buffer.anchor_before(Point::new(range.start, 0));
|
||||
let end = buffer.anchor_after(Point::new(range.end, 0));
|
||||
|
||||
|
@ -94,7 +96,7 @@ impl BufferDiff {
|
|||
&'a self,
|
||||
range: Range<Anchor>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let mut cursor = self
|
||||
.tree
|
||||
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
||||
|
@ -109,11 +111,8 @@ impl BufferDiff {
|
|||
})
|
||||
.flat_map(move |hunk| {
|
||||
[
|
||||
(
|
||||
&hunk.associated_range.start,
|
||||
hunk.diff_base_byte_range.start,
|
||||
),
|
||||
(&hunk.associated_range.end, hunk.diff_base_byte_range.end),
|
||||
(&hunk.buffer_range.start, hunk.diff_base_byte_range.start),
|
||||
(&hunk.buffer_range.end, hunk.diff_base_byte_range.end),
|
||||
]
|
||||
.into_iter()
|
||||
});
|
||||
|
@ -129,10 +128,9 @@ impl BufferDiff {
|
|||
}
|
||||
|
||||
Some(DiffHunk {
|
||||
associated_range: start_point.row..end_point.row,
|
||||
row_range: start_point.row..end_point.row,
|
||||
diff_base_byte_range: start_base..end_base,
|
||||
buffer_range: buffer.anchor_before(start_point)..buffer.anchor_after(end_point),
|
||||
buffer_id: buffer.remote_id(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -141,7 +139,7 @@ impl BufferDiff {
|
|||
&'a self,
|
||||
range: Range<Anchor>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let mut cursor = self
|
||||
.tree
|
||||
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
||||
|
@ -154,7 +152,7 @@ impl BufferDiff {
|
|||
cursor.prev(buffer);
|
||||
|
||||
let hunk = cursor.item()?;
|
||||
let range = hunk.associated_range.to_point(buffer);
|
||||
let range = hunk.buffer_range.to_point(buffer);
|
||||
let end_row = if range.end.column > 0 {
|
||||
range.end.row + 1
|
||||
} else {
|
||||
|
@ -162,10 +160,9 @@ impl BufferDiff {
|
|||
};
|
||||
|
||||
Some(DiffHunk {
|
||||
associated_range: range.start.row..end_row,
|
||||
row_range: range.start.row..end_row,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
buffer_id: hunk.buffer_id,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -196,7 +193,7 @@ impl BufferDiff {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let start = text.anchor_before(Point::new(0, 0));
|
||||
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
|
||||
self.hunks_intersecting_range(start..end, text)
|
||||
|
@ -229,7 +226,7 @@ impl BufferDiff {
|
|||
hunk_index: usize,
|
||||
buffer: &text::BufferSnapshot,
|
||||
buffer_row_divergence: &mut i64,
|
||||
) -> DiffHunk<Anchor> {
|
||||
) -> InternalDiffHunk {
|
||||
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
|
||||
assert!(line_item_count > 0);
|
||||
|
||||
|
@ -284,11 +281,9 @@ impl BufferDiff {
|
|||
let start = Point::new(buffer_row_range.start, 0);
|
||||
let end = Point::new(buffer_row_range.end, 0);
|
||||
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
|
||||
DiffHunk {
|
||||
associated_range: buffer_range.clone(),
|
||||
InternalDiffHunk {
|
||||
buffer_range,
|
||||
diff_base_byte_range,
|
||||
buffer_id: buffer.remote_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -302,17 +297,16 @@ pub fn assert_hunks<Iter>(
|
|||
diff_base: &str,
|
||||
expected_hunks: &[(Range<u32>, &str, &str)],
|
||||
) where
|
||||
Iter: Iterator<Item = DiffHunk<u32>>,
|
||||
Iter: Iterator<Item = DiffHunk>,
|
||||
{
|
||||
let actual_hunks = diff_hunks
|
||||
.map(|hunk| {
|
||||
(
|
||||
hunk.associated_range.clone(),
|
||||
hunk.row_range.clone(),
|
||||
&diff_base[hunk.diff_base_byte_range],
|
||||
buffer
|
||||
.text_for_range(
|
||||
Point::new(hunk.associated_range.start, 0)
|
||||
..Point::new(hunk.associated_range.end, 0),
|
||||
Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
|
||||
)
|
||||
.collect::<String>(),
|
||||
)
|
||||
|
|
|
@ -21,8 +21,8 @@ use async_watch as watch;
|
|||
pub use clock::ReplicaId;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
AnyElement, AppContext, EventEmitter, HighlightStyle, ModelContext, Pixels, Task, TaskLabel,
|
||||
WindowContext,
|
||||
AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext,
|
||||
Pixels, Task, TaskLabel, WindowContext,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use parking_lot::Mutex;
|
||||
|
@ -84,11 +84,17 @@ pub enum Capability {
|
|||
|
||||
pub type BufferRow = u32;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum BufferDiffBase {
|
||||
Git(Rope),
|
||||
PastBufferVersion(Model<Buffer>, BufferSnapshot),
|
||||
}
|
||||
|
||||
/// An in-memory representation of a source code file, including its text,
|
||||
/// syntax trees, git status, and diagnostics.
|
||||
pub struct Buffer {
|
||||
text: TextBuffer,
|
||||
diff_base: Option<Rope>,
|
||||
diff_base: Option<BufferDiffBase>,
|
||||
git_diff: git::diff::BufferDiff,
|
||||
file: Option<Arc<dyn File>>,
|
||||
/// The mtime of the file when this buffer was last loaded from
|
||||
|
@ -121,6 +127,7 @@ pub struct Buffer {
|
|||
/// Memoize calls to has_changes_since(saved_version).
|
||||
/// The contents of a cell are (self.version, has_changes) at the time of a last call.
|
||||
has_unsaved_edits: Cell<(clock::Global, bool)>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
|
@ -308,7 +315,10 @@ pub enum Operation {
|
|||
pub enum BufferEvent {
|
||||
/// The buffer was changed in a way that must be
|
||||
/// propagated to its other replicas.
|
||||
Operation(Operation),
|
||||
Operation {
|
||||
operation: Operation,
|
||||
is_local: bool,
|
||||
},
|
||||
/// The buffer was edited.
|
||||
Edited,
|
||||
/// The buffer's `dirty` bit changed.
|
||||
|
@ -644,7 +654,7 @@ impl Buffer {
|
|||
id: self.remote_id().into(),
|
||||
file: self.file.as_ref().map(|f| f.to_proto(cx)),
|
||||
base_text: self.base_text().to_string(),
|
||||
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
|
||||
diff_base: self.diff_base().as_ref().map(|h| h.to_string()),
|
||||
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
||||
saved_version: proto::serialize_version(&self.saved_version),
|
||||
saved_mtime: self.saved_mtime.map(|time| time.into()),
|
||||
|
@ -734,12 +744,10 @@ impl Buffer {
|
|||
was_dirty_before_starting_transaction: None,
|
||||
has_unsaved_edits: Cell::new((buffer.version(), false)),
|
||||
text: buffer,
|
||||
diff_base: diff_base
|
||||
.map(|mut raw_diff_base| {
|
||||
LineEnding::normalize(&mut raw_diff_base);
|
||||
raw_diff_base
|
||||
})
|
||||
.map(Rope::from),
|
||||
diff_base: diff_base.map(|mut raw_diff_base| {
|
||||
LineEnding::normalize(&mut raw_diff_base);
|
||||
BufferDiffBase::Git(Rope::from(raw_diff_base))
|
||||
}),
|
||||
diff_base_version: 0,
|
||||
git_diff,
|
||||
file,
|
||||
|
@ -759,6 +767,7 @@ impl Buffer {
|
|||
completion_triggers_timestamp: Default::default(),
|
||||
deferred_ops: OperationQueue::new(),
|
||||
has_conflict: false,
|
||||
_subscriptions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -782,6 +791,52 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn branch(&mut self, cx: &mut ModelContext<Self>) -> Model<Self> {
|
||||
let this = cx.handle();
|
||||
cx.new_model(|cx| {
|
||||
let mut branch = Self {
|
||||
diff_base: Some(BufferDiffBase::PastBufferVersion(
|
||||
this.clone(),
|
||||
self.snapshot(),
|
||||
)),
|
||||
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;
|
||||
}
|
||||
})],
|
||||
..Self::build(
|
||||
self.text.branch(),
|
||||
None,
|
||||
self.file.clone(),
|
||||
self.capability(),
|
||||
)
|
||||
};
|
||||
if let Some(language_registry) = self.language_registry() {
|
||||
branch.set_language_registry(language_registry);
|
||||
}
|
||||
|
||||
branch
|
||||
})
|
||||
}
|
||||
|
||||
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>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.edit(edits, None, cx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn as_text_snapshot(&self) -> &text::BufferSnapshot {
|
||||
&self.text
|
||||
|
@ -961,20 +1016,23 @@ impl Buffer {
|
|||
|
||||
/// Returns the current diff base, see [Buffer::set_diff_base].
|
||||
pub fn diff_base(&self) -> Option<&Rope> {
|
||||
self.diff_base.as_ref()
|
||||
match self.diff_base.as_ref()? {
|
||||
BufferDiffBase::Git(rope) => Some(rope),
|
||||
BufferDiffBase::PastBufferVersion(_, buffer_snapshot) => {
|
||||
Some(buffer_snapshot.as_rope())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the text that will be used to compute a Git diff
|
||||
/// against the buffer text.
|
||||
pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
|
||||
self.diff_base = diff_base
|
||||
.map(|mut raw_diff_base| {
|
||||
LineEnding::normalize(&mut raw_diff_base);
|
||||
raw_diff_base
|
||||
})
|
||||
.map(Rope::from);
|
||||
self.diff_base = diff_base.map(|mut raw_diff_base| {
|
||||
LineEnding::normalize(&mut raw_diff_base);
|
||||
BufferDiffBase::Git(Rope::from(raw_diff_base))
|
||||
});
|
||||
self.diff_base_version += 1;
|
||||
if let Some(recalc_task) = self.git_diff_recalc(cx) {
|
||||
if let Some(recalc_task) = self.recalculate_diff(cx) {
|
||||
cx.spawn(|buffer, mut cx| async move {
|
||||
recalc_task.await;
|
||||
buffer
|
||||
|
@ -992,14 +1050,21 @@ impl Buffer {
|
|||
self.diff_base_version
|
||||
}
|
||||
|
||||
/// Recomputes the Git diff status.
|
||||
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<()>> {
|
||||
let diff_base = self.diff_base.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()? {
|
||||
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()
|
||||
}
|
||||
};
|
||||
let snapshot = self.snapshot();
|
||||
|
||||
let mut diff = self.git_diff.clone();
|
||||
let diff = cx.background_executor().spawn(async move {
|
||||
diff.update(&diff_base, &snapshot).await;
|
||||
diff.update(&diff_base_rope, &snapshot).await;
|
||||
diff
|
||||
});
|
||||
|
||||
|
@ -1169,7 +1234,7 @@ impl Buffer {
|
|||
lamport_timestamp,
|
||||
};
|
||||
self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx);
|
||||
self.send_operation(op, cx);
|
||||
self.send_operation(op, true, cx);
|
||||
}
|
||||
|
||||
fn request_autoindent(&mut self, cx: &mut ModelContext<Self>) {
|
||||
|
@ -1743,6 +1808,7 @@ impl Buffer {
|
|||
lamport_timestamp,
|
||||
cursor_shape,
|
||||
},
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
self.non_text_state_update_count += 1;
|
||||
|
@ -1889,7 +1955,7 @@ impl Buffer {
|
|||
}
|
||||
|
||||
self.end_transaction(cx);
|
||||
self.send_operation(Operation::Buffer(edit_operation), cx);
|
||||
self.send_operation(Operation::Buffer(edit_operation), true, cx);
|
||||
Some(edit_id)
|
||||
}
|
||||
|
||||
|
@ -1991,6 +2057,9 @@ impl Buffer {
|
|||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for operation in buffer_ops.iter() {
|
||||
self.send_operation(Operation::Buffer(operation.clone()), false, cx);
|
||||
}
|
||||
self.text.apply_ops(buffer_ops);
|
||||
self.deferred_ops.insert(deferred_ops);
|
||||
self.flush_deferred_ops(cx);
|
||||
|
@ -2114,8 +2183,16 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
fn send_operation(&mut self, operation: Operation, cx: &mut ModelContext<Self>) {
|
||||
cx.emit(BufferEvent::Operation(operation));
|
||||
fn send_operation(
|
||||
&mut self,
|
||||
operation: Operation,
|
||||
is_local: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
cx.emit(BufferEvent::Operation {
|
||||
operation,
|
||||
is_local,
|
||||
});
|
||||
}
|
||||
|
||||
/// Removes the selections for a given peer.
|
||||
|
@ -2130,7 +2207,7 @@ impl Buffer {
|
|||
let old_version = self.version.clone();
|
||||
|
||||
if let Some((transaction_id, operation)) = self.text.undo() {
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||
self.did_edit(&old_version, was_dirty, cx);
|
||||
Some(transaction_id)
|
||||
} else {
|
||||
|
@ -2147,7 +2224,7 @@ impl Buffer {
|
|||
let was_dirty = self.is_dirty();
|
||||
let old_version = self.version.clone();
|
||||
if let Some(operation) = self.text.undo_transaction(transaction_id) {
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||
self.did_edit(&old_version, was_dirty, cx);
|
||||
true
|
||||
} else {
|
||||
|
@ -2167,7 +2244,7 @@ impl Buffer {
|
|||
let operations = self.text.undo_to_transaction(transaction_id);
|
||||
let undone = !operations.is_empty();
|
||||
for operation in operations {
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||
}
|
||||
if undone {
|
||||
self.did_edit(&old_version, was_dirty, cx)
|
||||
|
@ -2181,7 +2258,7 @@ impl Buffer {
|
|||
let old_version = self.version.clone();
|
||||
|
||||
if let Some((transaction_id, operation)) = self.text.redo() {
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||
self.did_edit(&old_version, was_dirty, cx);
|
||||
Some(transaction_id)
|
||||
} else {
|
||||
|
@ -2201,7 +2278,7 @@ impl Buffer {
|
|||
let operations = self.text.redo_to_transaction(transaction_id);
|
||||
let redone = !operations.is_empty();
|
||||
for operation in operations {
|
||||
self.send_operation(Operation::Buffer(operation), cx);
|
||||
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||
}
|
||||
if redone {
|
||||
self.did_edit(&old_version, was_dirty, cx)
|
||||
|
@ -2218,6 +2295,7 @@ impl Buffer {
|
|||
triggers,
|
||||
lamport_timestamp: self.completion_triggers_timestamp,
|
||||
},
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
|
@ -2297,7 +2375,7 @@ impl Buffer {
|
|||
let ops = self.text.randomly_undo_redo(rng);
|
||||
if !ops.is_empty() {
|
||||
for op in ops {
|
||||
self.send_operation(Operation::Buffer(op), cx);
|
||||
self.send_operation(Operation::Buffer(op), true, cx);
|
||||
self.did_edit(&old_version, was_dirty, cx);
|
||||
}
|
||||
}
|
||||
|
@ -3638,12 +3716,12 @@ impl BufferSnapshot {
|
|||
!self.git_diff.is_empty()
|
||||
}
|
||||
|
||||
/// Returns all the Git diff hunks intersecting the given
|
||||
/// row range.
|
||||
/// Returns all the Git diff hunks intersecting the given row range.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn git_diff_hunks_in_row_range(
|
||||
&self,
|
||||
range: Range<BufferRow>,
|
||||
) -> impl '_ + Iterator<Item = git::diff::DiffHunk<u32>> {
|
||||
) -> impl '_ + Iterator<Item = git::diff::DiffHunk> {
|
||||
self.git_diff.hunks_in_row_range(range, self)
|
||||
}
|
||||
|
||||
|
@ -3652,7 +3730,7 @@ impl BufferSnapshot {
|
|||
pub fn git_diff_hunks_intersecting_range(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
) -> impl '_ + Iterator<Item = git::diff::DiffHunk<u32>> {
|
||||
) -> impl '_ + Iterator<Item = git::diff::DiffHunk> {
|
||||
self.git_diff.hunks_intersecting_range(range, self)
|
||||
}
|
||||
|
||||
|
@ -3661,7 +3739,7 @@ impl BufferSnapshot {
|
|||
pub fn git_diff_hunks_intersecting_range_rev(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
) -> impl '_ + Iterator<Item = git::diff::DiffHunk<u32>> {
|
||||
) -> impl '_ + Iterator<Item = git::diff::DiffHunk> {
|
||||
self.git_diff.hunks_intersecting_range_rev(range, self)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::Buffer;
|
|||
use clock::ReplicaId;
|
||||
use collections::BTreeMap;
|
||||
use futures::FutureExt as _;
|
||||
use git::diff::assert_hunks;
|
||||
use gpui::{AppContext, BorrowAppContext, Model};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
|
@ -275,13 +276,19 @@ fn test_edit_events(cx: &mut gpui::AppContext) {
|
|||
|buffer, cx| {
|
||||
let buffer_1_events = buffer_1_events.clone();
|
||||
cx.subscribe(&buffer1, move |_, _, event, _| match event.clone() {
|
||||
BufferEvent::Operation(op) => buffer1_ops.lock().push(op),
|
||||
BufferEvent::Operation {
|
||||
operation,
|
||||
is_local: true,
|
||||
} => buffer1_ops.lock().push(operation),
|
||||
event => buffer_1_events.lock().push(event),
|
||||
})
|
||||
.detach();
|
||||
let buffer_2_events = buffer_2_events.clone();
|
||||
cx.subscribe(&buffer2, move |_, _, event, _| {
|
||||
buffer_2_events.lock().push(event.clone())
|
||||
cx.subscribe(&buffer2, move |_, _, event, _| match event.clone() {
|
||||
BufferEvent::Operation {
|
||||
is_local: false, ..
|
||||
} => {}
|
||||
event => buffer_2_events.lock().push(event),
|
||||
})
|
||||
.detach();
|
||||
|
||||
|
@ -2370,6 +2377,118 @@ async fn test_find_matching_indent(cx: &mut TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_branch_and_merge(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| init_settings(cx, |_| {}));
|
||||
|
||||
let base_buffer = cx.new_model(|cx| Buffer::local("one\ntwo\nthree\n", cx));
|
||||
|
||||
// Create a remote replica of the base buffer.
|
||||
let base_buffer_replica = cx.new_model(|cx| {
|
||||
Buffer::from_proto(
|
||||
1,
|
||||
Capability::ReadWrite,
|
||||
base_buffer.read(cx).to_proto(cx),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
base_buffer.update(cx, |_buffer, cx| {
|
||||
cx.subscribe(&base_buffer_replica, |this, _, event, cx| {
|
||||
if let BufferEvent::Operation {
|
||||
operation,
|
||||
is_local: true,
|
||||
} = event
|
||||
{
|
||||
this.apply_ops([operation.clone()], cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
// Create a branch, which initially has the same state as the base buffer.
|
||||
let branch_buffer = base_buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
branch_buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "one\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Edits to the branch are not applied to the base.
|
||||
branch_buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(Point::new(1, 0)..Point::new(1, 0), "ONE_POINT_FIVE\n")],
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
branch_buffer.read_with(cx, |branch_buffer, cx| {
|
||||
assert_eq!(base_buffer.read(cx).text(), "one\ntwo\nthree\n");
|
||||
assert_eq!(branch_buffer.text(), "one\nONE_POINT_FIVE\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Edits to the base are applied to the branch.
|
||||
base_buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "ZERO\n")], None, cx)
|
||||
});
|
||||
branch_buffer.read_with(cx, |branch_buffer, cx| {
|
||||
assert_eq!(base_buffer.read(cx).text(), "ZERO\none\ntwo\nthree\n");
|
||||
assert_eq!(
|
||||
branch_buffer.text(),
|
||||
"ZERO\none\nONE_POINT_FIVE\ntwo\nthree\n"
|
||||
);
|
||||
});
|
||||
|
||||
assert_diff_hunks(&branch_buffer, cx, &[(2..3, "", "ONE_POINT_FIVE\n")]);
|
||||
|
||||
// Edits to any replica of the base are applied to the branch.
|
||||
base_buffer_replica.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(Point::new(2, 0)..Point::new(2, 0), "TWO_POINT_FIVE\n")],
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
branch_buffer.read_with(cx, |branch_buffer, cx| {
|
||||
assert_eq!(
|
||||
base_buffer.read(cx).text(),
|
||||
"ZERO\none\ntwo\nTWO_POINT_FIVE\nthree\n"
|
||||
);
|
||||
assert_eq!(
|
||||
branch_buffer.text(),
|
||||
"ZERO\none\nONE_POINT_FIVE\ntwo\nTWO_POINT_FIVE\nthree\n"
|
||||
);
|
||||
});
|
||||
|
||||
// Merging the branch applies all of its changes to the base.
|
||||
base_buffer.update(cx, |base_buffer, cx| {
|
||||
base_buffer.merge(&branch_buffer, cx);
|
||||
assert_eq!(
|
||||
base_buffer.text(),
|
||||
"ZERO\none\nONE_POINT_FIVE\ntwo\nTWO_POINT_FIVE\nthree\n"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn assert_diff_hunks(
|
||||
buffer: &Model<Buffer>,
|
||||
cx: &mut TestAppContext,
|
||||
expected_hunks: &[(Range<u32>, &str, &str)],
|
||||
) {
|
||||
buffer
|
||||
.update(cx, |buffer, cx| buffer.recalculate_diff(cx).unwrap())
|
||||
.detach();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
assert_hunks(
|
||||
snapshot.git_diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX),
|
||||
&snapshot,
|
||||
&buffer.diff_base().unwrap().to_string(),
|
||||
expected_hunks,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
||||
let min_peers = env::var("MIN_PEERS")
|
||||
|
@ -2407,10 +2526,15 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
|||
buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
|
||||
let network = network.clone();
|
||||
cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
|
||||
if let BufferEvent::Operation(op) = event {
|
||||
network
|
||||
.lock()
|
||||
.broadcast(buffer.replica_id(), vec![proto::serialize_operation(op)]);
|
||||
if let BufferEvent::Operation {
|
||||
operation,
|
||||
is_local: true,
|
||||
} = event
|
||||
{
|
||||
network.lock().broadcast(
|
||||
buffer.replica_id(),
|
||||
vec![proto::serialize_operation(operation)],
|
||||
);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
@ -2533,10 +2657,14 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
|||
new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
|
||||
let network = network.clone();
|
||||
cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
|
||||
if let BufferEvent::Operation(op) = event {
|
||||
if let BufferEvent::Operation {
|
||||
operation,
|
||||
is_local: true,
|
||||
} = event
|
||||
{
|
||||
network.lock().broadcast(
|
||||
buffer.replica_id(),
|
||||
vec![proto::serialize_operation(op)],
|
||||
vec![proto::serialize_operation(operation)],
|
||||
);
|
||||
}
|
||||
})
|
||||
|
|
|
@ -27,7 +27,6 @@ collections.workspace = true
|
|||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
|
|
|
@ -5,7 +5,6 @@ use anyhow::{anyhow, Result};
|
|||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||
use futures::{channel::mpsc, SinkExt};
|
||||
use git::diff::DiffHunk;
|
||||
use gpui::{AppContext, EntityId, EventEmitter, Model, ModelContext};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
|
@ -110,6 +109,19 @@ pub enum Event {
|
|||
DiagnosticsUpdated,
|
||||
}
|
||||
|
||||
/// A diff hunk, representing a range of consequent lines in a multibuffer.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MultiBufferDiffHunk {
|
||||
/// The row range in the multibuffer where this diff hunk appears.
|
||||
pub row_range: Range<MultiBufferRow>,
|
||||
/// The buffer ID that this hunk belongs to.
|
||||
pub buffer_id: BufferId,
|
||||
/// The range of the underlying buffer that this hunk corresponds to.
|
||||
pub buffer_range: Range<text::Anchor>,
|
||||
/// The range within the buffer's diff base that this hunk corresponds to.
|
||||
pub diff_base_byte_range: Range<usize>,
|
||||
}
|
||||
|
||||
pub type MultiBufferPoint = Point;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, serde::Deserialize)]
|
||||
|
@ -1711,7 +1723,7 @@ impl MultiBuffer {
|
|||
}
|
||||
|
||||
//
|
||||
language::BufferEvent::Operation(_) => return,
|
||||
language::BufferEvent::Operation { .. } => return,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -3561,7 +3573,7 @@ impl MultiBufferSnapshot {
|
|||
pub fn git_diff_hunks_in_range_rev(
|
||||
&self,
|
||||
row_range: Range<MultiBufferRow>,
|
||||
) -> impl Iterator<Item = DiffHunk<MultiBufferRow>> + '_ {
|
||||
) -> impl Iterator<Item = MultiBufferDiffHunk> + '_ {
|
||||
let mut cursor = self.excerpts.cursor::<Point>(&());
|
||||
|
||||
cursor.seek(&Point::new(row_range.end.0, 0), Bias::Left, &());
|
||||
|
@ -3599,22 +3611,19 @@ impl MultiBufferSnapshot {
|
|||
.git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end)
|
||||
.map(move |hunk| {
|
||||
let start = multibuffer_start.row
|
||||
+ hunk
|
||||
.associated_range
|
||||
.start
|
||||
.saturating_sub(excerpt_start_point.row);
|
||||
+ hunk.row_range.start.saturating_sub(excerpt_start_point.row);
|
||||
let end = multibuffer_start.row
|
||||
+ hunk
|
||||
.associated_range
|
||||
.row_range
|
||||
.end
|
||||
.min(excerpt_end_point.row + 1)
|
||||
.saturating_sub(excerpt_start_point.row);
|
||||
|
||||
DiffHunk {
|
||||
associated_range: MultiBufferRow(start)..MultiBufferRow(end),
|
||||
MultiBufferDiffHunk {
|
||||
row_range: MultiBufferRow(start)..MultiBufferRow(end),
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
buffer_id: hunk.buffer_id,
|
||||
buffer_id: excerpt.buffer_id,
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -3628,7 +3637,7 @@ impl MultiBufferSnapshot {
|
|||
pub fn git_diff_hunks_in_range(
|
||||
&self,
|
||||
row_range: Range<MultiBufferRow>,
|
||||
) -> impl Iterator<Item = DiffHunk<MultiBufferRow>> + '_ {
|
||||
) -> impl Iterator<Item = MultiBufferDiffHunk> + '_ {
|
||||
let mut cursor = self.excerpts.cursor::<Point>(&());
|
||||
|
||||
cursor.seek(&Point::new(row_range.start.0, 0), Bias::Left, &());
|
||||
|
@ -3673,23 +3682,20 @@ impl MultiBufferSnapshot {
|
|||
MultiBufferRow(0)..MultiBufferRow(1)
|
||||
} else {
|
||||
let start = multibuffer_start.row
|
||||
+ hunk
|
||||
.associated_range
|
||||
.start
|
||||
.saturating_sub(excerpt_rows.start);
|
||||
+ hunk.row_range.start.saturating_sub(excerpt_rows.start);
|
||||
let end = multibuffer_start.row
|
||||
+ hunk
|
||||
.associated_range
|
||||
.row_range
|
||||
.end
|
||||
.min(excerpt_rows.end + 1)
|
||||
.saturating_sub(excerpt_rows.start);
|
||||
MultiBufferRow(start)..MultiBufferRow(end)
|
||||
};
|
||||
DiffHunk {
|
||||
associated_range: buffer_range,
|
||||
MultiBufferDiffHunk {
|
||||
row_range: buffer_range,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
buffer_id: hunk.buffer_id,
|
||||
buffer_id: excerpt.buffer_id,
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -2182,7 +2182,10 @@ impl Project {
|
|||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
match event {
|
||||
BufferEvent::Operation(operation) => {
|
||||
BufferEvent::Operation {
|
||||
operation,
|
||||
is_local: true,
|
||||
} => {
|
||||
let operation = language::proto::serialize_operation(operation);
|
||||
|
||||
if let Some(ssh) = &self.ssh_session {
|
||||
|
@ -2267,7 +2270,7 @@ impl Project {
|
|||
.filter_map(|buffer| {
|
||||
let buffer = buffer.upgrade()?;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx))
|
||||
.update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
|
|
|
@ -3288,7 +3288,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
|
|||
cx.subscribe(&buffer1, {
|
||||
let events = events.clone();
|
||||
move |_, _, event, _| match event {
|
||||
BufferEvent::Operation(_) => {}
|
||||
BufferEvent::Operation { .. } => {}
|
||||
_ => events.lock().push(event.clone()),
|
||||
}
|
||||
})
|
||||
|
|
|
@ -146,12 +146,15 @@ impl HeadlessProject {
|
|||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
BufferEvent::Operation(op) => cx
|
||||
BufferEvent::Operation {
|
||||
operation,
|
||||
is_local: true,
|
||||
} => cx
|
||||
.background_executor()
|
||||
.spawn(self.session.request(proto::UpdateBuffer {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
buffer_id: buffer.read(cx).remote_id().to_proto(),
|
||||
operations: vec![serialize_operation(op)],
|
||||
operations: vec![serialize_operation(operation)],
|
||||
}))
|
||||
.detach(),
|
||||
_ => {}
|
||||
|
|
|
@ -13,6 +13,7 @@ mod undo_map;
|
|||
pub use anchor::*;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
pub use clock::ReplicaId;
|
||||
use clock::LOCAL_BRANCH_REPLICA_ID;
|
||||
use collections::{HashMap, HashSet};
|
||||
use locator::Locator;
|
||||
use operation_queue::OperationQueue;
|
||||
|
@ -715,6 +716,19 @@ impl Buffer {
|
|||
self.snapshot.clone()
|
||||
}
|
||||
|
||||
pub fn branch(&self) -> Self {
|
||||
Self {
|
||||
snapshot: self.snapshot.clone(),
|
||||
history: History::new(self.base_text().clone()),
|
||||
deferred_ops: OperationQueue::new(),
|
||||
deferred_replicas: HashSet::default(),
|
||||
lamport_clock: clock::Lamport::new(LOCAL_BRANCH_REPLICA_ID),
|
||||
subscriptions: Default::default(),
|
||||
edit_id_resolvers: Default::default(),
|
||||
wait_for_version_txs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replica_id(&self) -> ReplicaId {
|
||||
self.lamport_clock.replica_id
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue