TODO:
- [x] merge main
- [x] nonshrinking `set_excerpts_for_path`
- [x] Test-drive potential problem areas in the app
- [x] prepare cloud side
- [x] test collaboration
- [ ] docstrings
- [ ] ???
## Context
### Background
Currently, a multibuffer consists of an arbitrary list of
anchor-delimited excerpts from individual buffers. Excerpt ranges for a
fixed buffer are permitted to overlap, and can appear in any order in
the multibuffer, possibly separated by excerpts from other buffers.
However, in practice all code that constructs multibuffers does so using
the APIs defined in the `path_key` submodule of the `multi_buffer` crate
(`set_excerpts_for_path` etc.) If you only use these APIs, the resulting
multibuffer will maintain the following invariants:
- All excerpts for the same buffer appear contiguously in the
multibuffer
- Excerpts for the same buffer cannot overlap
- Excerpts for the same buffer appear in order
- The placement of the excerpts for a specific buffer in the multibuffer
are determined by the `PathKey` passed to `set_excerpts_for_path`. There
is exactly one `PathKey` per buffer in the multibuffer
### Purpose of this PR
This PR changes the multibuffer so that the invariants maintained by the
`path_key` APIs *always* hold. It's no longer possible to construct a
multibuffer with overlapping excerpts, etc. The APIs that permitted
this, like `insert_excerpts_with_ids_after`, have been removed in favor
of the `path_key` suite.
The main upshot of this is that given a `text::Anchor` and a
multibuffer, it's possible to efficiently figure out the unique excerpt
that includes that anchor, if any:
```
impl MultiBufferSnapshot {
fn buffer_anchor_to_anchor(&self, anchor: text::Anchor) -> Option<multi_buffer::Anchor>;
}
```
And in the other direction, given a `multi_buffer::Anchor`, we can look
at its `text::Anchor` to locate the excerpt that contains it. That means
we don't need an `ExcerptId` to create or resolve
`multi_buffer::Anchor`, and in fact we can delete `ExcerptId` entirely,
so that excerpts no longer have any identity outside their
`Range<text::Anchor>`.
There are a large number of changes to `editor` and other downstream
crates as a result of removing `ExcerptId` and multibuffer APIs that
assumed it.
### Other changes
There are some other improvements that are not immediate consequences of
that big change, but helped make it smoother. Notably:
- The `buffer_id` field of `text::Anchor` is no longer optional.
`text::Anchor::{MIN, MAX}` have been removed in favor of
`min_for_buffer`, etc.
- `multi_buffer::Anchor` is now a three-variant enum (inlined slightly):
```
enum Anchor {
Min,
Excerpt {
text_anchor: text::Anchor,
path_key_index: PathKeyIndex,
diff_base_anchor: Option<text::Anchor>,
},
Max,
}
```
That means it's no longer possible to unconditionally access the
`text_anchor` field, which is good because most of the places that were
doing that were buggy for min/max! Instead, we have a new API that
correctly resolves min/max to the start of the first excerpt or the end
of the last excerpt:
```
impl MultiBufferSnapshot {
fn anchor_to_buffer_anchor(&self, anchor: multi_buffer::Anchor) -> Option<text::Anchor>;
}
```
- `MultiBufferExcerpt` has been removed in favor of a new
`map_excerpt_ranges` API directly on `MultiBufferSnapshot`.
## Self-Review Checklist
<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
---------
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
Co-authored-by: Conrad <conrad@zed.dev>
518 lines
18 KiB
Rust
518 lines
18 KiB
Rust
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);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|