Files
oak-gpui/crates/multi_buffer/src/transaction.rs
T
Lukas Wirth c98b2d6944 multi_buffer: Typed MultiBufferOffset (#42707)
This PR introduces a new `MultiBufferOffset` new type wrapping size. The
goal of this is to make it clear at the type level when we are
interacting with offsets of a multi buffer versus offsets of a language
/ text buffer. This improves readability of things quite a bit by making
it clear what kind of offsets one is working with while also reducing
accidental bugs by using the wrong kin of offset for the wrong API.

This PR also uncovered two minor bugs due to that.

Does not yet introduce the MultiBufferPoint equivalent, that is for a
follow up PR.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-19 22:00:58 +00:00

539 lines
19 KiB
Rust

use gpui::{App, Context, Entity};
use language::{self, Buffer, TransactionId};
use std::{
collections::HashMap,
ops::{AddAssign, Range, Sub},
time::{Duration, Instant},
};
use sum_tree::Bias;
use text::BufferId;
use crate::{BufferState, MultiBufferDimension};
use super::{Event, ExcerptSummary, 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<D>(
&self,
transaction_id: TransactionId,
cx: &App,
) -> Vec<Range<D>>
where
D: MultiBufferDimension
+ Ord
+ Sub<D, Output = D::TextDimension>
+ AddAssign<D::TextDimension>,
D::TextDimension: PartialOrd + Sub<D::TextDimension, Output = D::TextDimension>,
{
let Some(transaction) = self.history.transaction(transaction_id) else {
return Vec::new();
};
let mut ranges = Vec::new();
let snapshot = self.read(cx);
let mut cursor = snapshot.excerpts.cursor::<ExcerptSummary>(());
for (buffer_id, buffer_transaction) in &transaction.buffer_transactions {
let Some(buffer_state) = self.buffers.get(buffer_id) else {
continue;
};
let buffer = buffer_state.buffer.read(cx);
for range in
buffer.edited_ranges_for_transaction_id::<D::TextDimension>(*buffer_transaction)
{
for excerpt_id in &buffer_state.excerpts {
cursor.seek(excerpt_id, Bias::Left);
if let Some(excerpt) = cursor.item()
&& excerpt.locator == *excerpt_id
{
let excerpt_buffer_start = excerpt
.range
.context
.start
.summary::<D::TextDimension>(buffer);
let excerpt_buffer_end = excerpt
.range
.context
.end
.summary::<D::TextDimension>(buffer);
let excerpt_range = excerpt_buffer_start..excerpt_buffer_end;
if excerpt_range.contains(&range.start)
&& excerpt_range.contains(&range.end)
{
let excerpt_start = D::from_summary(&cursor.start().text);
let mut start = excerpt_start;
start += range.start - excerpt_buffer_start;
let mut end = excerpt_start;
end += range.end - excerpt_buffer_start;
ranges.push(start..end);
break;
}
}
}
}
}
ranges.sort_by_key(|range| range.start);
ranges
}
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);
});
}
}
}
}
}