Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ members = [
"crates/editor",
"crates/icons",
"crates/ryuk",
"crates/text",
"crates/ui",
"crates/util",
"crates/workspace",
Expand Down
4 changes: 2 additions & 2 deletions crates/buffer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ name = "buffer"
path = "src/buffer.rs"

[features]
test-support = ["text/test-support"]
test-support = []

[dependencies]
anyhow = { workspace = true }
gpui = { workspace = true }
text = { workspace = true }
ropey = { workspace = true }
serde = { workspace = true }
256 changes: 234 additions & 22 deletions crates/buffer/src/buffer.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,91 @@
mod format_span;
mod selection;
pub mod text;

pub use format_span::*;
pub use selection::*;
pub use text::*;

use std::ops::Range;
use std::{
collections::VecDeque,
ops::Range,
time::{Duration, Instant},
};

use text::{TextBuffer, TextPoint};
pub type TransactionId = usize;

pub struct TransactionContext {
texts: Vec<TextOperation>,
format: Option<FormatOperation>,
}

#[derive(Clone, Debug)]
pub struct TextOperation {
pub range: Range<usize>,
pub before: String,
pub after: String,
}

impl TextOperation {
pub fn invert(&self) -> Self {
TextOperation {
range: self.range.start..(self.range.start + self.after.len()),
before: self.after.clone(),
after: self.before.clone(),
}
}
}

#[derive(Clone, Debug)]
pub enum FormatOperation {
ToggleBold(Range<usize>),
ToggleItalic(Range<usize>),
ToggleUnderline(Range<usize>),
}

#[derive(Clone, Debug)]
pub enum TransactionKind {
Text(Vec<TextOperation>),
Format(FormatOperation),
}

#[derive(Clone, Debug)]
struct Transaction {
id: TransactionId,
timestamp: Instant,
kind: TransactionKind,
}

#[derive(Clone, Debug)]
pub struct Buffer {
text: TextBuffer,
text: Text,
format_spans: Vec<FormatSpan>,
next_transaction_id: usize,
undo_stack: VecDeque<Transaction>,
redo_stack: VecDeque<Transaction>,
group_interval: Duration,
}

impl Buffer {
pub fn new() -> Self {
Self {
text: TextBuffer::new(),
text: Text::new(),
format_spans: Vec::new(),
next_transaction_id: 0,
undo_stack: VecDeque::new(),
redo_stack: VecDeque::new(),
group_interval: Duration::from_millis(300),
}
}

pub fn from_text(text: impl Into<String>) -> Self {
Self {
text: TextBuffer::from(text.into().as_str()),
text: Text::from(text.into().as_str()),
format_spans: Vec::new(),
next_transaction_id: 0,
undo_stack: VecDeque::new(),
redo_stack: VecDeque::new(),
group_interval: Duration::from_millis(300),
}
}

Expand Down Expand Up @@ -82,34 +142,49 @@ impl Buffer {
&self.format_spans
}

pub fn insert(&mut self, offset: usize, text: &str) {
let len = text.len();
pub fn insert(&mut self, tx: &mut TransactionContext, offset: usize, text: &str) {
tx.texts.push(TextOperation {
range: offset..offset,
before: String::new(),
after: text.to_string(),
});

self.text.insert(offset, text);

let delta = len as isize;
for span in &mut self.format_spans {
span.shift_by_delta(offset, delta);
let delta = text.len() as isize;
for format_span in &mut self.format_spans {
format_span.shift_by_delta(offset, delta);
}
}

pub fn remove(&mut self, range: Range<usize>) {
let len = range.len();
pub fn remove(&mut self, tx: &mut TransactionContext, range: Range<usize>) {
tx.texts.push(TextOperation {
range: range.clone(),
before: self.text.slice(range.clone()).to_string(),
after: String::new(),
});

self.text.remove(range.clone());

let delta = -(len as isize);
for span in &mut self.format_spans {
span.shift_by_delta(range.start, delta);
let delta = -(range.len() as isize);
for format_span in &mut self.format_spans {
format_span.shift_by_delta(range.start, delta);
}
self.format_spans.retain(|s| !s.range.is_empty());
}

self.format_spans.retain(|span| !span.range.is_empty());
pub fn replace(&mut self, tx: &mut TransactionContext, range: Range<usize>, text: &str) {
self.remove(tx, range.clone());
self.insert(tx, range.start, text);
}

pub fn replace(&mut self, range: Range<usize>, text: &str) {
self.remove(range.clone());
self.insert(range.start, text);
pub fn toggle_bold(&mut self, tx: &mut TransactionContext, range: Range<usize>) {
tx.format = Some(FormatOperation::ToggleBold(range.clone()));
self.toggle_bold_unchecked(range);
}

pub fn toggle_bold(&mut self, range: Range<usize>) {
/// Toggles bold without recording in transaction history.
fn toggle_bold_unchecked(&mut self, range: Range<usize>) {
let is_fully_bold = self.is_formatted_with(&range, |span| span.bold);

let should_split = |span: &FormatSpan| {
Expand Down Expand Up @@ -159,7 +234,13 @@ impl Buffer {
}
}

pub fn toggle_italic(&mut self, range: Range<usize>) {
pub fn toggle_italic(&mut self, tx: &mut TransactionContext, range: Range<usize>) {
tx.format = Some(FormatOperation::ToggleItalic(range.clone()));
self.toggle_italic_unchecked(range);
}

/// Toggles italic without recording in transaction history.
fn toggle_italic_unchecked(&mut self, range: Range<usize>) {
let is_fully_italic = self.is_formatted_with(&range, |span| span.italic);

let should_split = |span: &FormatSpan| {
Expand Down Expand Up @@ -209,7 +290,13 @@ impl Buffer {
}
}

pub fn toggle_underline(&mut self, range: Range<usize>) {
pub fn toggle_underline(&mut self, tx: &mut TransactionContext, range: Range<usize>) {
tx.format = Some(FormatOperation::ToggleUnderline(range.clone()));
self.toggle_underline_unchecked(range);
}

/// Toggles underline without recording in transaction history.
fn toggle_underline_unchecked(&mut self, range: Range<usize>) {
let is_fully_underline = self.is_formatted_with(&range, |span| span.underline);

let should_split = |span: &FormatSpan| {
Expand Down Expand Up @@ -288,6 +375,131 @@ impl Buffer {
})
.is_some_and(|cursor| cursor >= range.end)
}

pub fn transaction<F>(&mut self, now: Instant, f: F) -> TransactionId
where
F: FnOnce(&mut Self, &mut TransactionContext),
{
let transaction_id = self.next_transaction_id;
let mut tx = TransactionContext {
texts: Vec::new(),
format: None,
};

f(self, &mut tx);

if !tx.texts.is_empty() {
self.commit_transaction(TransactionKind::Text(tx.texts), now)
} else if let Some(format_operation) = tx.format {
self.commit_transaction(TransactionKind::Format(format_operation), now)
} else {
transaction_id
}
}

fn commit_transaction(&mut self, kind: TransactionKind, now: Instant) -> TransactionId {
let transaction_id = self.next_transaction_id;
self.next_transaction_id += 1;

if let TransactionKind::Text(ref new_text_operations) = kind
&& let Some(last) = self.undo_stack.back_mut()
&& now.saturating_duration_since(last.timestamp) < self.group_interval
&& let TransactionKind::Text(ref mut last_text_operations) = last.kind
{
last_text_operations.extend_from_slice(new_text_operations);
last.timestamp = now;
self.redo_stack.clear();
return last.id;
}

self.undo_stack.push_back(Transaction {
id: transaction_id,
timestamp: now,
kind,
});
self.redo_stack.clear();
transaction_id
}

fn exec_format_operation(&mut self, format_operation: &FormatOperation) {
match format_operation {
FormatOperation::ToggleBold(range) => self.toggle_bold_unchecked(range.clone()),
FormatOperation::ToggleItalic(range) => self.toggle_italic_unchecked(range.clone()),
FormatOperation::ToggleUnderline(range) => {
self.toggle_underline_unchecked(range.clone())
}
}
}

fn exec_text_operation(&mut self, text_operation: &TextOperation) {
if text_operation.before.is_empty() && !text_operation.after.is_empty() {
self.text
.insert(text_operation.range.start, &text_operation.after);
let delta = text_operation.after.len() as isize;
for span in &mut self.format_spans {
span.shift_by_delta(text_operation.range.start, delta);
}
} else if !text_operation.before.is_empty() && text_operation.after.is_empty() {
self.text.remove(text_operation.range.clone());
let delta = -(text_operation.before.len() as isize);
for span in &mut self.format_spans {
span.shift_by_delta(text_operation.range.start, delta);
}
self.format_spans.retain(|s| !s.range.is_empty());
} else {
self.text.remove(text_operation.range.clone());
if !text_operation.after.is_empty() {
self.text
.insert(text_operation.range.start, &text_operation.after);
}
let delta = text_operation.after.len() as isize - text_operation.before.len() as isize;
for span in &mut self.format_spans {
span.shift_by_delta(text_operation.range.start, delta);
}
self.format_spans.retain(|span| !span.range.is_empty());
}
}

pub fn undo(&mut self) -> Option<TransactionId> {
let tx = self.undo_stack.pop_back()?;

match &tx.kind {
TransactionKind::Text(text_operations) => {
for text_operation in text_operations.iter().rev() {
self.exec_text_operation(&text_operation.invert());
}
}
TransactionKind::Format(format_operation) => {
self.exec_format_operation(format_operation);
}
}

self.redo_stack.push_back(tx.clone());
Some(tx.id)
}

pub fn redo(&mut self) -> Option<TransactionId> {
let tx = self.redo_stack.pop_back()?;

match &tx.kind {
TransactionKind::Text(text_operations) => {
for text_operation in text_operations {
self.exec_text_operation(text_operation);
}
}
TransactionKind::Format(format_operation) => {
self.exec_format_operation(format_operation);
}
}

self.undo_stack.push_back(tx.clone());
Some(tx.id)
}

#[cfg(any(test, feature = "test-support"))]
pub fn set_group_interval(&mut self, interval: Duration) {
self.group_interval = interval;
}
}

impl Default for Buffer {
Expand Down
Loading