logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
63
crates/multi_buffer/Cargo.toml
Normal file
63
crates/multi_buffer/Cargo.toml
Normal file
@@ -0,0 +1,63 @@
|
||||
[package]
|
||||
name = "multi_buffer"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/multi_buffer.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"buffer_diff/test-support",
|
||||
"gpui/test-support",
|
||||
"language/test-support",
|
||||
"text/test-support",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
ctor.workspace = true
|
||||
futures-lite.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rand.workspace = true
|
||||
rope.workspace = true
|
||||
settings.workspace = true
|
||||
serde.workspace = true
|
||||
smallvec.workspace = true
|
||||
sum_tree.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
ztracing.workspace = true
|
||||
tracing.workspace = true
|
||||
util.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
rand.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["tracing"]
|
||||
1
crates/multi_buffer/LICENSE-GPL
Symbolic link
1
crates/multi_buffer/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
536
crates/multi_buffer/src/anchor.rs
Normal file
536
crates/multi_buffer/src/anchor.rs
Normal file
@@ -0,0 +1,536 @@
|
||||
use crate::{
|
||||
ExcerptSummary, MultiBufferDimension, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
|
||||
PathKeyIndex, find_diff_state,
|
||||
};
|
||||
|
||||
use super::{MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use language::{BufferSnapshot, Point};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
ops::{Add, AddAssign, Range, Sub},
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
use text::BufferId;
|
||||
|
||||
/// A multibuffer anchor derived from an anchor into a specific excerpted buffer.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct ExcerptAnchor {
|
||||
pub(crate) text_anchor: text::Anchor,
|
||||
pub(crate) path: PathKeyIndex,
|
||||
pub(crate) diff_base_anchor: Option<text::Anchor>,
|
||||
}
|
||||
|
||||
/// A stable reference to a position within a [`MultiBuffer`](super::MultiBuffer).
|
||||
///
|
||||
/// Unlike simple offsets, anchors remain valid as the text is edited, automatically
|
||||
/// adjusting to reflect insertions and deletions around them.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Anchor {
|
||||
/// An anchor that always resolves to the start of the multibuffer.
|
||||
Min,
|
||||
/// An anchor that's attached to a specific excerpted buffer.
|
||||
Excerpt(ExcerptAnchor),
|
||||
/// An anchor that always resolves to the end of the multibuffer.
|
||||
Max,
|
||||
}
|
||||
|
||||
pub(crate) enum AnchorSeekTarget<'a> {
|
||||
// buffer no longer exists at its original path key in the multibuffer
|
||||
Missing {
|
||||
path_key: &'a PathKey,
|
||||
},
|
||||
// we have excerpts for the buffer at the expected path key
|
||||
Excerpt {
|
||||
path_key: &'a PathKey,
|
||||
path_key_index: PathKeyIndex,
|
||||
anchor: text::Anchor,
|
||||
snapshot: &'a BufferSnapshot,
|
||||
},
|
||||
// no excerpts and it's a min or max anchor
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AnchorSeekTarget<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Excerpt {
|
||||
path_key,
|
||||
path_key_index: _,
|
||||
anchor,
|
||||
snapshot: _,
|
||||
} => f
|
||||
.debug_struct("Excerpt")
|
||||
.field("path_key", path_key)
|
||||
.field("anchor", anchor)
|
||||
.finish(),
|
||||
Self::Missing { path_key } => f
|
||||
.debug_struct("Missing")
|
||||
.field("path_key", path_key)
|
||||
.finish(),
|
||||
Self::Empty => f.debug_struct("Empty").finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Anchor {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Anchor::Min => write!(f, "Anchor::Min"),
|
||||
Anchor::Max => write!(f, "Anchor::Max"),
|
||||
Anchor::Excerpt(excerpt_anchor) => write!(f, "{excerpt_anchor:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExcerptAnchor> for Anchor {
|
||||
fn from(anchor: ExcerptAnchor) -> Self {
|
||||
Anchor::Excerpt(anchor)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExcerptAnchor {
|
||||
pub(crate) fn buffer_id(&self) -> BufferId {
|
||||
self.text_anchor.buffer_id
|
||||
}
|
||||
|
||||
pub(crate) fn text_anchor(&self) -> text::Anchor {
|
||||
self.text_anchor
|
||||
}
|
||||
|
||||
pub(crate) fn with_diff_base_anchor(mut self, diff_base_anchor: text::Anchor) -> Self {
|
||||
self.diff_base_anchor = Some(diff_base_anchor);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> Ordering {
|
||||
let Some(self_path_key) = snapshot.path_keys.get_index(self.path.0 as usize) else {
|
||||
panic!("anchor's path was never added to multibuffer")
|
||||
};
|
||||
let Some(other_path_key) = snapshot.path_keys.get_index(other.path.0 as usize) else {
|
||||
panic!("anchor's path was never added to multibuffer")
|
||||
};
|
||||
|
||||
match self_path_key.cmp(other_path_key) {
|
||||
Ordering::Equal => (),
|
||||
ordering => return ordering,
|
||||
}
|
||||
|
||||
// in the case that you removed the buffer containing self,
|
||||
// and added the buffer containing other with the same path key
|
||||
// (ordering is arbitrary but consistent)
|
||||
if self.text_anchor.buffer_id != other.text_anchor.buffer_id {
|
||||
return self.text_anchor.buffer_id.cmp(&other.text_anchor.buffer_id);
|
||||
}
|
||||
|
||||
// two anchors into the same buffer at the same path
|
||||
let Some(buffer) = snapshot
|
||||
.buffers
|
||||
.get(&self.text_anchor.buffer_id)
|
||||
.filter(|buffer_state| buffer_state.path_key == *self_path_key)
|
||||
else {
|
||||
// buffer no longer exists at the original path (which may have been reused for a different buffer),
|
||||
// so no way to compare the anchors
|
||||
return Ordering::Equal;
|
||||
};
|
||||
// two anchors into the same buffer at the same path that still exists at that path in the multibuffer
|
||||
let text_cmp = self
|
||||
.text_anchor()
|
||||
.cmp(&other.text_anchor(), &buffer.buffer_snapshot);
|
||||
if text_cmp != Ordering::Equal {
|
||||
return text_cmp;
|
||||
}
|
||||
|
||||
if (self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some())
|
||||
&& let Some(base_text) = find_diff_state(&snapshot.diffs, self.text_anchor.buffer_id)
|
||||
.map(|diff| diff.base_text())
|
||||
{
|
||||
let self_anchor = self.diff_base_anchor.filter(|a| a.is_valid(base_text));
|
||||
let other_anchor = other.diff_base_anchor.filter(|a| a.is_valid(base_text));
|
||||
return match (self_anchor, other_anchor) {
|
||||
(Some(a), Some(b)) => a.cmp(&b, base_text),
|
||||
(Some(_), None) => match other.text_anchor().bias {
|
||||
Bias::Left => Ordering::Greater,
|
||||
Bias::Right => Ordering::Less,
|
||||
},
|
||||
(None, Some(_)) => match self.text_anchor().bias {
|
||||
Bias::Left => Ordering::Less,
|
||||
Bias::Right => Ordering::Greater,
|
||||
},
|
||||
(None, None) => Ordering::Equal,
|
||||
};
|
||||
}
|
||||
|
||||
Ordering::Equal
|
||||
}
|
||||
|
||||
fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Self {
|
||||
if self.text_anchor.bias == Bias::Left {
|
||||
return *self;
|
||||
}
|
||||
let Some(buffer) = snapshot.buffer_for_id(self.text_anchor.buffer_id) else {
|
||||
return *self;
|
||||
};
|
||||
let text_anchor = self.text_anchor().bias_left(&buffer);
|
||||
let ret = Self::in_buffer(self.path, text_anchor);
|
||||
if let Some(diff_base_anchor) = self.diff_base_anchor {
|
||||
if let Some(diff) = find_diff_state(&snapshot.diffs, self.text_anchor.buffer_id)
|
||||
&& diff_base_anchor.is_valid(&diff.base_text())
|
||||
{
|
||||
ret.with_diff_base_anchor(diff_base_anchor.bias_left(diff.base_text()))
|
||||
} else {
|
||||
ret.with_diff_base_anchor(diff_base_anchor)
|
||||
}
|
||||
} else {
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Self {
|
||||
if self.text_anchor.bias == Bias::Right {
|
||||
return *self;
|
||||
}
|
||||
let Some(buffer) = snapshot.buffer_for_id(self.text_anchor.buffer_id) else {
|
||||
return *self;
|
||||
};
|
||||
let text_anchor = self.text_anchor().bias_right(&buffer);
|
||||
let ret = Self::in_buffer(self.path, text_anchor);
|
||||
if let Some(diff_base_anchor) = self.diff_base_anchor {
|
||||
if let Some(diff) = find_diff_state(&snapshot.diffs, self.text_anchor.buffer_id)
|
||||
&& diff_base_anchor.is_valid(&diff.base_text())
|
||||
{
|
||||
ret.with_diff_base_anchor(diff_base_anchor.bias_right(diff.base_text()))
|
||||
} else {
|
||||
ret.with_diff_base_anchor(diff_base_anchor)
|
||||
}
|
||||
} else {
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn in_buffer(path: PathKeyIndex, text_anchor: text::Anchor) -> Self {
|
||||
ExcerptAnchor {
|
||||
path,
|
||||
diff_base_anchor: None,
|
||||
text_anchor,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
|
||||
let Some(target) = self.try_seek_target(snapshot) else {
|
||||
return false;
|
||||
};
|
||||
let Some(buffer_snapshot) = snapshot.buffer_for_id(self.buffer_id()) else {
|
||||
return false;
|
||||
};
|
||||
// Early check to avoid invalid comparisons when seeking
|
||||
if !buffer_snapshot.can_resolve(&self.text_anchor) {
|
||||
return false;
|
||||
}
|
||||
let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>(());
|
||||
cursor.seek(&target, Bias::Left);
|
||||
let Some(excerpt) = cursor.item() else {
|
||||
return false;
|
||||
};
|
||||
if excerpt.buffer_id != buffer_snapshot.remote_id() {
|
||||
return false;
|
||||
}
|
||||
let is_valid = self.text_anchor == excerpt.range.context.start
|
||||
|| self.text_anchor == excerpt.range.context.end
|
||||
|| self.text_anchor.is_valid(&buffer_snapshot);
|
||||
is_valid
|
||||
&& excerpt
|
||||
.range
|
||||
.context
|
||||
.start
|
||||
.cmp(&self.text_anchor(), buffer_snapshot)
|
||||
.is_le()
|
||||
&& excerpt
|
||||
.range
|
||||
.context
|
||||
.end
|
||||
.cmp(&self.text_anchor(), buffer_snapshot)
|
||||
.is_ge()
|
||||
}
|
||||
|
||||
pub(crate) fn seek_target<'a>(
|
||||
&self,
|
||||
snapshot: &'a MultiBufferSnapshot,
|
||||
) -> AnchorSeekTarget<'a> {
|
||||
self.try_seek_target(snapshot)
|
||||
.expect("anchor is from different multi-buffer")
|
||||
}
|
||||
|
||||
pub(crate) fn try_seek_target<'a>(
|
||||
&self,
|
||||
snapshot: &'a MultiBufferSnapshot,
|
||||
) -> Option<AnchorSeekTarget<'a>> {
|
||||
let path_key = snapshot.try_path_for_anchor(*self)?;
|
||||
|
||||
let Some(state) = snapshot
|
||||
.buffers
|
||||
.get(&self.buffer_id())
|
||||
.filter(|state| &state.path_key == path_key)
|
||||
else {
|
||||
return Some(AnchorSeekTarget::Missing { path_key });
|
||||
};
|
||||
|
||||
Some(AnchorSeekTarget::Excerpt {
|
||||
path_key,
|
||||
path_key_index: self.path,
|
||||
anchor: self.text_anchor(),
|
||||
snapshot: &state.buffer_snapshot,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for ExcerptAnchor {
|
||||
fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset {
|
||||
Anchor::from(*self).to_offset(snapshot)
|
||||
}
|
||||
|
||||
fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 {
|
||||
Anchor::from(*self).to_offset_utf16(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPoint for ExcerptAnchor {
|
||||
fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point {
|
||||
Anchor::from(*self).to_point(snapshot)
|
||||
}
|
||||
|
||||
fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> rope::PointUtf16 {
|
||||
Anchor::from(*self).to_point_utf16(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
impl Anchor {
|
||||
pub fn is_min(&self) -> bool {
|
||||
matches!(self, Self::Min)
|
||||
}
|
||||
|
||||
pub fn is_max(&self) -> bool {
|
||||
matches!(self, Self::Max)
|
||||
}
|
||||
|
||||
pub(crate) fn in_buffer(path: PathKeyIndex, text_anchor: text::Anchor) -> Self {
|
||||
Self::Excerpt(ExcerptAnchor::in_buffer(path, text_anchor))
|
||||
}
|
||||
|
||||
pub(crate) fn range_in_buffer(path: PathKeyIndex, range: Range<text::Anchor>) -> Range<Self> {
|
||||
Self::in_buffer(path, range.start)..Self::in_buffer(path, range.end)
|
||||
}
|
||||
|
||||
pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
|
||||
match (self, other) {
|
||||
(Anchor::Min, Anchor::Min) => return Ordering::Equal,
|
||||
(Anchor::Max, Anchor::Max) => return Ordering::Equal,
|
||||
(Anchor::Min, _) => return Ordering::Less,
|
||||
(Anchor::Max, _) => return Ordering::Greater,
|
||||
(_, Anchor::Max) => return Ordering::Less,
|
||||
(_, Anchor::Min) => return Ordering::Greater,
|
||||
(Anchor::Excerpt(self_excerpt_anchor), Anchor::Excerpt(other_excerpt_anchor)) => {
|
||||
self_excerpt_anchor.cmp(other_excerpt_anchor, snapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias(&self) -> Bias {
|
||||
match self {
|
||||
Anchor::Min => Bias::Left,
|
||||
Anchor::Max => Bias::Right,
|
||||
Anchor::Excerpt(anchor) => anchor.text_anchor.bias,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
match self {
|
||||
Anchor::Min => *self,
|
||||
Anchor::Max => snapshot.anchor_before(snapshot.max_point()),
|
||||
Anchor::Excerpt(anchor) => Anchor::Excerpt(anchor.bias_left(snapshot)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
match self {
|
||||
Anchor::Max => *self,
|
||||
Anchor::Min => snapshot.anchor_after(Point::zero()),
|
||||
Anchor::Excerpt(anchor) => Anchor::Excerpt(anchor.bias_right(snapshot)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn summary<D>(&self, snapshot: &MultiBufferSnapshot) -> D
|
||||
where
|
||||
D: MultiBufferDimension
|
||||
+ Ord
|
||||
+ Sub<Output = D::TextDimension>
|
||||
+ Sub<D::TextDimension, Output = D>
|
||||
+ AddAssign<D::TextDimension>
|
||||
+ Add<D::TextDimension, Output = D>,
|
||||
D::TextDimension: Sub<Output = D::TextDimension> + Ord,
|
||||
{
|
||||
snapshot.summary_for_anchor(self)
|
||||
}
|
||||
|
||||
pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
|
||||
match self {
|
||||
Anchor::Min | Anchor::Max => true,
|
||||
Anchor::Excerpt(excerpt_anchor) => excerpt_anchor.is_valid(snapshot),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_excerpt_anchor(&self, snapshot: &MultiBufferSnapshot) -> Option<ExcerptAnchor> {
|
||||
match self {
|
||||
Anchor::Min => {
|
||||
let excerpt = snapshot.excerpts.first()?;
|
||||
|
||||
Some(ExcerptAnchor {
|
||||
text_anchor: excerpt.range.context.start,
|
||||
path: excerpt.path_key_index,
|
||||
diff_base_anchor: None,
|
||||
})
|
||||
}
|
||||
Anchor::Excerpt(excerpt_anchor) => Some(*excerpt_anchor),
|
||||
Anchor::Max => {
|
||||
let excerpt = snapshot.excerpts.last()?;
|
||||
|
||||
Some(ExcerptAnchor {
|
||||
text_anchor: excerpt.range.context.end,
|
||||
path: excerpt.path_key_index,
|
||||
diff_base_anchor: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn seek_target<'a>(
|
||||
&self,
|
||||
snapshot: &'a MultiBufferSnapshot,
|
||||
) -> AnchorSeekTarget<'a> {
|
||||
let Some(excerpt_anchor) = self.to_excerpt_anchor(snapshot) else {
|
||||
return AnchorSeekTarget::Empty;
|
||||
};
|
||||
|
||||
excerpt_anchor.seek_target(snapshot)
|
||||
}
|
||||
|
||||
pub(crate) fn excerpt_anchor(&self) -> Option<ExcerptAnchor> {
|
||||
match self {
|
||||
Anchor::Min | Anchor::Max => None,
|
||||
Anchor::Excerpt(excerpt_anchor) => Some(*excerpt_anchor),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn text_anchor(&self) -> Option<text::Anchor> {
|
||||
match self {
|
||||
Anchor::Min | Anchor::Max => None,
|
||||
Anchor::Excerpt(excerpt_anchor) => Some(excerpt_anchor.text_anchor()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn opaque_id(&self) -> Option<[u8; 20]> {
|
||||
self.text_anchor().map(|a| a.opaque_id())
|
||||
}
|
||||
|
||||
/// Note: anchor_to_buffer_anchor is probably what you want
|
||||
pub fn raw_text_anchor(&self) -> Option<text::Anchor> {
|
||||
match self {
|
||||
Anchor::Min | Anchor::Max => None,
|
||||
Anchor::Excerpt(excerpt_anchor) => Some(excerpt_anchor.text_anchor),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn try_seek_target<'a>(
|
||||
&self,
|
||||
snapshot: &'a MultiBufferSnapshot,
|
||||
) -> Option<AnchorSeekTarget<'a>> {
|
||||
let Some(excerpt_anchor) = self.to_excerpt_anchor(snapshot) else {
|
||||
return Some(AnchorSeekTarget::Empty);
|
||||
};
|
||||
excerpt_anchor.try_seek_target(snapshot)
|
||||
}
|
||||
|
||||
/// Returns the text anchor for this anchor.
|
||||
/// Panics if the anchor is from a different buffer.
|
||||
pub fn text_anchor_in(&self, buffer: &BufferSnapshot) -> text::Anchor {
|
||||
match self {
|
||||
Anchor::Min => text::Anchor::min_for_buffer(buffer.remote_id()),
|
||||
Anchor::Excerpt(excerpt_anchor) => {
|
||||
let text_anchor = excerpt_anchor.text_anchor;
|
||||
assert_eq!(text_anchor.buffer_id, buffer.remote_id());
|
||||
text_anchor
|
||||
}
|
||||
Anchor::Max => text::Anchor::max_for_buffer(buffer.remote_id()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff_base_anchor(&self) -> Option<text::Anchor> {
|
||||
self.excerpt_anchor()?.diff_base_anchor
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn expect_text_anchor(&self) -> text::Anchor {
|
||||
self.excerpt_anchor().unwrap().text_anchor
|
||||
}
|
||||
|
||||
pub fn with_diff_base_anchor(mut self, diff_base_anchor: text::Anchor) -> Self {
|
||||
match &mut self {
|
||||
Anchor::Min | Anchor::Max => {}
|
||||
Anchor::Excerpt(excerpt_anchor) => {
|
||||
excerpt_anchor.diff_base_anchor = Some(diff_base_anchor);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for Anchor {
|
||||
fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset {
|
||||
self.summary(snapshot)
|
||||
}
|
||||
fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 {
|
||||
self.summary(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToPoint for Anchor {
|
||||
fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point {
|
||||
self.summary(snapshot)
|
||||
}
|
||||
fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> rope::PointUtf16 {
|
||||
self.summary(snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnchorRangeExt {
|
||||
fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
|
||||
fn includes(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool;
|
||||
fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool;
|
||||
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<MultiBufferOffset>;
|
||||
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
|
||||
}
|
||||
|
||||
impl AnchorRangeExt for Range<Anchor> {
|
||||
fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering {
|
||||
match self.start.cmp(&other.start, buffer) {
|
||||
Ordering::Equal => other.end.cmp(&self.end, buffer),
|
||||
ord => ord,
|
||||
}
|
||||
}
|
||||
|
||||
fn includes(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
|
||||
self.start.cmp(&other.start, buffer).is_le() && other.end.cmp(&self.end, buffer).is_le()
|
||||
}
|
||||
|
||||
fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
|
||||
self.end.cmp(&other.start, buffer).is_ge() && self.start.cmp(&other.end, buffer).is_le()
|
||||
}
|
||||
|
||||
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<MultiBufferOffset> {
|
||||
self.start.to_offset(content)..self.end.to_offset(content)
|
||||
}
|
||||
|
||||
fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point> {
|
||||
self.start.to_point(content)..self.end.to_point(content)
|
||||
}
|
||||
}
|
||||
8283
crates/multi_buffer/src/multi_buffer.rs
Normal file
8283
crates/multi_buffer/src/multi_buffer.rs
Normal file
File diff suppressed because it is too large
Load Diff
6259
crates/multi_buffer/src/multi_buffer_tests.rs
Normal file
6259
crates/multi_buffer/src/multi_buffer_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
694
crates/multi_buffer/src/path_key.rs
Normal file
694
crates/multi_buffer/src/path_key.rs
Normal file
@@ -0,0 +1,694 @@
|
||||
use std::{ops::Range, rc::Rc, sync::Arc};
|
||||
|
||||
use gpui::{App, AppContext, Context, Entity};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, BufferSnapshot};
|
||||
use rope::Point;
|
||||
use sum_tree::{Dimensions, SumTree};
|
||||
use text::{Bias, BufferId, Edit, OffsetRangeExt, Patch};
|
||||
use util::rel_path::RelPath;
|
||||
use ztracing::instrument;
|
||||
|
||||
use crate::{
|
||||
Anchor, BufferState, BufferStateSnapshot, DiffChangeKind, Event, Excerpt, ExcerptOffset,
|
||||
ExcerptRange, ExcerptSummary, ExpandExcerptDirection, MultiBuffer, MultiBufferOffset,
|
||||
PathKeyIndex, build_excerpt_ranges, remove_diff_state,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)]
|
||||
pub struct PathKey {
|
||||
// Used by the derived PartialOrd & Ord
|
||||
pub sort_prefix: Option<u64>,
|
||||
pub path: Arc<RelPath>,
|
||||
}
|
||||
|
||||
impl PathKey {
|
||||
pub fn min() -> Self {
|
||||
Self {
|
||||
sort_prefix: None,
|
||||
path: RelPath::empty().into_arc(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sorted(sort_prefix: u64) -> Self {
|
||||
Self {
|
||||
sort_prefix: Some(sort_prefix),
|
||||
path: RelPath::empty().into_arc(),
|
||||
}
|
||||
}
|
||||
pub fn with_sort_prefix(sort_prefix: u64, path: Arc<RelPath>) -> Self {
|
||||
Self {
|
||||
sort_prefix: Some(sort_prefix),
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Self {
|
||||
if let Some(file) = buffer.read(cx).file() {
|
||||
Self::with_sort_prefix(file.worktree_id(cx).to_proto(), file.path().clone())
|
||||
} else {
|
||||
Self {
|
||||
sort_prefix: None,
|
||||
path: RelPath::unix(&buffer.entity_id().to_string())
|
||||
.unwrap()
|
||||
.into_arc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiBuffer {
|
||||
pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option<Anchor> {
|
||||
let snapshot = self.snapshot(cx);
|
||||
let excerpt = snapshot.excerpts_for_path(path).next()?;
|
||||
let path_key_index = snapshot.path_key_index_for_buffer(excerpt.context.start.buffer_id)?;
|
||||
Some(Anchor::in_buffer(path_key_index, excerpt.context.start))
|
||||
}
|
||||
|
||||
pub fn set_excerpts_for_buffer(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
ranges: impl IntoIterator<Item = Range<Point>>,
|
||||
context_line_count: u32,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
self.set_excerpts_for_path(path, buffer, ranges, context_line_count, cx)
|
||||
}
|
||||
|
||||
/// Sets excerpts, returns `true` if at least one new excerpt was added.
|
||||
///
|
||||
/// Any existing excerpts for this buffer or this path will be replaced by the provided ranges.
|
||||
#[instrument(skip_all)]
|
||||
pub fn set_excerpts_for_path(
|
||||
&mut self,
|
||||
path: PathKey,
|
||||
buffer: Entity<Buffer>,
|
||||
ranges: impl IntoIterator<Item = Range<Point>>,
|
||||
context_line_count: u32,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let ranges: Vec<_> = ranges.into_iter().collect();
|
||||
let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot);
|
||||
|
||||
let merged = Self::merge_excerpt_ranges(&excerpt_ranges);
|
||||
let (inserted, _path_key_index) =
|
||||
self.set_merged_excerpt_ranges_for_path(path, buffer, &buffer_snapshot, merged, cx);
|
||||
inserted
|
||||
}
|
||||
|
||||
/// Like [`Self::set_excerpts_for_path`], but expands the provided ranges to cover any overlapping existing excerpts
|
||||
/// for the same buffer and path.
|
||||
///
|
||||
/// Existing excerpts that do not overlap any of the provided ranges are discarded.
|
||||
pub fn update_excerpts_for_path(
|
||||
&mut self,
|
||||
path: PathKey,
|
||||
buffer: Entity<Buffer>,
|
||||
ranges: impl IntoIterator<Item = Range<Point>>,
|
||||
context_line_count: u32,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let ranges: Vec<_> = ranges.into_iter().collect();
|
||||
let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot);
|
||||
let merged = self.merge_new_with_existing_excerpt_ranges(
|
||||
&path,
|
||||
&buffer_snapshot,
|
||||
excerpt_ranges,
|
||||
cx,
|
||||
);
|
||||
|
||||
let (inserted, _path_key_index) =
|
||||
self.set_merged_excerpt_ranges_for_path(path, buffer, &buffer_snapshot, merged, cx);
|
||||
inserted
|
||||
}
|
||||
|
||||
pub fn merge_new_with_existing_excerpt_ranges(
|
||||
&self,
|
||||
path: &PathKey,
|
||||
buffer_snapshot: &BufferSnapshot,
|
||||
mut excerpt_ranges: Vec<ExcerptRange<Point>>,
|
||||
cx: &App,
|
||||
) -> Vec<ExcerptRange<Point>> {
|
||||
let multibuffer_snapshot = self.snapshot(cx);
|
||||
|
||||
if multibuffer_snapshot.path_for_buffer(buffer_snapshot.remote_id()) == Some(path) {
|
||||
excerpt_ranges.sort_by_key(|range| range.context.start);
|
||||
let mut combined_ranges = Vec::new();
|
||||
let mut new_ranges = excerpt_ranges.into_iter().peekable();
|
||||
for existing_range in
|
||||
multibuffer_snapshot.excerpts_for_buffer(buffer_snapshot.remote_id())
|
||||
{
|
||||
let existing_range = ExcerptRange {
|
||||
context: existing_range.context.to_point(buffer_snapshot),
|
||||
primary: existing_range.primary.to_point(buffer_snapshot),
|
||||
};
|
||||
while let Some(new_range) = new_ranges.peek()
|
||||
&& new_range.context.end < existing_range.context.start
|
||||
{
|
||||
combined_ranges.push(new_range.clone());
|
||||
new_ranges.next();
|
||||
}
|
||||
|
||||
if let Some(new_range) = new_ranges.peek()
|
||||
&& new_range.context.start <= existing_range.context.end
|
||||
{
|
||||
combined_ranges.push(existing_range)
|
||||
}
|
||||
}
|
||||
combined_ranges.extend(new_ranges);
|
||||
excerpt_ranges = combined_ranges;
|
||||
}
|
||||
|
||||
excerpt_ranges.sort_by_key(|range| range.context.start);
|
||||
Self::merge_excerpt_ranges(&excerpt_ranges)
|
||||
}
|
||||
|
||||
pub fn set_excerpt_ranges_for_path(
|
||||
&mut self,
|
||||
path: PathKey,
|
||||
buffer: Entity<Buffer>,
|
||||
buffer_snapshot: &BufferSnapshot,
|
||||
excerpt_ranges: Vec<ExcerptRange<Point>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let merged = Self::merge_excerpt_ranges(&excerpt_ranges);
|
||||
let (inserted, _path_key_index) =
|
||||
self.set_merged_excerpt_ranges_for_path(path, buffer, buffer_snapshot, merged, cx);
|
||||
inserted
|
||||
}
|
||||
|
||||
pub fn set_anchored_excerpts_for_path(
|
||||
&self,
|
||||
path_key: PathKey,
|
||||
buffer: Entity<Buffer>,
|
||||
ranges: Vec<Range<text::Anchor>>,
|
||||
context_line_count: u32,
|
||||
cx: &Context<Self>,
|
||||
) -> impl Future<Output = Vec<Range<Anchor>>> + use<> {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let multi_buffer = cx.weak_entity();
|
||||
let mut app = cx.to_async();
|
||||
async move {
|
||||
let snapshot = buffer_snapshot.clone();
|
||||
let (ranges, merged_excerpt_ranges) = app
|
||||
.background_spawn(async move {
|
||||
let point_ranges = ranges.iter().map(|range| range.to_point(&snapshot));
|
||||
let excerpt_ranges =
|
||||
build_excerpt_ranges(point_ranges, context_line_count, &snapshot);
|
||||
let merged = Self::merge_excerpt_ranges(&excerpt_ranges);
|
||||
(ranges, merged)
|
||||
})
|
||||
.await;
|
||||
|
||||
multi_buffer
|
||||
.update(&mut app, move |multi_buffer, cx| {
|
||||
let (_, path_key_index) = multi_buffer.set_merged_excerpt_ranges_for_path(
|
||||
path_key,
|
||||
buffer,
|
||||
&buffer_snapshot,
|
||||
merged_excerpt_ranges,
|
||||
cx,
|
||||
);
|
||||
ranges
|
||||
.into_iter()
|
||||
.map(|range| Anchor::range_in_buffer(path_key_index, range))
|
||||
.collect()
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_excerpts(
|
||||
&mut self,
|
||||
anchors: impl IntoIterator<Item = Anchor>,
|
||||
line_count: u32,
|
||||
direction: ExpandExcerptDirection,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if line_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot(cx);
|
||||
let mut sorted_anchors = anchors
|
||||
.into_iter()
|
||||
.filter_map(|anchor| anchor.excerpt_anchor())
|
||||
.collect::<Vec<_>>();
|
||||
if sorted_anchors.is_empty() {
|
||||
return;
|
||||
}
|
||||
sorted_anchors.sort_by(|a, b| a.cmp(b, &snapshot));
|
||||
let buffers = sorted_anchors.into_iter().chunk_by(|anchor| anchor.path);
|
||||
let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>(());
|
||||
|
||||
for (path_index, excerpt_anchors) in &buffers {
|
||||
let path = snapshot
|
||||
.path_keys
|
||||
.get_index(path_index.0 as usize)
|
||||
.expect("anchor from wrong multibuffer");
|
||||
|
||||
let mut excerpt_anchors = excerpt_anchors.peekable();
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
cursor.seek_forward(path, Bias::Left);
|
||||
let Some((buffer, buffer_snapshot)) = cursor
|
||||
.item()
|
||||
.map(|excerpt| (excerpt.buffer(&self), excerpt.buffer_snapshot(&snapshot)))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
while let Some(excerpt) = cursor.item()
|
||||
&& &excerpt.path_key == path
|
||||
{
|
||||
let mut range = ExcerptRange {
|
||||
context: excerpt.range.context.to_point(buffer_snapshot),
|
||||
primary: excerpt.range.primary.to_point(buffer_snapshot),
|
||||
};
|
||||
|
||||
let mut needs_expand = false;
|
||||
while excerpt_anchors.peek().is_some_and(|anchor| {
|
||||
excerpt
|
||||
.range
|
||||
.contains(&anchor.text_anchor(), buffer_snapshot)
|
||||
}) {
|
||||
needs_expand = true;
|
||||
excerpt_anchors.next();
|
||||
}
|
||||
|
||||
if needs_expand {
|
||||
match direction {
|
||||
ExpandExcerptDirection::Up => {
|
||||
range.context.start.row =
|
||||
range.context.start.row.saturating_sub(line_count);
|
||||
range.context.start.column = 0;
|
||||
}
|
||||
ExpandExcerptDirection::Down => {
|
||||
range.context.end.row = (range.context.end.row + line_count)
|
||||
.min(excerpt.buffer_snapshot(&snapshot).max_point().row);
|
||||
range.context.end.column = excerpt
|
||||
.buffer_snapshot(&snapshot)
|
||||
.line_len(range.context.end.row);
|
||||
}
|
||||
ExpandExcerptDirection::UpAndDown => {
|
||||
range.context.start.row =
|
||||
range.context.start.row.saturating_sub(line_count);
|
||||
range.context.start.column = 0;
|
||||
range.context.end.row = (range.context.end.row + line_count)
|
||||
.min(excerpt.buffer_snapshot(&snapshot).max_point().row);
|
||||
range.context.end.column = excerpt
|
||||
.buffer_snapshot(&snapshot)
|
||||
.line_len(range.context.end.row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ranges.push(range);
|
||||
cursor.next();
|
||||
}
|
||||
|
||||
ranges.sort_by_key(|r| r.context.start);
|
||||
|
||||
self.set_excerpt_ranges_for_path(path.clone(), buffer, buffer_snapshot, ranges, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets excerpts, returns `true` if at least one new excerpt was added.
|
||||
pub(crate) fn set_merged_excerpt_ranges_for_path<T>(
|
||||
&mut self,
|
||||
path: PathKey,
|
||||
buffer: Entity<Buffer>,
|
||||
buffer_snapshot: &BufferSnapshot,
|
||||
new: Vec<ExcerptRange<T>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> (bool, PathKeyIndex)
|
||||
where
|
||||
T: language::ToOffset,
|
||||
{
|
||||
let anchor_ranges = new
|
||||
.into_iter()
|
||||
.map(|r| ExcerptRange {
|
||||
context: buffer_snapshot.anchor_before(r.context.start)
|
||||
..buffer_snapshot.anchor_after(r.context.end),
|
||||
primary: buffer_snapshot.anchor_before(r.primary.start)
|
||||
..buffer_snapshot.anchor_after(r.primary.end),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let inserted =
|
||||
self.update_path_excerpts(path.clone(), buffer, buffer_snapshot, &anchor_ranges, cx);
|
||||
let path_key_index = self.get_or_create_path_key_index(&path);
|
||||
(inserted, path_key_index)
|
||||
}
|
||||
|
||||
pub(crate) fn get_or_create_path_key_index(&mut self, path_key: &PathKey) -> PathKeyIndex {
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
|
||||
if let Some(existing) = snapshot.path_keys.get_index_of(path_key) {
|
||||
return PathKeyIndex(existing as u64);
|
||||
}
|
||||
|
||||
PathKeyIndex(
|
||||
Arc::make_mut(&mut snapshot.path_keys)
|
||||
.insert_full(path_key.clone())
|
||||
.0 as u64,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_path_excerpts(
|
||||
&mut self,
|
||||
path_key: PathKey,
|
||||
buffer: Entity<Buffer>,
|
||||
buffer_snapshot: &BufferSnapshot,
|
||||
to_insert: &Vec<ExcerptRange<text::Anchor>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let path_key_index = self.get_or_create_path_key_index(&path_key);
|
||||
if let Some(old_path_key) = self
|
||||
.snapshot(cx)
|
||||
.path_for_buffer(buffer_snapshot.remote_id())
|
||||
&& old_path_key != &path_key
|
||||
{
|
||||
self.remove_excerpts(old_path_key.clone(), cx);
|
||||
}
|
||||
|
||||
if to_insert.len() == 0 {
|
||||
self.remove_excerpts(path_key.clone(), cx);
|
||||
|
||||
return false;
|
||||
}
|
||||
assert_eq!(self.history.transaction_depth(), 0);
|
||||
self.sync_mut(cx);
|
||||
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
|
||||
let mut snapshot = self.snapshot.get_mut();
|
||||
let mut cursor = snapshot
|
||||
.excerpts
|
||||
.cursor::<Dimensions<PathKey, ExcerptOffset>>(());
|
||||
let mut new_excerpts = SumTree::new(());
|
||||
|
||||
let new_ranges = to_insert.clone();
|
||||
let mut to_insert = to_insert.iter().peekable();
|
||||
let mut patch = Patch::empty();
|
||||
let mut added_new_excerpt = false;
|
||||
|
||||
new_excerpts.append(cursor.slice(&path_key, Bias::Left), ());
|
||||
|
||||
// handle the case where the path key used to be associated
|
||||
// with a different buffer by removing its excerpts.
|
||||
if let Some(excerpt) = cursor.item()
|
||||
&& &excerpt.path_key == &path_key
|
||||
&& excerpt.buffer_id != buffer_id
|
||||
{
|
||||
let old_buffer_id = excerpt.buffer_id;
|
||||
self.buffers.remove(&old_buffer_id);
|
||||
snapshot.buffers.remove(&old_buffer_id);
|
||||
remove_diff_state(&mut snapshot.diffs, old_buffer_id);
|
||||
self.diffs.remove(&old_buffer_id);
|
||||
let before = cursor.position.1;
|
||||
cursor.seek_forward(&path_key, Bias::Right);
|
||||
let after = cursor.position.1;
|
||||
patch.push(Edit {
|
||||
old: before..after,
|
||||
new: new_excerpts.summary().len()..new_excerpts.summary().len(),
|
||||
});
|
||||
cx.emit(Event::BuffersRemoved {
|
||||
removed_buffer_ids: vec![old_buffer_id],
|
||||
});
|
||||
}
|
||||
|
||||
while let Some(excerpt) = cursor.item()
|
||||
&& excerpt.path_key == path_key
|
||||
{
|
||||
assert_eq!(excerpt.buffer_id, buffer_id);
|
||||
let Some(next_excerpt) = to_insert.peek() else {
|
||||
break;
|
||||
};
|
||||
if &excerpt.range == *next_excerpt {
|
||||
let before = new_excerpts.summary().len();
|
||||
new_excerpts.update_last(
|
||||
|prev_excerpt| {
|
||||
if !prev_excerpt.has_trailing_newline {
|
||||
prev_excerpt.has_trailing_newline = true;
|
||||
patch.push(Edit {
|
||||
old: cursor.position.1..cursor.position.1,
|
||||
new: before..before + MultiBufferOffset(1),
|
||||
});
|
||||
}
|
||||
},
|
||||
(),
|
||||
);
|
||||
new_excerpts.push(excerpt.clone(), ());
|
||||
to_insert.next();
|
||||
cursor.next();
|
||||
continue;
|
||||
}
|
||||
|
||||
if excerpt
|
||||
.range
|
||||
.context
|
||||
.start
|
||||
.cmp(&next_excerpt.context.start, &buffer_snapshot)
|
||||
.is_le()
|
||||
{
|
||||
// remove old excerpt
|
||||
let before = cursor.position.1;
|
||||
cursor.next();
|
||||
let after = cursor.position.1;
|
||||
patch.push(Edit {
|
||||
old: before..after,
|
||||
new: new_excerpts.summary().len()..new_excerpts.summary().len(),
|
||||
});
|
||||
} else {
|
||||
// insert new excerpt
|
||||
let next_excerpt = to_insert.next().unwrap();
|
||||
added_new_excerpt = true;
|
||||
let before = new_excerpts.summary().len();
|
||||
new_excerpts.update_last(
|
||||
|prev_excerpt| {
|
||||
prev_excerpt.has_trailing_newline = true;
|
||||
},
|
||||
(),
|
||||
);
|
||||
new_excerpts.push(
|
||||
Excerpt::new(
|
||||
path_key.clone(),
|
||||
path_key_index,
|
||||
&buffer_snapshot,
|
||||
next_excerpt.clone(),
|
||||
false,
|
||||
),
|
||||
(),
|
||||
);
|
||||
let after = new_excerpts.summary().len();
|
||||
patch.push_maybe_empty(Edit {
|
||||
old: cursor.position.1..cursor.position.1,
|
||||
new: before..after,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// remove any further trailing excerpts
|
||||
let mut before = cursor.position.1;
|
||||
cursor.seek_forward(&path_key, Bias::Right);
|
||||
let after = cursor.position.1;
|
||||
// if we removed the previous last excerpt, remove the trailing newline from the new last excerpt
|
||||
if cursor.item().is_none() && to_insert.peek().is_none() {
|
||||
new_excerpts.update_last(
|
||||
|excerpt| {
|
||||
if excerpt.has_trailing_newline {
|
||||
before.0.0 = before
|
||||
.0
|
||||
.0
|
||||
.checked_sub(1)
|
||||
.expect("should have preceding excerpt");
|
||||
excerpt.has_trailing_newline = false;
|
||||
}
|
||||
},
|
||||
(),
|
||||
);
|
||||
}
|
||||
patch.push(Edit {
|
||||
old: before..after,
|
||||
new: new_excerpts.summary().len()..new_excerpts.summary().len(),
|
||||
});
|
||||
|
||||
while let Some(next_excerpt) = to_insert.next() {
|
||||
added_new_excerpt = true;
|
||||
let before = new_excerpts.summary().len();
|
||||
new_excerpts.update_last(
|
||||
|prev_excerpt| {
|
||||
prev_excerpt.has_trailing_newline = true;
|
||||
},
|
||||
(),
|
||||
);
|
||||
new_excerpts.push(
|
||||
Excerpt::new(
|
||||
path_key.clone(),
|
||||
path_key_index,
|
||||
&buffer_snapshot,
|
||||
next_excerpt.clone(),
|
||||
false,
|
||||
),
|
||||
(),
|
||||
);
|
||||
let after = new_excerpts.summary().len();
|
||||
patch.push_maybe_empty(Edit {
|
||||
old: cursor.position.1..cursor.position.1,
|
||||
new: before..after,
|
||||
});
|
||||
}
|
||||
|
||||
let suffix_start = cursor.position.1;
|
||||
let suffix = cursor.suffix();
|
||||
let changed_trailing_excerpt = suffix.is_empty();
|
||||
if !suffix.is_empty() {
|
||||
let before = new_excerpts.summary().len();
|
||||
new_excerpts.update_last(
|
||||
|prev_excerpt| {
|
||||
if !prev_excerpt.has_trailing_newline {
|
||||
prev_excerpt.has_trailing_newline = true;
|
||||
patch.push(Edit {
|
||||
old: suffix_start..suffix_start,
|
||||
new: before..before + MultiBufferOffset(1),
|
||||
});
|
||||
}
|
||||
},
|
||||
(),
|
||||
);
|
||||
}
|
||||
new_excerpts.append(suffix, ());
|
||||
drop(cursor);
|
||||
|
||||
snapshot.excerpts = new_excerpts;
|
||||
snapshot.buffers.insert(
|
||||
buffer_id,
|
||||
BufferStateSnapshot {
|
||||
path_key: path_key.clone(),
|
||||
path_key_index,
|
||||
buffer_snapshot: buffer_snapshot.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
self.buffers.entry(buffer_id).or_insert_with(|| {
|
||||
self.buffer_changed_since_sync.replace(true);
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.record_changes(Rc::downgrade(&self.buffer_changed_since_sync));
|
||||
});
|
||||
BufferState {
|
||||
_subscriptions: [
|
||||
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
],
|
||||
buffer: buffer.clone(),
|
||||
}
|
||||
});
|
||||
|
||||
if changed_trailing_excerpt {
|
||||
snapshot.trailing_excerpt_update_count += 1;
|
||||
}
|
||||
|
||||
let edits = Self::sync_diff_transforms(
|
||||
&mut snapshot,
|
||||
patch.into_inner(),
|
||||
DiffChangeKind::BufferEdited,
|
||||
);
|
||||
if !edits.is_empty() {
|
||||
self.subscriptions.publish(edits);
|
||||
}
|
||||
|
||||
cx.emit(Event::Edited {
|
||||
edited_buffer: None,
|
||||
is_local: true,
|
||||
});
|
||||
cx.emit(Event::BufferRangesUpdated {
|
||||
buffer,
|
||||
path_key: path_key.clone(),
|
||||
ranges: new_ranges,
|
||||
});
|
||||
cx.notify();
|
||||
|
||||
added_new_excerpt
|
||||
}
|
||||
|
||||
pub fn remove_excerpts_for_buffer(&mut self, buffer: BufferId, cx: &mut Context<Self>) {
|
||||
let snapshot = self.sync_mut(cx);
|
||||
let Some(path) = snapshot.path_for_buffer(buffer).cloned() else {
|
||||
return;
|
||||
};
|
||||
self.remove_excerpts(path, cx);
|
||||
}
|
||||
|
||||
pub fn remove_excerpts(&mut self, path: PathKey, cx: &mut Context<Self>) {
|
||||
assert_eq!(self.history.transaction_depth(), 0);
|
||||
self.sync_mut(cx);
|
||||
|
||||
let mut snapshot = self.snapshot.get_mut();
|
||||
let mut cursor = snapshot
|
||||
.excerpts
|
||||
.cursor::<Dimensions<PathKey, ExcerptOffset>>(());
|
||||
let mut new_excerpts = SumTree::new(());
|
||||
new_excerpts.append(cursor.slice(&path, Bias::Left), ());
|
||||
let mut edit_start = cursor.position.1;
|
||||
let mut buffer_id = None;
|
||||
if let Some(excerpt) = cursor.item()
|
||||
&& excerpt.path_key == path
|
||||
{
|
||||
buffer_id = Some(excerpt.buffer_id);
|
||||
}
|
||||
cursor.seek(&path, Bias::Right);
|
||||
let edit_end = cursor.position.1;
|
||||
let suffix = cursor.suffix();
|
||||
let changed_trailing_excerpt = suffix.is_empty();
|
||||
new_excerpts.append(suffix, ());
|
||||
|
||||
if let Some(buffer_id) = buffer_id {
|
||||
snapshot.buffers.remove(&buffer_id);
|
||||
remove_diff_state(&mut snapshot.diffs, buffer_id);
|
||||
self.buffers.remove(&buffer_id);
|
||||
self.diffs.remove(&buffer_id);
|
||||
cx.emit(Event::BuffersRemoved {
|
||||
removed_buffer_ids: vec![buffer_id],
|
||||
})
|
||||
}
|
||||
drop(cursor);
|
||||
if changed_trailing_excerpt {
|
||||
snapshot.trailing_excerpt_update_count += 1;
|
||||
new_excerpts.update_last(
|
||||
|excerpt| {
|
||||
if excerpt.has_trailing_newline {
|
||||
excerpt.has_trailing_newline = false;
|
||||
edit_start.0.0 = edit_start
|
||||
.0
|
||||
.0
|
||||
.checked_sub(1)
|
||||
.expect("should have at least one excerpt");
|
||||
}
|
||||
},
|
||||
(),
|
||||
)
|
||||
}
|
||||
|
||||
let edit = Edit {
|
||||
old: edit_start..edit_end,
|
||||
new: edit_start..edit_start,
|
||||
};
|
||||
snapshot.excerpts = new_excerpts;
|
||||
|
||||
let edits =
|
||||
Self::sync_diff_transforms(&mut snapshot, vec![edit], DiffChangeKind::BufferEdited);
|
||||
if !edits.is_empty() {
|
||||
self.subscriptions.publish(edits);
|
||||
}
|
||||
|
||||
cx.emit(Event::Edited {
|
||||
edited_buffer: None,
|
||||
is_local: true,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
517
crates/multi_buffer/src/transaction.rs
Normal file
517
crates/multi_buffer/src/transaction.rs
Normal file
@@ -0,0 +1,517 @@
|
||||
use gpui::{App, Context, Entity};
|
||||
use language::{self, Buffer, TransactionId};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ops::Range,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
use text::BufferId;
|
||||
|
||||
use crate::{Anchor, BufferState, MultiBufferOffset};
|
||||
|
||||
use super::{Event, MultiBuffer};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct History {
|
||||
next_transaction_id: TransactionId,
|
||||
undo_stack: Vec<Transaction>,
|
||||
redo_stack: Vec<Transaction>,
|
||||
transaction_depth: usize,
|
||||
group_interval: Duration,
|
||||
}
|
||||
|
||||
impl Default for History {
|
||||
fn default() -> Self {
|
||||
History {
|
||||
next_transaction_id: clock::Lamport::MIN,
|
||||
undo_stack: Vec::new(),
|
||||
redo_stack: Vec::new(),
|
||||
transaction_depth: 0,
|
||||
group_interval: Duration::from_millis(300),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Transaction {
|
||||
id: TransactionId,
|
||||
buffer_transactions: HashMap<BufferId, text::TransactionId>,
|
||||
first_edit_at: Instant,
|
||||
last_edit_at: Instant,
|
||||
suppress_grouping: bool,
|
||||
}
|
||||
|
||||
impl History {
|
||||
fn start_transaction(&mut self, now: Instant) -> Option<TransactionId> {
|
||||
self.transaction_depth += 1;
|
||||
if self.transaction_depth == 1 {
|
||||
let id = self.next_transaction_id.tick();
|
||||
self.undo_stack.push(Transaction {
|
||||
id,
|
||||
buffer_transactions: Default::default(),
|
||||
first_edit_at: now,
|
||||
last_edit_at: now,
|
||||
suppress_grouping: false,
|
||||
});
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn end_transaction(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
buffer_transactions: HashMap<BufferId, text::TransactionId>,
|
||||
) -> bool {
|
||||
assert_ne!(self.transaction_depth, 0);
|
||||
self.transaction_depth -= 1;
|
||||
if self.transaction_depth == 0 {
|
||||
if buffer_transactions.is_empty() {
|
||||
self.undo_stack.pop();
|
||||
false
|
||||
} else {
|
||||
self.redo_stack.clear();
|
||||
let transaction = self.undo_stack.last_mut().unwrap();
|
||||
transaction.last_edit_at = now;
|
||||
for (buffer_id, transaction_id) in buffer_transactions {
|
||||
transaction
|
||||
.buffer_transactions
|
||||
.entry(buffer_id)
|
||||
.or_insert(transaction_id);
|
||||
}
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn push_transaction<'a, T>(
|
||||
&mut self,
|
||||
buffer_transactions: T,
|
||||
now: Instant,
|
||||
cx: &Context<MultiBuffer>,
|
||||
) where
|
||||
T: IntoIterator<Item = (&'a Entity<Buffer>, &'a language::Transaction)>,
|
||||
{
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
let transaction = Transaction {
|
||||
id: self.next_transaction_id.tick(),
|
||||
buffer_transactions: buffer_transactions
|
||||
.into_iter()
|
||||
.map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id))
|
||||
.collect(),
|
||||
first_edit_at: now,
|
||||
last_edit_at: now,
|
||||
suppress_grouping: false,
|
||||
};
|
||||
if !transaction.buffer_transactions.is_empty() {
|
||||
self.undo_stack.push(transaction);
|
||||
self.redo_stack.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_last_transaction(&mut self) {
|
||||
if let Some(transaction) = self.undo_stack.last_mut() {
|
||||
transaction.suppress_grouping = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
|
||||
if let Some(ix) = self
|
||||
.undo_stack
|
||||
.iter()
|
||||
.rposition(|transaction| transaction.id == transaction_id)
|
||||
{
|
||||
Some(self.undo_stack.remove(ix))
|
||||
} else if let Some(ix) = self
|
||||
.redo_stack
|
||||
.iter()
|
||||
.rposition(|transaction| transaction.id == transaction_id)
|
||||
{
|
||||
Some(self.redo_stack.remove(ix))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> {
|
||||
self.undo_stack
|
||||
.iter()
|
||||
.find(|transaction| transaction.id == transaction_id)
|
||||
.or_else(|| {
|
||||
self.redo_stack
|
||||
.iter()
|
||||
.find(|transaction| transaction.id == transaction_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
|
||||
self.undo_stack
|
||||
.iter_mut()
|
||||
.find(|transaction| transaction.id == transaction_id)
|
||||
.or_else(|| {
|
||||
self.redo_stack
|
||||
.iter_mut()
|
||||
.find(|transaction| transaction.id == transaction_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn pop_undo(&mut self) -> Option<&mut Transaction> {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
if let Some(transaction) = self.undo_stack.pop() {
|
||||
self.redo_stack.push(transaction);
|
||||
self.redo_stack.last_mut()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_redo(&mut self) -> Option<&mut Transaction> {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
if let Some(transaction) = self.redo_stack.pop() {
|
||||
self.undo_stack.push(transaction);
|
||||
self.undo_stack.last_mut()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
|
||||
let ix = self
|
||||
.undo_stack
|
||||
.iter()
|
||||
.rposition(|transaction| transaction.id == transaction_id)?;
|
||||
let transaction = self.undo_stack.remove(ix);
|
||||
self.redo_stack.push(transaction);
|
||||
self.redo_stack.last()
|
||||
}
|
||||
|
||||
fn group(&mut self) -> Option<TransactionId> {
|
||||
let mut count = 0;
|
||||
let mut transactions = self.undo_stack.iter();
|
||||
if let Some(mut transaction) = transactions.next_back() {
|
||||
while let Some(prev_transaction) = transactions.next_back() {
|
||||
if !prev_transaction.suppress_grouping
|
||||
&& transaction.first_edit_at - prev_transaction.last_edit_at
|
||||
<= self.group_interval
|
||||
{
|
||||
transaction = prev_transaction;
|
||||
count += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.group_trailing(count)
|
||||
}
|
||||
|
||||
fn group_until(&mut self, transaction_id: TransactionId) {
|
||||
let mut count = 0;
|
||||
for transaction in self.undo_stack.iter().rev() {
|
||||
if transaction.id == transaction_id {
|
||||
self.group_trailing(count);
|
||||
break;
|
||||
} else if transaction.suppress_grouping {
|
||||
break;
|
||||
} else {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn group_trailing(&mut self, n: usize) -> Option<TransactionId> {
|
||||
let new_len = self.undo_stack.len() - n;
|
||||
let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len);
|
||||
if let Some(last_transaction) = transactions_to_keep.last_mut() {
|
||||
if let Some(transaction) = transactions_to_merge.last() {
|
||||
last_transaction.last_edit_at = transaction.last_edit_at;
|
||||
}
|
||||
for to_merge in transactions_to_merge {
|
||||
for (buffer_id, transaction_id) in &to_merge.buffer_transactions {
|
||||
last_transaction
|
||||
.buffer_transactions
|
||||
.entry(*buffer_id)
|
||||
.or_insert(*transaction_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.undo_stack.truncate(new_len);
|
||||
self.undo_stack.last().map(|t| t.id)
|
||||
}
|
||||
|
||||
pub(super) fn transaction_depth(&self) -> usize {
|
||||
self.transaction_depth
|
||||
}
|
||||
|
||||
pub fn set_group_interval(&mut self, group_interval: Duration) {
|
||||
self.group_interval = group_interval;
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiBuffer {
|
||||
pub fn start_transaction(&mut self, cx: &mut Context<Self>) -> Option<TransactionId> {
|
||||
self.start_transaction_at(Instant::now(), cx)
|
||||
}
|
||||
|
||||
pub fn start_transaction_at(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<TransactionId> {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now));
|
||||
}
|
||||
|
||||
for BufferState { buffer, .. } in self.buffers.values() {
|
||||
buffer.update(cx, |buffer, _| buffer.start_transaction_at(now));
|
||||
}
|
||||
self.history.start_transaction(now)
|
||||
}
|
||||
|
||||
pub fn last_transaction_id(&self, cx: &App) -> Option<TransactionId> {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
buffer
|
||||
.read(cx)
|
||||
.peek_undo_stack()
|
||||
.map(|history_entry| history_entry.transaction_id())
|
||||
} else {
|
||||
let last_transaction = self.history.undo_stack.last()?;
|
||||
Some(last_transaction.id)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_transaction(&mut self, cx: &mut Context<Self>) -> Option<TransactionId> {
|
||||
self.end_transaction_at(Instant::now(), cx)
|
||||
}
|
||||
|
||||
pub fn end_transaction_at(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<TransactionId> {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
return buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx));
|
||||
}
|
||||
|
||||
let mut buffer_transactions = HashMap::default();
|
||||
for BufferState { buffer, .. } in self.buffers.values() {
|
||||
if let Some(transaction_id) =
|
||||
buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
|
||||
{
|
||||
buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id);
|
||||
}
|
||||
}
|
||||
|
||||
if self.history.end_transaction(now, buffer_transactions) {
|
||||
let transaction_id = self.history.group().unwrap();
|
||||
Some(transaction_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edited_ranges_for_transaction(
|
||||
&self,
|
||||
transaction_id: TransactionId,
|
||||
cx: &App,
|
||||
) -> Vec<Range<MultiBufferOffset>> {
|
||||
let Some(transaction) = self.history.transaction(transaction_id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let snapshot = self.read(cx);
|
||||
let mut buffer_anchors = Vec::new();
|
||||
|
||||
for (buffer_id, buffer_transaction) in &transaction.buffer_transactions {
|
||||
let Some(buffer) = self.buffer(*buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(excerpt) = snapshot.first_excerpt_for_buffer(*buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
for range in buffer
|
||||
.read(cx)
|
||||
.edited_ranges_for_transaction_id::<usize>(*buffer_transaction)
|
||||
{
|
||||
buffer_anchors.push(Anchor::in_buffer(
|
||||
excerpt.path_key_index,
|
||||
buffer_snapshot.anchor_at(range.start, Bias::Left),
|
||||
));
|
||||
buffer_anchors.push(Anchor::in_buffer(
|
||||
excerpt.path_key_index,
|
||||
buffer_snapshot.anchor_at(range.end, Bias::Right),
|
||||
));
|
||||
}
|
||||
}
|
||||
buffer_anchors.sort_unstable_by(|a, b| a.cmp(b, &snapshot));
|
||||
|
||||
snapshot
|
||||
.summaries_for_anchors(buffer_anchors.iter())
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|&[s, e]| s..e)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn merge_transactions(
|
||||
&mut self,
|
||||
transaction: TransactionId,
|
||||
destination: TransactionId,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.merge_transactions(transaction, destination)
|
||||
});
|
||||
} else if let Some(transaction) = self.history.forget(transaction)
|
||||
&& let Some(destination) = self.history.transaction_mut(destination)
|
||||
{
|
||||
for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions {
|
||||
if let Some(destination_buffer_transaction_id) =
|
||||
destination.buffer_transactions.get(&buffer_id)
|
||||
{
|
||||
if let Some(state) = self.buffers.get(&buffer_id) {
|
||||
state.buffer.update(cx, |buffer, _| {
|
||||
buffer.merge_transactions(
|
||||
buffer_transaction_id,
|
||||
*destination_buffer_transaction_id,
|
||||
)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
destination
|
||||
.buffer_transactions
|
||||
.insert(buffer_id, buffer_transaction_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finalize_last_transaction(&mut self, cx: &mut Context<Self>) {
|
||||
self.history.finalize_last_transaction();
|
||||
for BufferState { buffer, .. } in self.buffers.values() {
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.finalize_last_transaction();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &Context<Self>)
|
||||
where
|
||||
T: IntoIterator<Item = (&'a Entity<Buffer>, &'a language::Transaction)>,
|
||||
{
|
||||
self.history
|
||||
.push_transaction(buffer_transactions, Instant::now(), cx);
|
||||
self.history.finalize_last_transaction();
|
||||
}
|
||||
|
||||
pub fn group_until_transaction(
|
||||
&mut self,
|
||||
transaction_id: TransactionId,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.group_until_transaction(transaction_id)
|
||||
});
|
||||
} else {
|
||||
self.history.group_until(transaction_id);
|
||||
}
|
||||
}
|
||||
pub fn undo(&mut self, cx: &mut Context<Self>) -> Option<TransactionId> {
|
||||
let mut transaction_id = None;
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
} else {
|
||||
while let Some(transaction) = self.history.pop_undo() {
|
||||
let mut undone = false;
|
||||
for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions {
|
||||
if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) {
|
||||
undone |= buffer.update(cx, |buffer, cx| {
|
||||
let undo_to = *buffer_transaction_id;
|
||||
if let Some(entry) = buffer.peek_undo_stack() {
|
||||
*buffer_transaction_id = entry.transaction_id();
|
||||
}
|
||||
buffer.undo_to_transaction(undo_to, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if undone {
|
||||
transaction_id = Some(transaction.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(transaction_id) = transaction_id {
|
||||
cx.emit(Event::TransactionUndone { transaction_id });
|
||||
}
|
||||
|
||||
transaction_id
|
||||
}
|
||||
|
||||
pub fn redo(&mut self, cx: &mut Context<Self>) -> Option<TransactionId> {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
return buffer.update(cx, |buffer, cx| buffer.redo(cx));
|
||||
}
|
||||
|
||||
while let Some(transaction) = self.history.pop_redo() {
|
||||
let mut redone = false;
|
||||
for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions.iter_mut() {
|
||||
if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) {
|
||||
redone |= buffer.update(cx, |buffer, cx| {
|
||||
let redo_to = *buffer_transaction_id;
|
||||
if let Some(entry) = buffer.peek_redo_stack() {
|
||||
*buffer_transaction_id = entry.transaction_id();
|
||||
}
|
||||
buffer.redo_to_transaction(redo_to, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if redone {
|
||||
return Some(transaction.id);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context<Self>) {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
} else if let Some(transaction) = self.history.remove_from_undo(transaction_id) {
|
||||
for (buffer_id, transaction_id) in &transaction.buffer_transactions {
|
||||
if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.undo_transaction(*transaction_id, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forget_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context<Self>) {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.forget_transaction(transaction_id);
|
||||
});
|
||||
} else if let Some(transaction) = self.history.forget(transaction_id) {
|
||||
for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions {
|
||||
if let Some(state) = self.buffers.get_mut(&buffer_id) {
|
||||
state.buffer.update(cx, |buffer, _| {
|
||||
buffer.forget_transaction(buffer_transaction_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user