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",
|
"ctor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures 0.3.30",
|
"futures 0.3.30",
|
||||||
"git",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"language",
|
"language",
|
||||||
|
|
|
@ -1006,9 +1006,12 @@ impl Context {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
language::BufferEvent::Operation(operation) => cx.emit(ContextEvent::Operation(
|
language::BufferEvent::Operation {
|
||||||
ContextOperation::BufferOperation(operation.clone()),
|
operation,
|
||||||
)),
|
is_local: true,
|
||||||
|
} => cx.emit(ContextEvent::Operation(ContextOperation::BufferOperation(
|
||||||
|
operation.clone(),
|
||||||
|
))),
|
||||||
language::BufferEvent::Edited => {
|
language::BufferEvent::Edited => {
|
||||||
self.count_remaining_tokens(cx);
|
self.count_remaining_tokens(cx);
|
||||||
self.reparse(cx);
|
self.reparse(cx);
|
||||||
|
|
|
@ -175,7 +175,10 @@ impl ChannelBuffer {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
language::BufferEvent::Operation(operation) => {
|
language::BufferEvent::Operation {
|
||||||
|
operation,
|
||||||
|
is_local: true,
|
||||||
|
} => {
|
||||||
if *ZED_ALWAYS_ACTIVE {
|
if *ZED_ALWAYS_ACTIVE {
|
||||||
if let language::Operation::UpdateSelections { selections, .. } = operation {
|
if let language::Operation::UpdateSelections { selections, .. } = operation {
|
||||||
if selections.is_empty() {
|
if selections.is_empty() {
|
||||||
|
|
|
@ -9,6 +9,8 @@ use std::{
|
||||||
|
|
||||||
pub use system_clock::*;
|
pub use system_clock::*;
|
||||||
|
|
||||||
|
pub const LOCAL_BRANCH_REPLICA_ID: u16 = u16::MAX;
|
||||||
|
|
||||||
/// A unique identifier for each distributed node.
|
/// A unique identifier for each distributed node.
|
||||||
pub type ReplicaId = u16;
|
pub type ReplicaId = u16;
|
||||||
|
|
||||||
|
@ -25,7 +27,10 @@ pub struct Lamport {
|
||||||
|
|
||||||
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock).
|
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock).
|
||||||
#[derive(Clone, Default, Hash, Eq, PartialEq)]
|
#[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 {
|
impl Global {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
@ -33,41 +38,51 @@ impl Global {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, replica_id: ReplicaId) -> Seq {
|
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) {
|
pub fn observe(&mut self, timestamp: Lamport) {
|
||||||
if timestamp.value > 0 {
|
if timestamp.value > 0 {
|
||||||
let new_len = timestamp.replica_id as usize + 1;
|
if timestamp.replica_id == LOCAL_BRANCH_REPLICA_ID {
|
||||||
if new_len > self.0.len() {
|
self.local_branch_value = cmp::max(self.local_branch_value, timestamp.value);
|
||||||
self.0.resize(new_len, 0);
|
} 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];
|
let entry = &mut self.values[timestamp.replica_id as usize];
|
||||||
*entry = cmp::max(*entry, timestamp.value);
|
*entry = cmp::max(*entry, timestamp.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn join(&mut self, other: &Self) {
|
pub fn join(&mut self, other: &Self) {
|
||||||
if other.0.len() > self.0.len() {
|
if other.values.len() > self.values.len() {
|
||||||
self.0.resize(other.0.len(), 0);
|
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);
|
*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) {
|
pub fn meet(&mut self, other: &Self) {
|
||||||
if other.0.len() > self.0.len() {
|
if other.values.len() > self.values.len() {
|
||||||
self.0.resize(other.0.len(), 0);
|
self.values.resize(other.values.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut new_len = 0;
|
let mut new_len = 0;
|
||||||
for (ix, (left, right)) in self
|
for (ix, (left, right)) in self
|
||||||
.0
|
.values
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.zip(other.0.iter().chain(iter::repeat(&0)))
|
.zip(other.values.iter().chain(iter::repeat(&0)))
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
if *left == 0 {
|
if *left == 0 {
|
||||||
|
@ -80,7 +95,8 @@ impl Global {
|
||||||
new_len = ix + 1;
|
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 {
|
pub fn observed(&self, timestamp: Lamport) -> bool {
|
||||||
|
@ -88,34 +104,44 @@ impl Global {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observed_any(&self, other: &Self) -> bool {
|
pub fn observed_any(&self, other: &Self) -> bool {
|
||||||
self.0
|
self.values
|
||||||
.iter()
|
.iter()
|
||||||
.zip(other.0.iter())
|
.zip(other.values.iter())
|
||||||
.any(|(left, right)| *right > 0 && left >= right)
|
.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 {
|
pub fn observed_all(&self, other: &Self) -> bool {
|
||||||
let mut rhs = other.0.iter();
|
let mut rhs = other.values.iter();
|
||||||
self.0.iter().all(|left| match rhs.next() {
|
self.values.iter().all(|left| match rhs.next() {
|
||||||
Some(right) => left >= right,
|
Some(right) => left >= right,
|
||||||
None => true,
|
None => true,
|
||||||
}) && rhs.next().is_none()
|
}) && rhs.next().is_none()
|
||||||
|
&& self.local_branch_value >= other.local_branch_value
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn changed_since(&self, other: &Self) -> bool {
|
pub fn changed_since(&self, other: &Self) -> bool {
|
||||||
self.0.len() > other.0.len()
|
self.values.len() > other.values.len()
|
||||||
|| self
|
|| self
|
||||||
.0
|
.values
|
||||||
.iter()
|
.iter()
|
||||||
.zip(other.0.iter())
|
.zip(other.values.iter())
|
||||||
.any(|(left, right)| left > right)
|
.any(|(left, right)| left > right)
|
||||||
|
|| self.local_branch_value > other.local_branch_value
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = Lamport> + '_ {
|
pub fn iter(&self) -> impl Iterator<Item = Lamport> + '_ {
|
||||||
self.0.iter().enumerate().map(|(replica_id, seq)| Lamport {
|
self.values
|
||||||
replica_id: replica_id as ReplicaId,
|
.iter()
|
||||||
value: *seq,
|
.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)?;
|
write!(f, "{}: {}", timestamp.replica_id, timestamp.value)?;
|
||||||
}
|
}
|
||||||
|
if self.local_branch_value > 0 {
|
||||||
|
write!(f, "<branch>: {}", self.local_branch_value)?;
|
||||||
|
}
|
||||||
write!(f, "}}")
|
write!(f, "}}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,6 +273,7 @@ gpui::actions!(
|
||||||
NextScreen,
|
NextScreen,
|
||||||
OpenExcerpts,
|
OpenExcerpts,
|
||||||
OpenExcerptsSplit,
|
OpenExcerptsSplit,
|
||||||
|
OpenProposedChangesEditor,
|
||||||
OpenFile,
|
OpenFile,
|
||||||
OpenPermalinkToLine,
|
OpenPermalinkToLine,
|
||||||
OpenUrl,
|
OpenUrl,
|
||||||
|
|
|
@ -35,6 +35,7 @@ mod lsp_ext;
|
||||||
mod mouse_context_menu;
|
mod mouse_context_menu;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
mod persistence;
|
mod persistence;
|
||||||
|
mod proposed_changes_editor;
|
||||||
mod rust_analyzer_ext;
|
mod rust_analyzer_ext;
|
||||||
pub mod scroll;
|
pub mod scroll;
|
||||||
mod selections_collection;
|
mod selections_collection;
|
||||||
|
@ -46,7 +47,7 @@ mod signature_help;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
use ::git::diff::{DiffHunk, DiffHunkStatus};
|
use ::git::diff::DiffHunkStatus;
|
||||||
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
||||||
pub(crate) use actions::*;
|
pub(crate) use actions::*;
|
||||||
use aho_corasick::AhoCorasick;
|
use aho_corasick::AhoCorasick;
|
||||||
|
@ -98,6 +99,7 @@ use language::{
|
||||||
};
|
};
|
||||||
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
|
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
|
||||||
use linked_editing_ranges::refresh_linked_ranges;
|
use linked_editing_ranges::refresh_linked_ranges;
|
||||||
|
use proposed_changes_editor::{ProposedChangesBuffer, ProposedChangesEditor};
|
||||||
use similar::{ChangeTag, TextDiff};
|
use similar::{ChangeTag, TextDiff};
|
||||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||||
|
|
||||||
|
@ -113,7 +115,9 @@ pub use multi_buffer::{
|
||||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
|
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
|
||||||
ToPoint,
|
ToPoint,
|
||||||
};
|
};
|
||||||
use multi_buffer::{ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16};
|
use multi_buffer::{
|
||||||
|
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
|
||||||
|
};
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
||||||
|
@ -6152,7 +6156,7 @@ impl Editor {
|
||||||
pub fn prepare_revert_change(
|
pub fn prepare_revert_change(
|
||||||
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
|
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
|
||||||
multi_buffer: &Model<MultiBuffer>,
|
multi_buffer: &Model<MultiBuffer>,
|
||||||
hunk: &DiffHunk<MultiBufferRow>,
|
hunk: &MultiBufferDiffHunk,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let buffer = multi_buffer.read(cx).buffer(hunk.buffer_id)?;
|
let buffer = multi_buffer.read(cx).buffer(hunk.buffer_id)?;
|
||||||
|
@ -9338,7 +9342,7 @@ impl Editor {
|
||||||
snapshot: &DisplaySnapshot,
|
snapshot: &DisplaySnapshot,
|
||||||
initial_point: Point,
|
initial_point: Point,
|
||||||
is_wrapped: bool,
|
is_wrapped: bool,
|
||||||
hunks: impl Iterator<Item = DiffHunk<MultiBufferRow>>,
|
hunks: impl Iterator<Item = MultiBufferDiffHunk>,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let display_point = initial_point.to_display_point(snapshot);
|
let display_point = initial_point.to_display_point(snapshot);
|
||||||
|
@ -11885,6 +11889,52 @@ impl Editor {
|
||||||
self.searchable
|
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>) {
|
fn open_excerpts_in_split(&mut self, _: &OpenExcerptsSplit, cx: &mut ViewContext<Self>) {
|
||||||
self.open_excerpts_common(true, cx)
|
self.open_excerpts_common(true, cx)
|
||||||
}
|
}
|
||||||
|
@ -12399,7 +12449,7 @@ impl Editor {
|
||||||
fn hunks_for_selections(
|
fn hunks_for_selections(
|
||||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||||
selections: &[Selection<Anchor>],
|
selections: &[Selection<Anchor>],
|
||||||
) -> Vec<DiffHunk<MultiBufferRow>> {
|
) -> Vec<MultiBufferDiffHunk> {
|
||||||
let buffer_rows_for_selections = selections.iter().map(|selection| {
|
let buffer_rows_for_selections = selections.iter().map(|selection| {
|
||||||
let head = selection.head();
|
let head = selection.head();
|
||||||
let tail = selection.tail();
|
let tail = selection.tail();
|
||||||
|
@ -12418,7 +12468,7 @@ fn hunks_for_selections(
|
||||||
pub fn hunks_for_rows(
|
pub fn hunks_for_rows(
|
||||||
rows: impl Iterator<Item = Range<MultiBufferRow>>,
|
rows: impl Iterator<Item = Range<MultiBufferRow>>,
|
||||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||||
) -> Vec<DiffHunk<MultiBufferRow>> {
|
) -> Vec<MultiBufferDiffHunk> {
|
||||||
let mut hunks = Vec::new();
|
let mut hunks = Vec::new();
|
||||||
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
|
let mut processed_buffer_rows: HashMap<BufferId, HashSet<Range<text::Anchor>>> =
|
||||||
HashMap::default();
|
HashMap::default();
|
||||||
|
@ -12430,14 +12480,14 @@ pub fn hunks_for_rows(
|
||||||
// when the caret is just above or just below the deleted hunk.
|
// when the caret is just above or just below the deleted hunk.
|
||||||
let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed;
|
let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed;
|
||||||
let related_to_selection = if allow_adjacent {
|
let related_to_selection = if allow_adjacent {
|
||||||
hunk.associated_range.overlaps(&query_rows)
|
hunk.row_range.overlaps(&query_rows)
|
||||||
|| hunk.associated_range.start == query_rows.end
|
|| hunk.row_range.start == query_rows.end
|
||||||
|| hunk.associated_range.end == query_rows.start
|
|| hunk.row_range.end == query_rows.start
|
||||||
} else {
|
} else {
|
||||||
// `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected)
|
// `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.row_range` is exclusive (e.g. [2..3] means 2nd row is selected)
|
||||||
hunk.associated_range.overlaps(&selected_multi_buffer_rows)
|
hunk.row_range.overlaps(&selected_multi_buffer_rows)
|
||||||
|| selected_multi_buffer_rows.end == hunk.associated_range.start
|
|| selected_multi_buffer_rows.end == hunk.row_range.start
|
||||||
};
|
};
|
||||||
if related_to_selection {
|
if related_to_selection {
|
||||||
if !processed_buffer_rows
|
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() {
|
if hunk.diff_base_byte_range.is_empty() {
|
||||||
DiffHunkStatus::Added
|
DiffHunkStatus::Added
|
||||||
} else if hunk.associated_range.is_empty() {
|
} else if hunk.row_range.is_empty() {
|
||||||
DiffHunkStatus::Removed
|
DiffHunkStatus::Removed
|
||||||
} else {
|
} else {
|
||||||
DiffHunkStatus::Modified
|
DiffHunkStatus::Modified
|
||||||
|
|
|
@ -346,6 +346,7 @@ impl EditorElement {
|
||||||
register_action(view, cx, Editor::toggle_code_actions);
|
register_action(view, cx, Editor::toggle_code_actions);
|
||||||
register_action(view, cx, Editor::open_excerpts);
|
register_action(view, cx, Editor::open_excerpts);
|
||||||
register_action(view, cx, Editor::open_excerpts_in_split);
|
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_soft_wrap);
|
||||||
register_action(view, cx, Editor::toggle_tab_bar);
|
register_action(view, cx, Editor::toggle_tab_bar);
|
||||||
register_action(view, cx, Editor::toggle_line_numbers);
|
register_action(view, cx, Editor::toggle_line_numbers);
|
||||||
|
@ -3710,11 +3711,11 @@ impl EditorElement {
|
||||||
)
|
)
|
||||||
.map(|hunk| {
|
.map(|hunk| {
|
||||||
let start_display_row =
|
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)
|
.to_display_point(&snapshot.display_snapshot)
|
||||||
.row();
|
.row();
|
||||||
let mut end_display_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)
|
.to_display_point(&snapshot.display_snapshot)
|
||||||
.row();
|
.row();
|
||||||
if end_display_row != start_display_row {
|
if end_display_row != start_display_row {
|
||||||
|
|
|
@ -2,9 +2,9 @@ pub mod blame;
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
use git::diff::DiffHunkStatus;
|
||||||
use language::Point;
|
use language::Point;
|
||||||
use multi_buffer::{Anchor, MultiBufferRow};
|
use multi_buffer::{Anchor, MultiBufferDiffHunk};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||||
|
@ -49,25 +49,25 @@ impl DisplayDiffHunk {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diff_hunk_to_display(
|
pub fn diff_hunk_to_display(
|
||||||
hunk: &DiffHunk<MultiBufferRow>,
|
hunk: &MultiBufferDiffHunk,
|
||||||
snapshot: &DisplaySnapshot,
|
snapshot: &DisplaySnapshot,
|
||||||
) -> DisplayDiffHunk {
|
) -> DisplayDiffHunk {
|
||||||
let hunk_start_point = Point::new(hunk.associated_range.start.0, 0);
|
let hunk_start_point = Point::new(hunk.row_range.start.0, 0);
|
||||||
let hunk_start_point_sub = Point::new(hunk.associated_range.start.0.saturating_sub(1), 0);
|
let hunk_start_point_sub = Point::new(hunk.row_range.start.0.saturating_sub(1), 0);
|
||||||
let hunk_end_point_sub = Point::new(
|
let hunk_end_point_sub = Point::new(
|
||||||
hunk.associated_range
|
hunk.row_range
|
||||||
.end
|
.end
|
||||||
.0
|
.0
|
||||||
.saturating_sub(1)
|
.saturating_sub(1)
|
||||||
.max(hunk.associated_range.start.0),
|
.max(hunk.row_range.start.0),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
let status = hunk_status(hunk);
|
let status = hunk_status(hunk);
|
||||||
let is_removal = status == DiffHunkStatus::Removed;
|
let is_removal = status == DiffHunkStatus::Removed;
|
||||||
|
|
||||||
let folds_start = Point::new(hunk.associated_range.start.0.saturating_sub(2), 0);
|
let folds_start = Point::new(hunk.row_range.start.0.saturating_sub(2), 0);
|
||||||
let folds_end = Point::new(hunk.associated_range.end.0 + 2, 0);
|
let folds_end = Point::new(hunk.row_range.end.0 + 2, 0);
|
||||||
let folds_range = folds_start..folds_end;
|
let folds_range = folds_start..folds_end;
|
||||||
|
|
||||||
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
|
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
|
||||||
|
@ -87,7 +87,7 @@ pub fn diff_hunk_to_display(
|
||||||
} else {
|
} else {
|
||||||
let start = hunk_start_point.to_display_point(snapshot).row();
|
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 hunk_end_point = Point::new(hunk_end_row.0, 0);
|
||||||
|
|
||||||
let multi_buffer_start = snapshot.buffer_snapshot.anchor_after(hunk_start_point);
|
let multi_buffer_start = snapshot.buffer_snapshot.anchor_after(hunk_start_point);
|
||||||
|
@ -288,7 +288,7 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot
|
snapshot
|
||||||
.git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12))
|
.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<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
&expected,
|
&expected,
|
||||||
);
|
);
|
||||||
|
@ -296,7 +296,7 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snapshot
|
snapshot
|
||||||
.git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12))
|
.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<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
expected
|
expected
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -4,11 +4,12 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use collections::{hash_map, HashMap, HashSet};
|
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 gpui::{Action, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use multi_buffer::{
|
use multi_buffer::{
|
||||||
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
|
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
|
||||||
|
MultiBufferSnapshot, ToPoint,
|
||||||
};
|
};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use text::{BufferId, Point};
|
use text::{BufferId, Point};
|
||||||
|
@ -190,9 +191,9 @@ impl Editor {
|
||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
|
.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
|
||||||
.filter(|hunk| {
|
.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)
|
.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);
|
.to_display_point(&snapshot.display_snapshot);
|
||||||
let row_range_end =
|
let row_range_end =
|
||||||
display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row());
|
display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row());
|
||||||
|
@ -203,7 +204,7 @@ impl Editor {
|
||||||
|
|
||||||
fn toggle_hunks_expanded(
|
fn toggle_hunks_expanded(
|
||||||
&mut self,
|
&mut self,
|
||||||
hunks_to_toggle: Vec<DiffHunk<MultiBufferRow>>,
|
hunks_to_toggle: Vec<MultiBufferDiffHunk>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let previous_toggle_task = self.expanded_hunks.hunk_update_tasks.remove(&None);
|
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 {
|
for remaining_hunk in hunks_to_toggle {
|
||||||
let remaining_hunk_point_range =
|
let remaining_hunk_point_range =
|
||||||
Point::new(remaining_hunk.associated_range.start.0, 0)
|
Point::new(remaining_hunk.row_range.start.0, 0)
|
||||||
..Point::new(remaining_hunk.associated_range.end.0, 0);
|
..Point::new(remaining_hunk.row_range.end.0, 0);
|
||||||
hunks_to_expand.push(HoveredHunk {
|
hunks_to_expand.push(HoveredHunk {
|
||||||
status: hunk_status(&remaining_hunk),
|
status: hunk_status(&remaining_hunk),
|
||||||
multi_buffer_range: remaining_hunk_point_range
|
multi_buffer_range: remaining_hunk_point_range
|
||||||
|
@ -705,7 +706,7 @@ impl Editor {
|
||||||
fn to_diff_hunk(
|
fn to_diff_hunk(
|
||||||
hovered_hunk: &HoveredHunk,
|
hovered_hunk: &HoveredHunk,
|
||||||
multi_buffer_snapshot: &MultiBufferSnapshot,
|
multi_buffer_snapshot: &MultiBufferSnapshot,
|
||||||
) -> Option<DiffHunk<MultiBufferRow>> {
|
) -> Option<MultiBufferDiffHunk> {
|
||||||
let buffer_id = hovered_hunk
|
let buffer_id = hovered_hunk
|
||||||
.multi_buffer_range
|
.multi_buffer_range
|
||||||
.start
|
.start
|
||||||
|
@ -716,9 +717,8 @@ fn to_diff_hunk(
|
||||||
let point_range = hovered_hunk
|
let point_range = hovered_hunk
|
||||||
.multi_buffer_range
|
.multi_buffer_range
|
||||||
.to_point(multi_buffer_snapshot);
|
.to_point(multi_buffer_snapshot);
|
||||||
Some(DiffHunk {
|
Some(MultiBufferDiffHunk {
|
||||||
associated_range: MultiBufferRow(point_range.start.row)
|
row_range: MultiBufferRow(point_range.start.row)..MultiBufferRow(point_range.end.row),
|
||||||
..MultiBufferRow(point_range.end.row),
|
|
||||||
buffer_id,
|
buffer_id,
|
||||||
buffer_range,
|
buffer_range,
|
||||||
diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
|
diff_base_byte_range: hovered_hunk.diff_base_byte_range.clone(),
|
||||||
|
@ -868,7 +868,7 @@ fn editor_with_deleted_text(
|
||||||
fn buffer_diff_hunk(
|
fn buffer_diff_hunk(
|
||||||
buffer_snapshot: &MultiBufferSnapshot,
|
buffer_snapshot: &MultiBufferSnapshot,
|
||||||
row_range: Range<Point>,
|
row_range: Range<Point>,
|
||||||
) -> Option<DiffHunk<MultiBufferRow>> {
|
) -> Option<MultiBufferDiffHunk> {
|
||||||
let mut hunks = buffer_snapshot.git_diff_hunks_in_range(
|
let mut hunks = buffer_snapshot.git_diff_hunks_in_range(
|
||||||
MultiBufferRow(row_range.start.row)..MultiBufferRow(row_range.end.row),
|
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
|
.buffer_snapshot
|
||||||
.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
|
.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX)
|
||||||
.map(|hunk| {
|
.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)
|
.to_display_point(snapshot)
|
||||||
.row()
|
.row()
|
||||||
..Point::new(hunk.associated_range.end.0, 0)
|
..Point::new(hunk.row_range.end.0, 0)
|
||||||
.to_display_point(snapshot)
|
.to_display_point(snapshot)
|
||||||
.row();
|
.row();
|
||||||
let (_, buffer, _) = editor
|
let (_, buffer, _) = editor
|
||||||
.buffer()
|
.buffer()
|
||||||
.read(cx)
|
.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");
|
.expect("no excerpt for expanded buffer's hunk start");
|
||||||
let diff_base = buffer
|
let diff_base = buffer
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use std::{iter, ops::Range};
|
use std::{iter, ops::Range};
|
||||||
use sum_tree::SumTree;
|
use sum_tree::SumTree;
|
||||||
use text::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, Point};
|
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
|
||||||
|
|
||||||
pub use git2 as libgit;
|
pub use git2 as libgit;
|
||||||
use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
|
use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
|
||||||
|
@ -13,29 +13,30 @@ pub enum DiffHunkStatus {
|
||||||
Removed,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct DiffHunk<T> {
|
pub struct DiffHunk {
|
||||||
/// E.g. a range in multibuffer, that has an excerpt added, singleton buffer for which has this diff hunk.
|
/// The buffer range, expressed in terms of rows.
|
||||||
/// Consider a singleton buffer with 10 lines, all of them are modified — so a corresponding diff hunk would have a range 0..10.
|
pub row_range: Range<u32>,
|
||||||
/// And a multibuffer with the excerpt of lines 2-6 from the singleton buffer.
|
/// The range in the buffer to which this hunk corresponds.
|
||||||
/// 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 buffer_range: Range<Anchor>,
|
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>,
|
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;
|
type Summary = DiffHunkSummary;
|
||||||
|
|
||||||
fn summary(&self) -> Self::Summary {
|
fn summary(&self) -> Self::Summary {
|
||||||
DiffHunkSummary {
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BufferDiff {
|
pub struct BufferDiff {
|
||||||
last_buffer_version: Option<clock::Global>,
|
last_buffer_version: Option<clock::Global>,
|
||||||
tree: SumTree<DiffHunk<Anchor>>,
|
tree: SumTree<InternalDiffHunk>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BufferDiff {
|
impl BufferDiff {
|
||||||
|
@ -79,11 +80,12 @@ impl BufferDiff {
|
||||||
self.tree.is_empty()
|
self.tree.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn hunks_in_row_range<'a>(
|
pub fn hunks_in_row_range<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
range: Range<u32>,
|
range: Range<u32>,
|
||||||
buffer: &'a BufferSnapshot,
|
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 start = buffer.anchor_before(Point::new(range.start, 0));
|
||||||
let end = buffer.anchor_after(Point::new(range.end, 0));
|
let end = buffer.anchor_after(Point::new(range.end, 0));
|
||||||
|
|
||||||
|
@ -94,7 +96,7 @@ impl BufferDiff {
|
||||||
&'a self,
|
&'a self,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
buffer: &'a BufferSnapshot,
|
buffer: &'a BufferSnapshot,
|
||||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||||
let mut cursor = self
|
let mut cursor = self
|
||||||
.tree
|
.tree
|
||||||
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
||||||
|
@ -109,11 +111,8 @@ impl BufferDiff {
|
||||||
})
|
})
|
||||||
.flat_map(move |hunk| {
|
.flat_map(move |hunk| {
|
||||||
[
|
[
|
||||||
(
|
(&hunk.buffer_range.start, hunk.diff_base_byte_range.start),
|
||||||
&hunk.associated_range.start,
|
(&hunk.buffer_range.end, hunk.diff_base_byte_range.end),
|
||||||
hunk.diff_base_byte_range.start,
|
|
||||||
),
|
|
||||||
(&hunk.associated_range.end, hunk.diff_base_byte_range.end),
|
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
});
|
});
|
||||||
|
@ -129,10 +128,9 @@ impl BufferDiff {
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(DiffHunk {
|
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,
|
diff_base_byte_range: start_base..end_base,
|
||||||
buffer_range: buffer.anchor_before(start_point)..buffer.anchor_after(end_point),
|
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,
|
&'a self,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
buffer: &'a BufferSnapshot,
|
buffer: &'a BufferSnapshot,
|
||||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||||
let mut cursor = self
|
let mut cursor = self
|
||||||
.tree
|
.tree
|
||||||
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
||||||
|
@ -154,7 +152,7 @@ impl BufferDiff {
|
||||||
cursor.prev(buffer);
|
cursor.prev(buffer);
|
||||||
|
|
||||||
let hunk = cursor.item()?;
|
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 {
|
let end_row = if range.end.column > 0 {
|
||||||
range.end.row + 1
|
range.end.row + 1
|
||||||
} else {
|
} else {
|
||||||
|
@ -162,10 +160,9 @@ impl BufferDiff {
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(DiffHunk {
|
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(),
|
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||||
buffer_range: hunk.buffer_range.clone(),
|
buffer_range: hunk.buffer_range.clone(),
|
||||||
buffer_id: hunk.buffer_id,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -196,7 +193,7 @@ impl BufferDiff {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[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 start = text.anchor_before(Point::new(0, 0));
|
||||||
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
|
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
|
||||||
self.hunks_intersecting_range(start..end, text)
|
self.hunks_intersecting_range(start..end, text)
|
||||||
|
@ -229,7 +226,7 @@ impl BufferDiff {
|
||||||
hunk_index: usize,
|
hunk_index: usize,
|
||||||
buffer: &text::BufferSnapshot,
|
buffer: &text::BufferSnapshot,
|
||||||
buffer_row_divergence: &mut i64,
|
buffer_row_divergence: &mut i64,
|
||||||
) -> DiffHunk<Anchor> {
|
) -> InternalDiffHunk {
|
||||||
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
|
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
|
||||||
assert!(line_item_count > 0);
|
assert!(line_item_count > 0);
|
||||||
|
|
||||||
|
@ -284,11 +281,9 @@ impl BufferDiff {
|
||||||
let start = Point::new(buffer_row_range.start, 0);
|
let start = Point::new(buffer_row_range.start, 0);
|
||||||
let end = Point::new(buffer_row_range.end, 0);
|
let end = Point::new(buffer_row_range.end, 0);
|
||||||
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
|
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
|
||||||
DiffHunk {
|
InternalDiffHunk {
|
||||||
associated_range: buffer_range.clone(),
|
|
||||||
buffer_range,
|
buffer_range,
|
||||||
diff_base_byte_range,
|
diff_base_byte_range,
|
||||||
buffer_id: buffer.remote_id(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -302,17 +297,16 @@ pub fn assert_hunks<Iter>(
|
||||||
diff_base: &str,
|
diff_base: &str,
|
||||||
expected_hunks: &[(Range<u32>, &str, &str)],
|
expected_hunks: &[(Range<u32>, &str, &str)],
|
||||||
) where
|
) where
|
||||||
Iter: Iterator<Item = DiffHunk<u32>>,
|
Iter: Iterator<Item = DiffHunk>,
|
||||||
{
|
{
|
||||||
let actual_hunks = diff_hunks
|
let actual_hunks = diff_hunks
|
||||||
.map(|hunk| {
|
.map(|hunk| {
|
||||||
(
|
(
|
||||||
hunk.associated_range.clone(),
|
hunk.row_range.clone(),
|
||||||
&diff_base[hunk.diff_base_byte_range],
|
&diff_base[hunk.diff_base_byte_range],
|
||||||
buffer
|
buffer
|
||||||
.text_for_range(
|
.text_for_range(
|
||||||
Point::new(hunk.associated_range.start, 0)
|
Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0),
|
||||||
..Point::new(hunk.associated_range.end, 0),
|
|
||||||
)
|
)
|
||||||
.collect::<String>(),
|
.collect::<String>(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,8 +21,8 @@ use async_watch as watch;
|
||||||
pub use clock::ReplicaId;
|
pub use clock::ReplicaId;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AppContext, EventEmitter, HighlightStyle, ModelContext, Pixels, Task, TaskLabel,
|
AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext,
|
||||||
WindowContext,
|
Pixels, Task, TaskLabel, WindowContext,
|
||||||
};
|
};
|
||||||
use lsp::LanguageServerId;
|
use lsp::LanguageServerId;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -84,11 +84,17 @@ pub enum Capability {
|
||||||
|
|
||||||
pub type BufferRow = u32;
|
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,
|
/// An in-memory representation of a source code file, including its text,
|
||||||
/// syntax trees, git status, and diagnostics.
|
/// syntax trees, git status, and diagnostics.
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
text: TextBuffer,
|
text: TextBuffer,
|
||||||
diff_base: Option<Rope>,
|
diff_base: Option<BufferDiffBase>,
|
||||||
git_diff: git::diff::BufferDiff,
|
git_diff: git::diff::BufferDiff,
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
/// The mtime of the file when this buffer was last loaded from
|
/// 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).
|
/// 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.
|
/// The contents of a cell are (self.version, has_changes) at the time of a last call.
|
||||||
has_unsaved_edits: Cell<(clock::Global, bool)>,
|
has_unsaved_edits: Cell<(clock::Global, bool)>,
|
||||||
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
@ -308,7 +315,10 @@ pub enum Operation {
|
||||||
pub enum BufferEvent {
|
pub enum BufferEvent {
|
||||||
/// The buffer was changed in a way that must be
|
/// The buffer was changed in a way that must be
|
||||||
/// propagated to its other replicas.
|
/// propagated to its other replicas.
|
||||||
Operation(Operation),
|
Operation {
|
||||||
|
operation: Operation,
|
||||||
|
is_local: bool,
|
||||||
|
},
|
||||||
/// The buffer was edited.
|
/// The buffer was edited.
|
||||||
Edited,
|
Edited,
|
||||||
/// The buffer's `dirty` bit changed.
|
/// The buffer's `dirty` bit changed.
|
||||||
|
@ -644,7 +654,7 @@ impl Buffer {
|
||||||
id: self.remote_id().into(),
|
id: self.remote_id().into(),
|
||||||
file: self.file.as_ref().map(|f| f.to_proto(cx)),
|
file: self.file.as_ref().map(|f| f.to_proto(cx)),
|
||||||
base_text: self.base_text().to_string(),
|
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,
|
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
||||||
saved_version: proto::serialize_version(&self.saved_version),
|
saved_version: proto::serialize_version(&self.saved_version),
|
||||||
saved_mtime: self.saved_mtime.map(|time| time.into()),
|
saved_mtime: self.saved_mtime.map(|time| time.into()),
|
||||||
|
@ -734,12 +744,10 @@ impl Buffer {
|
||||||
was_dirty_before_starting_transaction: None,
|
was_dirty_before_starting_transaction: None,
|
||||||
has_unsaved_edits: Cell::new((buffer.version(), false)),
|
has_unsaved_edits: Cell::new((buffer.version(), false)),
|
||||||
text: buffer,
|
text: buffer,
|
||||||
diff_base: diff_base
|
diff_base: diff_base.map(|mut raw_diff_base| {
|
||||||
.map(|mut raw_diff_base| {
|
LineEnding::normalize(&mut raw_diff_base);
|
||||||
LineEnding::normalize(&mut raw_diff_base);
|
BufferDiffBase::Git(Rope::from(raw_diff_base))
|
||||||
raw_diff_base
|
}),
|
||||||
})
|
|
||||||
.map(Rope::from),
|
|
||||||
diff_base_version: 0,
|
diff_base_version: 0,
|
||||||
git_diff,
|
git_diff,
|
||||||
file,
|
file,
|
||||||
|
@ -759,6 +767,7 @@ impl Buffer {
|
||||||
completion_triggers_timestamp: Default::default(),
|
completion_triggers_timestamp: Default::default(),
|
||||||
deferred_ops: OperationQueue::new(),
|
deferred_ops: OperationQueue::new(),
|
||||||
has_conflict: false,
|
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)]
|
#[cfg(test)]
|
||||||
pub(crate) fn as_text_snapshot(&self) -> &text::BufferSnapshot {
|
pub(crate) fn as_text_snapshot(&self) -> &text::BufferSnapshot {
|
||||||
&self.text
|
&self.text
|
||||||
|
@ -961,20 +1016,23 @@ impl Buffer {
|
||||||
|
|
||||||
/// Returns the current diff base, see [Buffer::set_diff_base].
|
/// Returns the current diff base, see [Buffer::set_diff_base].
|
||||||
pub fn diff_base(&self) -> Option<&Rope> {
|
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
|
/// Sets the text that will be used to compute a Git diff
|
||||||
/// against the buffer text.
|
/// against the buffer text.
|
||||||
pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
|
pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
|
||||||
self.diff_base = diff_base
|
self.diff_base = diff_base.map(|mut raw_diff_base| {
|
||||||
.map(|mut raw_diff_base| {
|
LineEnding::normalize(&mut raw_diff_base);
|
||||||
LineEnding::normalize(&mut raw_diff_base);
|
BufferDiffBase::Git(Rope::from(raw_diff_base))
|
||||||
raw_diff_base
|
});
|
||||||
})
|
|
||||||
.map(Rope::from);
|
|
||||||
self.diff_base_version += 1;
|
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 {
|
cx.spawn(|buffer, mut cx| async move {
|
||||||
recalc_task.await;
|
recalc_task.await;
|
||||||
buffer
|
buffer
|
||||||
|
@ -992,14 +1050,21 @@ impl Buffer {
|
||||||
self.diff_base_version
|
self.diff_base_version
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recomputes the Git diff status.
|
/// Recomputes the diff.
|
||||||
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<()>> {
|
pub fn recalculate_diff(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<()>> {
|
||||||
let diff_base = self.diff_base.clone()?;
|
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 snapshot = self.snapshot();
|
||||||
|
|
||||||
let mut diff = self.git_diff.clone();
|
let mut diff = self.git_diff.clone();
|
||||||
let diff = cx.background_executor().spawn(async move {
|
let diff = cx.background_executor().spawn(async move {
|
||||||
diff.update(&diff_base, &snapshot).await;
|
diff.update(&diff_base_rope, &snapshot).await;
|
||||||
diff
|
diff
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1169,7 +1234,7 @@ impl Buffer {
|
||||||
lamport_timestamp,
|
lamport_timestamp,
|
||||||
};
|
};
|
||||||
self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx);
|
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>) {
|
fn request_autoindent(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
@ -1743,6 +1808,7 @@ impl Buffer {
|
||||||
lamport_timestamp,
|
lamport_timestamp,
|
||||||
cursor_shape,
|
cursor_shape,
|
||||||
},
|
},
|
||||||
|
true,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
self.non_text_state_update_count += 1;
|
self.non_text_state_update_count += 1;
|
||||||
|
@ -1889,7 +1955,7 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.end_transaction(cx);
|
self.end_transaction(cx);
|
||||||
self.send_operation(Operation::Buffer(edit_operation), cx);
|
self.send_operation(Operation::Buffer(edit_operation), true, cx);
|
||||||
Some(edit_id)
|
Some(edit_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1991,6 +2057,9 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
for operation in buffer_ops.iter() {
|
||||||
|
self.send_operation(Operation::Buffer(operation.clone()), false, cx);
|
||||||
|
}
|
||||||
self.text.apply_ops(buffer_ops);
|
self.text.apply_ops(buffer_ops);
|
||||||
self.deferred_ops.insert(deferred_ops);
|
self.deferred_ops.insert(deferred_ops);
|
||||||
self.flush_deferred_ops(cx);
|
self.flush_deferred_ops(cx);
|
||||||
|
@ -2114,8 +2183,16 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_operation(&mut self, operation: Operation, cx: &mut ModelContext<Self>) {
|
fn send_operation(
|
||||||
cx.emit(BufferEvent::Operation(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.
|
/// Removes the selections for a given peer.
|
||||||
|
@ -2130,7 +2207,7 @@ impl Buffer {
|
||||||
let old_version = self.version.clone();
|
let old_version = self.version.clone();
|
||||||
|
|
||||||
if let Some((transaction_id, operation)) = self.text.undo() {
|
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);
|
self.did_edit(&old_version, was_dirty, cx);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
|
@ -2147,7 +2224,7 @@ impl Buffer {
|
||||||
let was_dirty = self.is_dirty();
|
let was_dirty = self.is_dirty();
|
||||||
let old_version = self.version.clone();
|
let old_version = self.version.clone();
|
||||||
if let Some(operation) = self.text.undo_transaction(transaction_id) {
|
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);
|
self.did_edit(&old_version, was_dirty, cx);
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
@ -2167,7 +2244,7 @@ impl Buffer {
|
||||||
let operations = self.text.undo_to_transaction(transaction_id);
|
let operations = self.text.undo_to_transaction(transaction_id);
|
||||||
let undone = !operations.is_empty();
|
let undone = !operations.is_empty();
|
||||||
for operation in operations {
|
for operation in operations {
|
||||||
self.send_operation(Operation::Buffer(operation), cx);
|
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||||
}
|
}
|
||||||
if undone {
|
if undone {
|
||||||
self.did_edit(&old_version, was_dirty, cx)
|
self.did_edit(&old_version, was_dirty, cx)
|
||||||
|
@ -2181,7 +2258,7 @@ impl Buffer {
|
||||||
let old_version = self.version.clone();
|
let old_version = self.version.clone();
|
||||||
|
|
||||||
if let Some((transaction_id, operation)) = self.text.redo() {
|
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);
|
self.did_edit(&old_version, was_dirty, cx);
|
||||||
Some(transaction_id)
|
Some(transaction_id)
|
||||||
} else {
|
} else {
|
||||||
|
@ -2201,7 +2278,7 @@ impl Buffer {
|
||||||
let operations = self.text.redo_to_transaction(transaction_id);
|
let operations = self.text.redo_to_transaction(transaction_id);
|
||||||
let redone = !operations.is_empty();
|
let redone = !operations.is_empty();
|
||||||
for operation in operations {
|
for operation in operations {
|
||||||
self.send_operation(Operation::Buffer(operation), cx);
|
self.send_operation(Operation::Buffer(operation), true, cx);
|
||||||
}
|
}
|
||||||
if redone {
|
if redone {
|
||||||
self.did_edit(&old_version, was_dirty, cx)
|
self.did_edit(&old_version, was_dirty, cx)
|
||||||
|
@ -2218,6 +2295,7 @@ impl Buffer {
|
||||||
triggers,
|
triggers,
|
||||||
lamport_timestamp: self.completion_triggers_timestamp,
|
lamport_timestamp: self.completion_triggers_timestamp,
|
||||||
},
|
},
|
||||||
|
true,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -2297,7 +2375,7 @@ impl Buffer {
|
||||||
let ops = self.text.randomly_undo_redo(rng);
|
let ops = self.text.randomly_undo_redo(rng);
|
||||||
if !ops.is_empty() {
|
if !ops.is_empty() {
|
||||||
for op in ops {
|
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);
|
self.did_edit(&old_version, was_dirty, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3638,12 +3716,12 @@ impl BufferSnapshot {
|
||||||
!self.git_diff.is_empty()
|
!self.git_diff.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all the Git diff hunks intersecting the given
|
/// Returns all the Git diff hunks intersecting the given row range.
|
||||||
/// row range.
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn git_diff_hunks_in_row_range(
|
pub fn git_diff_hunks_in_row_range(
|
||||||
&self,
|
&self,
|
||||||
range: Range<BufferRow>,
|
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)
|
self.git_diff.hunks_in_row_range(range, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3652,7 +3730,7 @@ impl BufferSnapshot {
|
||||||
pub fn git_diff_hunks_intersecting_range(
|
pub fn git_diff_hunks_intersecting_range(
|
||||||
&self,
|
&self,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
) -> impl '_ + Iterator<Item = git::diff::DiffHunk<u32>> {
|
) -> impl '_ + Iterator<Item = git::diff::DiffHunk> {
|
||||||
self.git_diff.hunks_intersecting_range(range, self)
|
self.git_diff.hunks_intersecting_range(range, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3661,7 +3739,7 @@ impl BufferSnapshot {
|
||||||
pub fn git_diff_hunks_intersecting_range_rev(
|
pub fn git_diff_hunks_intersecting_range_rev(
|
||||||
&self,
|
&self,
|
||||||
range: Range<Anchor>,
|
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)
|
self.git_diff.hunks_intersecting_range_rev(range, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ use crate::Buffer;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use futures::FutureExt as _;
|
use futures::FutureExt as _;
|
||||||
|
use git::diff::assert_hunks;
|
||||||
use gpui::{AppContext, BorrowAppContext, Model};
|
use gpui::{AppContext, BorrowAppContext, Model};
|
||||||
use gpui::{Context, TestAppContext};
|
use gpui::{Context, TestAppContext};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
@ -275,13 +276,19 @@ fn test_edit_events(cx: &mut gpui::AppContext) {
|
||||||
|buffer, cx| {
|
|buffer, cx| {
|
||||||
let buffer_1_events = buffer_1_events.clone();
|
let buffer_1_events = buffer_1_events.clone();
|
||||||
cx.subscribe(&buffer1, move |_, _, event, _| match event.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),
|
event => buffer_1_events.lock().push(event),
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
let buffer_2_events = buffer_2_events.clone();
|
let buffer_2_events = buffer_2_events.clone();
|
||||||
cx.subscribe(&buffer2, move |_, _, event, _| {
|
cx.subscribe(&buffer2, move |_, _, event, _| match event.clone() {
|
||||||
buffer_2_events.lock().push(event.clone())
|
BufferEvent::Operation {
|
||||||
|
is_local: false, ..
|
||||||
|
} => {}
|
||||||
|
event => buffer_2_events.lock().push(event),
|
||||||
})
|
})
|
||||||
.detach();
|
.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)]
|
#[gpui::test(iterations = 100)]
|
||||||
fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
||||||
let min_peers = env::var("MIN_PEERS")
|
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)));
|
buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
|
||||||
let network = network.clone();
|
let network = network.clone();
|
||||||
cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
|
cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
|
||||||
if let BufferEvent::Operation(op) = event {
|
if let BufferEvent::Operation {
|
||||||
network
|
operation,
|
||||||
.lock()
|
is_local: true,
|
||||||
.broadcast(buffer.replica_id(), vec![proto::serialize_operation(op)]);
|
} = event
|
||||||
|
{
|
||||||
|
network.lock().broadcast(
|
||||||
|
buffer.replica_id(),
|
||||||
|
vec![proto::serialize_operation(operation)],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.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)));
|
new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
|
||||||
let network = network.clone();
|
let network = network.clone();
|
||||||
cx.subscribe(&cx.handle(), move |buffer, _, event, _| {
|
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(
|
network.lock().broadcast(
|
||||||
buffer.replica_id(),
|
buffer.replica_id(),
|
||||||
vec![proto::serialize_operation(op)],
|
vec![proto::serialize_operation(operation)],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -27,7 +27,6 @@ collections.workspace = true
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
git.workspace = true
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
|
|
@ -5,7 +5,6 @@ use anyhow::{anyhow, Result};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||||
use futures::{channel::mpsc, SinkExt};
|
use futures::{channel::mpsc, SinkExt};
|
||||||
use git::diff::DiffHunk;
|
|
||||||
use gpui::{AppContext, EntityId, EventEmitter, Model, ModelContext};
|
use gpui::{AppContext, EntityId, EventEmitter, Model, ModelContext};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
|
@ -110,6 +109,19 @@ pub enum Event {
|
||||||
DiagnosticsUpdated,
|
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;
|
pub type MultiBufferPoint = Point;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, serde::Deserialize)]
|
#[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(
|
pub fn git_diff_hunks_in_range_rev(
|
||||||
&self,
|
&self,
|
||||||
row_range: Range<MultiBufferRow>,
|
row_range: Range<MultiBufferRow>,
|
||||||
) -> impl Iterator<Item = DiffHunk<MultiBufferRow>> + '_ {
|
) -> impl Iterator<Item = MultiBufferDiffHunk> + '_ {
|
||||||
let mut cursor = self.excerpts.cursor::<Point>(&());
|
let mut cursor = self.excerpts.cursor::<Point>(&());
|
||||||
|
|
||||||
cursor.seek(&Point::new(row_range.end.0, 0), Bias::Left, &());
|
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)
|
.git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end)
|
||||||
.map(move |hunk| {
|
.map(move |hunk| {
|
||||||
let start = multibuffer_start.row
|
let start = multibuffer_start.row
|
||||||
+ hunk
|
+ hunk.row_range.start.saturating_sub(excerpt_start_point.row);
|
||||||
.associated_range
|
|
||||||
.start
|
|
||||||
.saturating_sub(excerpt_start_point.row);
|
|
||||||
let end = multibuffer_start.row
|
let end = multibuffer_start.row
|
||||||
+ hunk
|
+ hunk
|
||||||
.associated_range
|
.row_range
|
||||||
.end
|
.end
|
||||||
.min(excerpt_end_point.row + 1)
|
.min(excerpt_end_point.row + 1)
|
||||||
.saturating_sub(excerpt_start_point.row);
|
.saturating_sub(excerpt_start_point.row);
|
||||||
|
|
||||||
DiffHunk {
|
MultiBufferDiffHunk {
|
||||||
associated_range: MultiBufferRow(start)..MultiBufferRow(end),
|
row_range: MultiBufferRow(start)..MultiBufferRow(end),
|
||||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||||
buffer_range: hunk.buffer_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(
|
pub fn git_diff_hunks_in_range(
|
||||||
&self,
|
&self,
|
||||||
row_range: Range<MultiBufferRow>,
|
row_range: Range<MultiBufferRow>,
|
||||||
) -> impl Iterator<Item = DiffHunk<MultiBufferRow>> + '_ {
|
) -> impl Iterator<Item = MultiBufferDiffHunk> + '_ {
|
||||||
let mut cursor = self.excerpts.cursor::<Point>(&());
|
let mut cursor = self.excerpts.cursor::<Point>(&());
|
||||||
|
|
||||||
cursor.seek(&Point::new(row_range.start.0, 0), Bias::Left, &());
|
cursor.seek(&Point::new(row_range.start.0, 0), Bias::Left, &());
|
||||||
|
@ -3673,23 +3682,20 @@ impl MultiBufferSnapshot {
|
||||||
MultiBufferRow(0)..MultiBufferRow(1)
|
MultiBufferRow(0)..MultiBufferRow(1)
|
||||||
} else {
|
} else {
|
||||||
let start = multibuffer_start.row
|
let start = multibuffer_start.row
|
||||||
+ hunk
|
+ hunk.row_range.start.saturating_sub(excerpt_rows.start);
|
||||||
.associated_range
|
|
||||||
.start
|
|
||||||
.saturating_sub(excerpt_rows.start);
|
|
||||||
let end = multibuffer_start.row
|
let end = multibuffer_start.row
|
||||||
+ hunk
|
+ hunk
|
||||||
.associated_range
|
.row_range
|
||||||
.end
|
.end
|
||||||
.min(excerpt_rows.end + 1)
|
.min(excerpt_rows.end + 1)
|
||||||
.saturating_sub(excerpt_rows.start);
|
.saturating_sub(excerpt_rows.start);
|
||||||
MultiBufferRow(start)..MultiBufferRow(end)
|
MultiBufferRow(start)..MultiBufferRow(end)
|
||||||
};
|
};
|
||||||
DiffHunk {
|
MultiBufferDiffHunk {
|
||||||
associated_range: buffer_range,
|
row_range: buffer_range,
|
||||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||||
buffer_range: hunk.buffer_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();
|
let buffer_id = buffer.read(cx).remote_id();
|
||||||
match event {
|
match event {
|
||||||
BufferEvent::Operation(operation) => {
|
BufferEvent::Operation {
|
||||||
|
operation,
|
||||||
|
is_local: true,
|
||||||
|
} => {
|
||||||
let operation = language::proto::serialize_operation(operation);
|
let operation = language::proto::serialize_operation(operation);
|
||||||
|
|
||||||
if let Some(ssh) = &self.ssh_session {
|
if let Some(ssh) = &self.ssh_session {
|
||||||
|
@ -2267,7 +2270,7 @@ impl Project {
|
||||||
.filter_map(|buffer| {
|
.filter_map(|buffer| {
|
||||||
let buffer = buffer.upgrade()?;
|
let buffer = buffer.upgrade()?;
|
||||||
buffer
|
buffer
|
||||||
.update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx))
|
.update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx))
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
})
|
})
|
||||||
|
|
|
@ -3288,7 +3288,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
|
||||||
cx.subscribe(&buffer1, {
|
cx.subscribe(&buffer1, {
|
||||||
let events = events.clone();
|
let events = events.clone();
|
||||||
move |_, _, event, _| match event {
|
move |_, _, event, _| match event {
|
||||||
BufferEvent::Operation(_) => {}
|
BufferEvent::Operation { .. } => {}
|
||||||
_ => events.lock().push(event.clone()),
|
_ => events.lock().push(event.clone()),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -146,12 +146,15 @@ impl HeadlessProject {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
BufferEvent::Operation(op) => cx
|
BufferEvent::Operation {
|
||||||
|
operation,
|
||||||
|
is_local: true,
|
||||||
|
} => cx
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.spawn(self.session.request(proto::UpdateBuffer {
|
.spawn(self.session.request(proto::UpdateBuffer {
|
||||||
project_id: SSH_PROJECT_ID,
|
project_id: SSH_PROJECT_ID,
|
||||||
buffer_id: buffer.read(cx).remote_id().to_proto(),
|
buffer_id: buffer.read(cx).remote_id().to_proto(),
|
||||||
operations: vec![serialize_operation(op)],
|
operations: vec![serialize_operation(operation)],
|
||||||
}))
|
}))
|
||||||
.detach(),
|
.detach(),
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
@ -13,6 +13,7 @@ mod undo_map;
|
||||||
pub use anchor::*;
|
pub use anchor::*;
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
pub use clock::ReplicaId;
|
pub use clock::ReplicaId;
|
||||||
|
use clock::LOCAL_BRANCH_REPLICA_ID;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use locator::Locator;
|
use locator::Locator;
|
||||||
use operation_queue::OperationQueue;
|
use operation_queue::OperationQueue;
|
||||||
|
@ -715,6 +716,19 @@ impl Buffer {
|
||||||
self.snapshot.clone()
|
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 {
|
pub fn replica_id(&self) -> ReplicaId {
|
||||||
self.lamport_clock.replica_id
|
self.lamport_clock.replica_id
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue