use std::{
fs,
ops::Range,
path::{Path, PathBuf},
};
use std::sync::Arc;
use ropey::{LineType, Rope};
use serde::{Deserialize, Serialize};
use crate::editor::history::{apply_changeset, BufferOp, ChangeKind, ChangeSet, History};
use crate::editor::position::Position;
use crate::editor::selection::Selection;
use crate::prelude::*;
use crate::settings::adapters;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BufferId(u64);
impl BufferId {
pub fn new() -> Self {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
Self(COUNTER.fetch_add(1, Ordering::Relaxed))
}
}
impl Default for BufferId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
pub struct Version(u64);
impl Version {
pub fn new() -> Self {
Self(0)
}
pub fn value(&self) -> u64 {
self.0
}
#[cfg(test)]
pub(crate) fn from_raw(v: u64) -> Self {
Self(v)
}
fn increment(&mut self) {
self.0 += 1;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableSnapshot {
lines: Vec<String>,
cursor: Position,
#[serde(default)]
selection: Option<Selection>,
dirty: bool,
version: Version,
}
impl From<&Buffer> for SerializableSnapshot {
fn from(buf: &Buffer) -> Self {
let lines = buf.lines();
SerializableSnapshot {
lines,
cursor: buf.cursor,
selection: buf.selection,
dirty: buf.dirty,
version: buf.version,
}
}
}
impl SerializableSnapshot {
pub fn lines(&self) -> &[String] {
&self.lines
}
pub fn cursor(&self) -> Position {
self.cursor
}
pub fn selection(&self) -> Option<Selection> {
self.selection
}
pub fn dirty(&self) -> bool {
self.dirty
}
pub fn version(&self) -> Version {
self.version
}
}
#[derive(Debug, Clone)]
pub struct BufferSnapshot {
pub text: Rope,
pub version: Version,
}
impl BufferSnapshot {
pub fn lines(&self) -> Vec<String> {
let line_type = LineType::LF;
(0..self.text.len_lines(line_type))
.map(|i| {
let mut line_text = self.text.line(i, line_type).to_string();
if line_text.ends_with('\n') {
line_text.pop();
}
line_text
})
.collect()
}
pub fn line_count(&self) -> usize {
self.text.len_lines(LineType::LF)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Marker {
pub line: usize,
pub label: String,
}
#[derive(Debug)]
struct Transaction {
ops: Vec<BufferOp>,
kind: ChangeKind,
before_cursor: Position,
before_selection: Option<Selection>,
}
#[derive(Debug)]
pub struct Buffer {
id: BufferId,
version: Version,
text: Rope,
pub path: Option<PathBuf>,
cursor: Position,
selection: Option<Selection>,
pub scroll: usize,
pub scroll_x: usize,
dirty: bool,
history: History,
pub markers: Vec<Marker>,
active_tx: Option<Transaction>,
pub file_hash: Option<u64>,
pub file_mtime: Option<u64>,
pub file_size: Option<u64>,
pub external_modification_detected: bool,
pub is_saving: bool,
cached_lines: Option<Arc<Vec<String>>>,
}
impl Buffer {
pub fn new() -> Self {
Self {
id: BufferId::new(),
version: Version::new(),
text: Rope::new(),
path: None,
cursor: Position::default(),
selection: None,
scroll: 0,
scroll_x: 0,
dirty: false,
history: History::new(),
markers: Vec::new(),
active_tx: None,
file_hash: None,
file_mtime: None,
file_size: None,
external_modification_detected: false,
is_saving: false,
cached_lines: None,
}
}
pub fn from_text(text: &str) -> Self {
let mut buf = Self::new();
buf.text = Rope::from_str(text);
let line_count = buf.text.len_lines(LineType::LF);
if line_count > 0 {
let last_line_idx = line_count - 1;
let last_line_len = buf.text.line(last_line_idx, LineType::LF).len_chars();
buf.cursor = Position::new(last_line_idx, last_line_len);
}
buf
}
#[cfg(test)]
pub(crate) fn from_text_no_merge(text: &str) -> Self {
let mut buf = Self::from_text(text);
buf.history = History::with_threshold(0);
buf
}
pub fn from_lines(lines: Vec<String>, path: Option<PathBuf>) -> Self {
let text = if lines.is_empty() {
String::new()
} else {
lines.join("\n")
};
let mut buf = Self::new();
buf.text = Rope::from_str(&text);
buf.path = path;
buf.cursor = Position::new(0, 0);
buf
}
pub fn open(path: &Path) -> Result<Self> {
let content = fs::read_to_string(path)?;
let mut buf = Self::new();
buf.text = Rope::from_str(&content);
buf.path = Some(path.to_path_buf());
buf.normalize_cursor();
Ok(buf)
}
#[allow(clippy::too_many_arguments)]
pub fn restore(
path: PathBuf,
lines: Vec<String>,
selection: Option<Selection>,
scroll: usize,
dirty: bool,
markers: Vec<Marker>,
undo_stack: Vec<SerializableSnapshot>,
redo_stack: Vec<SerializableSnapshot>,
) -> Self {
let text = if lines.is_empty() {
Rope::new()
} else {
Rope::from_str(&lines.join("\n"))
};
let cursor = selection.as_ref().map(|s| s.active).unwrap_or_default();
let version = Version::new();
let mut history = History::new();
history.restore_from_snapshots(undo_stack, redo_stack, version, lines.clone());
let file_hash = Self::compute_hash(&text);
let file_metadata = Self::get_file_metadata(&path);
let file_mtime = file_metadata.as_ref().map(|(m, _)| *m);
let file_size = file_metadata.map(|(_, s)| s);
let mut buf = Self {
id: BufferId::new(),
version,
text,
path: Some(path),
cursor,
selection,
scroll,
scroll_x: 0,
dirty,
history,
markers,
active_tx: None,
file_hash,
file_mtime,
file_size,
external_modification_detected: false,
is_saving: false,
cached_lines: None,
};
let sel = buf.selection.take();
if let Some(s) = sel {
buf.set_selection(Some(Selection {
anchor: buf.normalize_position(s.anchor),
active: buf.normalize_position(s.active),
}));
} else {
buf.normalize_cursor();
}
buf
}
pub fn id(&self) -> BufferId {
self.id
}
pub fn version(&self) -> Version {
self.version
}
pub fn snapshot(&self) -> BufferSnapshot {
BufferSnapshot {
text: self.text.clone(),
version: self.version,
}
}
fn line_type(&self) -> LineType {
LineType::LF
}
fn pos_to_byte(&self, pos: Position) -> usize {
let line_count = self.text.len_lines(self.line_type());
if pos.line >= line_count {
if pos.line == line_count && pos.column == 0 && line_count > 0 {
let last_line_idx = line_count - 1;
let last_line = self.text.line(last_line_idx, self.line_type());
if last_line.chars().last() == Some('\n') {
let last_line_start =
self.text.line_to_byte_idx(last_line_idx, self.line_type());
return last_line_start + last_line.len() - 1;
}
}
return self.text.len();
}
let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
let line = self.text.line(pos.line, self.line_type());
let mut char_count = 0;
let mut byte_offset = 0;
for ch in line.chars() {
if char_count >= pos.column {
break;
}
byte_offset += ch.len_utf8();
char_count += 1;
}
if pos.column > char_count {
byte_offset = line.len();
}
line_start_byte + byte_offset
}
fn byte_to_pos(&self, byte_idx: usize) -> Position {
let clamped_byte_idx = byte_idx.min(self.text.len());
let line_count = self.text.len_lines(self.line_type());
if clamped_byte_idx == self.text.len() {
if line_count == 0 {
return Position::new(0, 0);
}
let last_line_idx = line_count - 1;
let last_line = self.text.line(last_line_idx, self.line_type());
let last_line_len = last_line.len_chars();
let has_trailing_nl = last_line.chars().last() == Some('\n');
if has_trailing_nl {
return Position::new(last_line_idx + 1, 0);
}
return Position::new(last_line_idx, last_line_len);
}
let byte_idx = clamped_byte_idx;
let line_idx = self.text.byte_to_line_idx(byte_idx, self.line_type());
let line_start_byte = self.text.line_to_byte_idx(line_idx, self.line_type());
let char_idx = self.text.byte_to_char_idx(byte_idx);
let line_start_char = self.text.byte_to_char_idx(line_start_byte);
let line = self.text.line(line_idx, self.line_type());
let line_len_chars = line.len_chars();
let line_has_trailing_nl = line.chars().last() == Some('\n');
let column = char_idx - line_start_char;
if column >= line_len_chars && line_has_trailing_nl {
return Position::new(line_idx + 1, 0);
}
Position::new(line_idx, column)
}
fn cursor_to_byte(&self) -> usize {
self.pos_to_byte(self.cursor)
}
pub fn pos_to_char(&self, pos: Position) -> usize {
let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
let line_start_char = self.text.byte_to_char_idx(line_start_byte);
line_start_char + pos.column
}
pub fn char_to_pos(&self, char_idx: usize) -> Position {
let byte_idx = self.text.char_to_byte_idx(char_idx).min(self.text.len());
self.byte_to_pos(byte_idx)
}
pub fn cursor(&self) -> Position {
self.cursor
}
pub fn selection(&self) -> Option<Selection> {
self.selection
}
pub fn set_cursor(&mut self, pos: Position) {
self.selection = None;
self.cursor = self.normalize_position(pos);
}
pub fn set_selection(&mut self, selection: Option<Selection>) {
self.selection = selection.map(|sel| Selection {
anchor: self.normalize_position(sel.anchor),
active: self.normalize_position(sel.active),
});
if let Some(ref sel) = self.selection {
self.cursor = sel.active;
}
}
pub fn normalize_position(&self, pos: Position) -> Position {
let line_count = self.text.len_lines(self.line_type());
if line_count == 0 {
return Position::new(0, 0);
}
let line = pos.line.min(line_count - 1);
let line_len = self.line_display_len(line);
Position::new(line, pos.column.min(line_len))
}
pub fn normalize_cursor(&mut self) {
self.cursor = self.normalize_position(self.cursor);
}
fn line_display_len(&self, line_idx: usize) -> usize {
let line = self.text.line(line_idx, self.line_type());
let has_trailing_nl = line.chars().last() == Some('\n');
line.len_chars().saturating_sub(has_trailing_nl as usize)
}
pub fn line_count(&self) -> usize {
self.text.len_lines(self.line_type()).max(1)
}
pub fn current_line(&self) -> usize {
self.cursor.line
}
pub fn char_count(&self) -> usize {
self.text.len_chars()
}
pub fn line(&self, line_idx: usize) -> Option<String> {
let actual_line_count = self.text.len_lines(self.line_type());
if line_idx >= actual_line_count {
return None;
}
let mut line_text = self.text.line(line_idx, self.line_type()).to_string();
if line_text.ends_with('\n') {
line_text.pop();
}
if line_text.ends_with('\r') {
line_text.pop();
}
Some(line_text)
}
pub fn lines(&self) -> Vec<String> {
(0..self.text.len_lines(self.line_type()))
.map(|i| {
let mut s: String = self.text.line(i, self.line_type()).into();
if s.ends_with('\n') {
s.pop();
}
if s.ends_with('\r') {
s.pop();
}
s
})
.collect()
}
pub fn lines_arc(&mut self) -> Arc<Vec<String>> {
if let Some(ref cached) = self.cached_lines {
return Arc::clone(cached);
}
let v = self.lines();
let arc = Arc::new(v);
self.cached_lines = Some(arc.clone());
arc
}
pub fn into_lines(self) -> Vec<String> {
if let Some(arc) = self.cached_lines {
match Arc::try_unwrap(arc) {
Ok(v) => v,
Err(arc) => (*arc).clone(),
}
} else {
self.lines()
}
}
pub fn is_empty(&self) -> bool {
self.text.len_chars() == 0
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
fn increment_version(&mut self) {
self.version.increment();
self.dirty = true;
self.file_hash = None;
self.file_mtime = None;
self.file_size = None;
self.cached_lines = None;
}
pub fn can_undo(&self) -> bool {
self.history.can_undo()
}
pub fn can_redo(&self) -> bool {
self.history.can_redo()
}
pub fn history_len(&self) -> usize {
self.history.len()
}
pub fn history_position(&self) -> usize {
self.history.current_position()
}
pub fn apply_change(&mut self, change: ChangeSet, kind: ChangeKind) {
if change.is_empty() {
return;
}
if let Some(tx) = &mut self.active_tx {
tx.ops.extend(change.ops);
return;
}
let merged = matches!(kind, ChangeKind::InsertText | ChangeKind::DeleteText)
&& self.history.try_merge(change.clone(), kind, self.version);
if !merged {
self.history.push(change, kind, self.version);
}
}
pub fn begin_transaction(&mut self, kind: ChangeKind) {
debug_assert!(
self.active_tx.is_none(),
"begin_transaction called while a transaction is already active; \
nested transactions are not supported"
);
if self.active_tx.is_some() {
return;
}
self.active_tx = Some(Transaction {
ops: Vec::new(),
kind,
before_cursor: self.cursor,
before_selection: self.selection,
});
}
pub fn end_transaction(&mut self) {
let tx = match self.active_tx.take() {
Some(tx) => tx,
None => return,
};
if tx.ops.is_empty() {
return;
}
let change = ChangeSet::new(
tx.ops,
tx.before_cursor,
self.cursor,
tx.before_selection,
self.selection,
);
self.history.push(change, tx.kind, self.version);
self.increment_version();
}
pub fn abort_transaction(&mut self) {
self.active_tx = None;
}
pub fn in_transaction(&self) -> bool {
self.active_tx.is_some()
}
pub fn insert(&mut self, text: &str) {
if let Some(sel) = self.selection {
let start = sel.min();
let end = sel.max();
let before_cursor = self.cursor;
let before_selection = self.selection;
let start_byte = self.pos_to_byte(start);
let end_byte = self.pos_to_byte(end).min(self.text.len());
let old_text: String = self.text.slice(start_byte..end_byte).into();
self.text.remove(start_byte..end_byte);
self.text.insert(start_byte, text);
let new_char_idx = self.text.byte_to_char_idx(start_byte) + text.chars().count();
let after_cursor = self.byte_to_pos(
self.text
.char_to_byte_idx(new_char_idx)
.min(self.text.len()),
);
self.cursor = after_cursor;
self.selection = None;
let op = BufferOp::Replace {
range: start..end,
text: text.to_string(),
old_text,
end_position: after_cursor,
};
let change = ChangeSet::new(
vec![op],
before_cursor,
after_cursor,
before_selection,
None,
);
self.apply_change(change, ChangeKind::Replace);
self.increment_version();
return;
}
let before_cursor = self.cursor;
let before_selection = self.selection;
let byte_idx = self.cursor_to_byte();
let old_char_idx = self.text.byte_to_char_idx(byte_idx);
self.text.insert(byte_idx, text);
let new_char_idx = old_char_idx + text.chars().count();
let new_char_byte = self
.text
.char_to_byte_idx(new_char_idx)
.min(self.text.len());
let new_cursor = self.byte_to_pos(new_char_byte);
self.cursor = new_cursor;
let after_cursor = new_cursor;
let after_selection = None;
let range = before_cursor..after_cursor;
let op = BufferOp::Replace {
range,
text: text.to_string(),
old_text: String::new(),
end_position: after_cursor,
};
let change = ChangeSet::new(
vec![op],
before_cursor,
after_cursor,
before_selection,
after_selection,
);
self.apply_change(change, ChangeKind::InsertText);
self.increment_version();
}
fn delete_selection_if_active(&mut self) -> bool {
if let Some(sel) = self.selection {
self.delete_range(sel.min(), sel.max());
true
} else {
false
}
}
pub fn delete_backward(&mut self) {
if self.delete_selection_if_active() {
return;
}
let byte_idx = self.cursor_to_byte();
if byte_idx == 0 {
return;
}
let before_cursor = self.cursor;
let before_selection = self.selection;
let char_idx = self.text.byte_to_char_idx(byte_idx);
if char_idx == 0 {
return;
}
let prev_char_idx = char_idx - 1;
let del_start_byte = self.text.char_to_byte_idx(prev_char_idx);
let del_end_byte = byte_idx;
let prev_char_byte_idx = del_start_byte;
let prev_char = self.text.char(prev_char_byte_idx);
let new_cursor_pos: Position;
if prev_char == '\n' {
let line_idx = self.text.byte_to_line_idx(byte_idx, self.line_type());
if line_idx > 0 {
let new_cursor_col = self.line_display_len(line_idx - 1);
new_cursor_pos = Position::new(line_idx - 1, new_cursor_col);
} else {
new_cursor_pos = before_cursor;
}
} else {
let new_col = before_cursor.column.saturating_sub(1);
new_cursor_pos = Position::new(before_cursor.line, new_col);
}
let deleted_text: String = self.text.slice(del_start_byte..del_end_byte).into();
self.text.remove(del_start_byte..del_end_byte);
self.cursor = new_cursor_pos;
self.selection = None;
let range = new_cursor_pos..before_cursor;
let op = BufferOp::Replace {
range,
text: String::new(),
old_text: deleted_text,
end_position: new_cursor_pos,
};
let change = ChangeSet::new(
vec![op],
before_cursor,
new_cursor_pos,
before_selection,
None,
);
self.apply_change(change, ChangeKind::DeleteText);
self.increment_version();
}
pub fn delete_forward(&mut self) {
if self.delete_selection_if_active() {
return;
}
let byte_idx = self.cursor_to_byte();
if byte_idx >= self.text.len() {
return;
}
let before_cursor = self.cursor;
let before_selection = self.selection;
let char_idx = self.text.byte_to_char_idx(byte_idx);
let next_byte = self
.text
.char_to_byte_idx(char_idx + 1)
.min(self.text.len());
let deleted_text: String = self.text.slice(byte_idx..next_byte).into();
self.text.remove(byte_idx..next_byte);
let after_cursor = before_cursor;
let after_selection = None;
let range = before_cursor..after_cursor;
let op = BufferOp::Replace {
range,
text: String::new(),
old_text: deleted_text,
end_position: before_cursor,
};
let change = ChangeSet::new(
vec![op],
before_cursor,
after_cursor,
before_selection,
after_selection,
);
self.apply_change(change, ChangeKind::DeleteText);
self.increment_version();
}
pub fn delete_range(&mut self, start_pos: Position, end_pos: Position) {
if start_pos == end_pos {
return;
}
let start_byte = self.pos_to_byte(start_pos);
let end_byte = self.pos_to_byte(end_pos).min(self.text.len());
if start_byte >= self.text.len() || start_byte >= end_byte {
return;
}
let before_cursor = self.cursor;
let before_selection = self.selection;
let deleted_text: String = self.text.slice(start_byte..end_byte).into();
self.text.remove(start_byte..end_byte);
self.cursor = start_pos;
self.selection = None;
let range = start_pos..end_pos;
let op = BufferOp::Replace {
range,
text: String::new(),
old_text: deleted_text,
end_position: start_pos,
};
let change = ChangeSet::new(vec![op], before_cursor, start_pos, before_selection, None);
self.apply_change(change, ChangeKind::DeleteText);
self.increment_version();
}
pub fn replace_range(&mut self, start: Position, end: Position, text: &str) {
let start_byte = self.pos_to_byte(start);
let end_byte = self.pos_to_byte(end).min(self.text.len());
if start_byte >= self.text.len() && start != end {
return;
}
let before_cursor = self.cursor;
let before_selection = self.selection;
let actual_end_byte = end_byte.min(self.text.len());
let old_text: String = self.text.slice(start_byte..actual_end_byte).into();
if old_text == text && before_cursor == start {
return;
}
self.text.remove(start_byte..actual_end_byte);
self.text.insert(start_byte, text);
let new_char_idx = self.text.byte_to_char_idx(start_byte) + text.chars().count();
let after_cursor = self.byte_to_pos(self.text.char_to_byte_idx(new_char_idx));
self.cursor = after_cursor;
self.selection = None;
let range = start..end;
let op = BufferOp::Replace {
range,
text: text.to_string(),
old_text,
end_position: after_cursor,
};
let change = ChangeSet::new(
vec![op],
before_cursor,
after_cursor,
before_selection,
None,
);
self.apply_change(change, ChangeKind::Replace);
self.increment_version();
}
pub fn insert_newline(&mut self) {
let before_cursor = self.cursor;
let before_selection = self.selection;
let byte_idx = self.cursor_to_byte();
self.text.insert(byte_idx, "\n");
let new_char_idx = self.text.byte_to_char_idx(byte_idx) + 1;
let new_cursor_byte = self
.text
.char_to_byte_idx(new_char_idx)
.min(self.text.len());
let new_cursor = self.byte_to_pos(new_cursor_byte);
self.cursor = new_cursor;
let range = before_cursor..new_cursor;
let op = BufferOp::Replace {
range,
text: "\n".to_string(),
old_text: String::new(),
end_position: new_cursor,
};
let change = ChangeSet::new(vec![op], before_cursor, new_cursor, before_selection, None);
self.apply_change(change, ChangeKind::Structural);
self.increment_version();
}
pub fn insert_newline_with_indent(&mut self, _use_spaces: bool, _indent_width: usize) {
let leading_indent = if self.cursor.line < self.text.len_lines(self.line_type()) {
let line = self.text.line(self.cursor.line, self.line_type());
line.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect::<String>()
} else {
String::new()
};
let text = format!("\n{}", leading_indent);
let before_cursor = self.cursor;
let before_selection = self.selection;
let byte_idx = self.cursor_to_byte();
self.text.insert(byte_idx, &text);
let new_char_idx = self.text.byte_to_char_idx(byte_idx) + text.chars().count();
let new_cursor = self.byte_to_pos(self.text.char_to_byte_idx(new_char_idx));
self.cursor = new_cursor;
let range = before_cursor..new_cursor;
let op = BufferOp::Replace {
range,
text,
old_text: String::new(),
end_position: new_cursor,
};
let change = ChangeSet::new(vec![op], before_cursor, new_cursor, before_selection, None);
self.apply_change(change, ChangeKind::Structural);
self.increment_version();
}
pub fn delete_word_backward(&mut self) {
let end_byte = self.cursor_to_byte();
if end_byte == 0 {
return;
}
let before_cursor = self.cursor;
let before_selection = self.selection;
let start_pos = self.word_boundary_prev(self.cursor);
let start_byte = self.pos_to_byte(start_pos);
if start_byte < end_byte {
let deleted_text: String = self.text.slice(start_byte..end_byte).into();
self.text.remove(start_byte..end_byte);
let new_pos = self.byte_to_pos(start_byte);
self.cursor = new_pos;
let range = new_pos..before_cursor;
let op = BufferOp::Replace {
range,
text: String::new(),
old_text: deleted_text,
end_position: new_pos,
};
let change = ChangeSet::new(vec![op], before_cursor, new_pos, before_selection, None);
self.apply_change(change, ChangeKind::DeleteText);
self.increment_version();
}
}
pub fn delete_word_forward(&mut self) {
let start = self.cursor_to_byte();
if start >= self.text.len() {
return;
}
let before_cursor = self.cursor;
let before_selection = self.selection;
let mut char_idx = self.text.byte_to_char_idx(start);
let end_char = self.text.len_chars();
let first_char = self.text.char(self.text.char_to_byte_idx(char_idx));
let at_whitespace = first_char.is_whitespace();
while char_idx < end_char
&& self
.text
.char(self.text.char_to_byte_idx(char_idx))
.is_whitespace()
{
char_idx += 1;
}
if !at_whitespace {
while char_idx < end_char {
let curr_byte = self.text.char_to_byte_idx(char_idx);
if self.text.char(curr_byte).is_whitespace() {
break;
}
char_idx += 1;
}
while char_idx < end_char
&& self
.text
.char(self.text.char_to_byte_idx(char_idx))
.is_whitespace()
{
char_idx += 1;
}
}
let end_byte = if char_idx < end_char {
self.text.char_to_byte_idx(char_idx)
} else {
self.text.len()
};
let start_byte = self
.text
.char_to_byte_idx(self.text.byte_to_char_idx(start));
if start_byte < end_byte {
let deleted_text: String = self.text.slice(start_byte..end_byte).into();
self.text.remove(start_byte..end_byte);
let range = before_cursor..before_cursor;
let op = BufferOp::Replace {
range,
text: String::new(),
old_text: deleted_text,
end_position: before_cursor,
};
let change = ChangeSet::new(
vec![op],
before_cursor,
before_cursor,
before_selection,
None,
);
self.apply_change(change, ChangeKind::DeleteText);
self.increment_version();
}
}
pub fn word_range_at(&self, pos: Position) -> (usize, usize) {
let byte_idx = self.pos_to_byte(pos);
let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
let line = self.text.line(pos.line, self.line_type());
let mut word_start = 0;
let mut word_end = line.len_chars();
let mut in_word = false;
for (i, (byte_offset, ch)) in line.char_indices().enumerate() {
let abs_byte = line_start_byte + byte_offset;
if abs_byte >= byte_idx && ch.is_alphanumeric() && !in_word {
word_start = i;
in_word = true;
}
if in_word && !ch.is_alphanumeric() {
word_end = i;
break;
}
}
(word_start, word_end)
}
pub fn insert_at(&mut self, pos: Position, text: &str) {
let before_cursor = self.cursor;
let before_selection = self.selection;
let byte_idx = self.pos_to_byte(pos);
self.text.insert(byte_idx, text);
let new_char_idx = self.text.byte_to_char_idx(byte_idx) + text.chars().count();
let after_cursor = self.byte_to_pos(self.text.char_to_byte_idx(new_char_idx));
if pos == before_cursor {
self.cursor = after_cursor;
}
let range = pos..after_cursor;
let op = BufferOp::Replace {
range,
text: text.to_string(),
old_text: String::new(),
end_position: after_cursor,
};
let change = ChangeSet::new(
vec![op],
before_cursor,
after_cursor,
before_selection,
None,
);
self.apply_change(change, ChangeKind::InsertText);
self.increment_version();
}
pub fn insert_text_raw(&mut self, text: &str) {
let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
self.insert(&normalized);
}
pub fn delete_line(&mut self) -> String {
let line_idx = self.cursor.line;
if line_idx >= self.text.len_lines(self.line_type()) {
return String::new();
}
let line_start = self.text.line_to_byte_idx(line_idx, self.line_type());
let line_end = if line_idx + 1 < self.text.len_lines(self.line_type()) {
self.text.line_to_byte_idx(line_idx + 1, self.line_type())
} else {
self.text.len()
};
let deleted: String = self.text.slice(line_start..line_end).into();
let before_cursor = self.cursor;
let before_selection = self.selection;
self.text.remove(line_start..line_end);
if self.text.len_lines(self.line_type()) == 0 {
self.text.insert(0, "");
}
let new_line_idx = line_idx.min(self.text.len_lines(self.line_type()).saturating_sub(1));
let new_pos = Position::new(new_line_idx, 0);
self.cursor = new_pos;
self.normalize_cursor();
let range = new_pos..before_cursor;
let op = BufferOp::Replace {
range,
text: String::new(),
old_text: deleted.clone(),
end_position: new_pos,
};
let change = ChangeSet::new(vec![op], before_cursor, new_pos, before_selection, None);
self.apply_change(change, ChangeKind::DeleteText);
self.increment_version();
deleted
}
pub fn selected_text_all(&self) -> String {
self.selected_text()
}
pub fn cursor_left(&mut self) -> Position {
let pos = self.offset_left(self.cursor);
self.cursor = pos;
self.cursor
}
pub fn offset_left(&self, pos: Position) -> Position {
if pos.column > 0 {
Position::new(pos.line, pos.column - 1)
} else if pos.line > 0 {
let prev_line = pos.line - 1;
let prev_line_len = self.line_display_len(prev_line);
Position::new(prev_line, prev_line_len)
} else {
pos
}
}
pub fn cursor_right(&mut self) -> Position {
let pos = self.offset_right(self.cursor);
self.cursor = pos;
self.cursor
}
pub fn offset_right(&self, pos: Position) -> Position {
let line_len = self.line_display_len(pos.line);
if pos.column < line_len {
Position::new(pos.line, pos.column + 1)
} else if pos.line < self.text.len_lines(self.line_type()) - 1 {
Position::new(pos.line + 1, 0)
} else {
pos
}
}
fn apply_movement(&mut self, new_pos: Position) -> Position {
self.cursor = new_pos;
self.cursor
}
pub fn cursor_up(&mut self) -> Position {
self.apply_movement(self.offset_up(self.cursor))
}
pub fn offset_up(&self, pos: Position) -> Position {
if pos.line == 0 { pos } else { self.offset_up_n(pos, 1) }
}
pub fn cursor_down(&mut self) -> Position {
self.apply_movement(self.offset_down(self.cursor))
}
pub fn offset_down(&self, pos: Position) -> Position {
if pos.line >= self.text.len_lines(self.line_type()).saturating_sub(1) {
pos
} else {
self.offset_down_n(pos, 1)
}
}
pub fn cursor_up_n(&mut self, n: usize) -> Position {
self.apply_movement(self.offset_up_n(self.cursor, n))
}
pub fn offset_up_n(&self, pos: Position, n: usize) -> Position {
let new_line = pos.line.saturating_sub(n);
let line_len = self.line_display_len(new_line);
Position::new(new_line, pos.column.min(line_len))
}
pub fn cursor_down_n(&mut self, n: usize) -> Position {
self.apply_movement(self.offset_down_n(self.cursor, n))
}
pub fn offset_down_n(&self, pos: Position, n: usize) -> Position {
let new_line = (pos.line + n).min(self.text.len_lines(self.line_type()).saturating_sub(1));
let line_len = self.line_display_len(new_line);
Position::new(new_line, pos.column.min(line_len))
}
pub fn cursor_page_up(&mut self, height: usize) -> Position {
self.apply_movement(self.offset_up_n(self.cursor, height))
}
pub fn cursor_page_down(&mut self, height: usize) -> Position {
self.apply_movement(self.offset_down_n(self.cursor, height))
}
pub fn cursor_line_start(&mut self) -> Position {
self.cursor.column = 0;
self.cursor
}
pub fn offset_line_start(&self, pos: Position) -> Position {
Position::new(pos.line, 0)
}
pub fn cursor_line_end(&mut self) -> Position {
self.cursor.column = self.line_display_len(self.cursor.line);
self.cursor
}
pub fn offset_line_end(&self, pos: Position) -> Position {
Position::new(pos.line, self.line_display_len(pos.line))
}
pub fn cursor_word_prev(&mut self) -> Position {
self.apply_movement(self.word_boundary_prev(self.cursor))
}
pub fn cursor_word_next(&mut self) -> Position {
self.apply_movement(self.word_boundary_next_for_selection(self.cursor))
}
pub fn word_boundary_prev(&self, pos: Position) -> Position {
if pos.line > 0 {
let line_slice = self.text.line(pos.line, self.line_type());
let mut first_non_ws_col: Option<usize> = None;
for (i, c) in line_slice.chars().enumerate() {
if !c.is_whitespace() || c == '\n' {
first_non_ws_col = Some(i);
break;
}
}
if pos.column == 0 || first_non_ws_col == Some(pos.column) {
let prev_line = pos.line - 1;
return Position::new(prev_line, self.line_display_len(prev_line));
}
}
let line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
let byte_idx = line_start_byte + self.text.char_to_byte_idx(pos.column);
if byte_idx == 0 {
return pos;
}
let mut char_idx = self.text.byte_to_char_idx(byte_idx);
if char_idx == 0 {
return pos;
}
char_idx = char_idx.saturating_sub(1);
while char_idx > 0 {
let c_byte = self.text.char_to_byte_idx(char_idx);
let c = self.text.char(c_byte);
if !c.is_whitespace() || c == '\n' {
break;
}
char_idx = char_idx.saturating_sub(1);
}
let c_byte = self.text.char_to_byte_idx(char_idx);
let first_char = self.text.char(c_byte);
let is_punct = first_char.is_ascii_punctuation();
while char_idx > 0 {
let prev_byte = self.text.char_to_byte_idx(char_idx.saturating_sub(1));
let prev_char = self.text.char(prev_byte);
if is_punct {
if !prev_char.is_ascii_punctuation() {
break;
}
} else if prev_char.is_whitespace() || prev_char.is_ascii_punctuation() {
break;
}
char_idx = char_idx.saturating_sub(1);
}
self.byte_to_pos(self.text.char_to_byte_idx(char_idx))
}
pub fn word_boundary_next(&self, pos: Position) -> Position {
self.word_boundary_next_impl(pos, true)
}
pub fn word_boundary_next_for_selection(&self, pos: Position) -> Position {
self.word_boundary_next_impl(pos, false)
}
fn word_boundary_next_impl(&self, mut pos: Position, skip_whitespace_after: bool) -> Position {
let end_char = self.text.len_chars();
let mut line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
let mut line_start_char = self.text.byte_to_char_idx(line_start_byte);
let mut line_len_chars = self.text.line(pos.line, self.line_type()).len_chars();
let line_slice = self.text.line(pos.line, self.line_type());
let mut printable_line_len = if line_slice.chars().last() == Some('\n') {
line_len_chars.saturating_sub(1)
} else {
line_len_chars
};
let mut printable_line_end_char_idx = line_start_char + printable_line_len;
let mut char_idx = self.pos_to_char(pos);
if char_idx >= end_char {
return pos;
}
let total_lines = self.text.len_lines(self.line_type());
if char_idx >= printable_line_end_char_idx {
if pos.line + 1 >= total_lines {
return Position::new(pos.line, printable_line_len);
}
pos = Position::new(pos.line + 1, 0);
line_start_byte = self.text.line_to_byte_idx(pos.line, self.line_type());
line_start_char = self.text.byte_to_char_idx(line_start_byte);
line_len_chars = self.text.line(pos.line, self.line_type()).len_chars();
let line_slice = self.text.line(pos.line, self.line_type());
printable_line_len = if line_slice.chars().last() == Some('\n') {
line_len_chars.saturating_sub(1)
} else {
line_len_chars
};
printable_line_end_char_idx = line_start_char + printable_line_len;
let mut next_char_idx = self.pos_to_char(pos);
while next_char_idx < end_char
&& self
.text
.char(self.text.char_to_byte_idx(next_char_idx))
.is_whitespace()
{
next_char_idx += 1;
if next_char_idx >= printable_line_end_char_idx {
return Position::new(pos.line, printable_line_len);
}
}
if next_char_idx >= end_char {
return pos;
}
return self.byte_to_pos(self.text.char_to_byte_idx(next_char_idx));
}
while char_idx < end_char
&& self
.text
.char(self.text.char_to_byte_idx(char_idx))
.is_whitespace()
{
char_idx += 1;
if char_idx >= printable_line_end_char_idx {
return Position::new(pos.line, printable_line_len);
}
}
if char_idx >= end_char {
return pos;
}
let first_char = self.text.char(self.text.char_to_byte_idx(char_idx));
let is_punct = first_char.is_ascii_punctuation();
while char_idx < end_char {
let c = self.text.char(self.text.char_to_byte_idx(char_idx));
if is_punct {
if !c.is_ascii_punctuation() {
break;
}
} else if c.is_whitespace() || c.is_ascii_punctuation() {
break;
}
char_idx += 1;
if char_idx >= printable_line_end_char_idx {
return Position::new(pos.line, printable_line_len);
}
}
if skip_whitespace_after {
while char_idx < end_char
&& self
.text
.char(self.text.char_to_byte_idx(char_idx))
.is_whitespace()
{
char_idx += 1;
if char_idx >= printable_line_end_char_idx {
return Position::new(pos.line, printable_line_len);
}
}
}
if char_idx >= printable_line_end_char_idx {
return Position::new(pos.line, printable_line_len);
}
self.byte_to_pos(self.text.char_to_byte_idx(char_idx))
}
pub fn select_all(&mut self) {
let last_line = self.text.len_lines(self.line_type()).saturating_sub(1);
let pos = Position::new(
last_line,
self.text.line(last_line, self.line_type()).len_chars(),
);
self.selection = Some(Selection::new(Position::new(0, 0), pos));
self.cursor = pos;
}
pub fn selected_text(&self) -> String {
if let Some(sel) = self.selection {
let start = self.pos_to_byte(sel.min());
let end = self.pos_to_byte(sel.max());
self.text.slice(start..end).into()
} else {
String::new()
}
}
pub fn delete_selection(&mut self) -> String {
if let Some(sel) = self.selection {
let start = sel.min();
let end = sel.max();
let text = self.slice(start..end);
self.delete_range(start, end);
text
} else {
String::new()
}
}
pub fn undo(&mut self) -> bool {
let inverse = {
match self.history.undo() {
Some(node) => node.inverse.clone(),
None => return false,
}
};
apply_changeset(self, &inverse);
true
}
pub fn redo(&mut self) -> bool {
let change = {
match self.history.redo() {
Some(node) => node.change.clone(),
None => return false,
}
};
apply_changeset(self, &change);
true
}
fn export_history_snapshots(&self) -> (Vec<SerializableSnapshot>, Vec<SerializableSnapshot>) {
let entries = self.history.timeline_entries();
let cursor = self.history.current_position();
let mut temp = Buffer::from_lines(self.lines(), self.path.clone());
temp.cursor = self.cursor;
temp.selection = self.selection;
temp.dirty = self.dirty;
temp.version = self.version;
let mut undo_rev: Vec<SerializableSnapshot> = Vec::new();
for i in (0..cursor).rev() {
let (change, inverse, version) = &entries[i];
temp.set_cursor(change.after_cursor);
temp.set_selection(change.after_selection);
let snap = SerializableSnapshot {
lines: temp.lines(),
cursor: change.after_cursor,
selection: change.after_selection,
dirty: temp.dirty,
version: *version,
};
undo_rev.push(snap);
apply_changeset(&mut temp, inverse);
}
let base_snap = SerializableSnapshot {
lines: temp.lines(),
cursor: temp.cursor,
selection: temp.selection,
dirty: temp.dirty,
version: temp.version,
};
undo_rev.push(base_snap);
undo_rev.reverse();
let mut redo: Vec<SerializableSnapshot> = Vec::new();
for (change, _inv, version) in entries.iter().skip(cursor) {
apply_changeset(&mut temp, change);
let snap = SerializableSnapshot {
lines: temp.lines(),
cursor: change.after_cursor,
selection: change.after_selection,
dirty: temp.dirty,
version: *version,
};
redo.push(snap);
}
(undo_rev, redo)
}
pub fn undo_stack(&self) -> Vec<SerializableSnapshot> {
let (undo, _redo) = self.export_history_snapshots();
undo
}
pub fn redo_stack(&self) -> Vec<SerializableSnapshot> {
let (_undo, redo) = self.export_history_snapshots();
redo
}
pub fn save(&mut self, settings: &crate::settings::Settings) -> Result<()> {
let path = self
.path
.clone()
.ok_or_else(|| anyhow!("No path associated with buffer"))?;
self.is_saving = true;
let result = self.do_save(&path, settings);
self.is_saving = false;
result
}
fn do_save(&mut self, path: &PathBuf, settings: &crate::settings::Settings) -> Result<()> {
let should_trim = *adapters::editor::trim_on_save(settings);
let mut text = self.text.clone();
if should_trim {
let content = text.to_string();
let had_trailing_newline = content.ends_with('\n');
let trimmed: String = content
.lines()
.map(|l| l.trim_end().to_string())
.collect::<Vec<_>>()
.join("\n");
let trailing_empty = trimmed.lines().rev().take_while(|l| l.is_empty()).count();
let final_lines: String = if trailing_empty > 1 {
trimmed
.lines()
.take(trimmed.lines().count() - (trailing_empty - 1))
.collect::<Vec<_>>()
.join("\n")
} else {
trimmed.clone()
};
let final_with_newline = if had_trailing_newline && !final_lines.ends_with('\n') {
format!("{}\n", final_lines)
} else {
final_lines
};
text = Rope::from_str(&final_with_newline);
self.text = text.clone();
self.normalize_cursor();
}
let content = {
let s = text.to_string();
let without_trailing = s.trim_end_matches('\n');
if s.ends_with('\n') {
format!("{}\n", without_trailing)
} else {
without_trailing.to_string()
}
};
let tmp = path.with_extension("tmp");
fs::write(&tmp, &content)?;
fs::rename(&tmp, path).or_else(|_| {
fs::remove_file(path)?;
fs::rename(&tmp, path)
})?;
self.file_hash = Self::compute_hash(&text);
self.dirty = false;
if let Some((mtime, size)) = Self::get_file_metadata(path) {
self.file_mtime = Some(mtime);
self.file_size = Some(size);
}
Ok(())
}
pub fn compute_hash(text: &Rope) -> Option<u64> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
text.hash(&mut hasher);
Some(hasher.finish())
}
pub fn compute_file_hash(path: &Path) -> Option<u64> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io::Read;
let mut file = fs::File::open(path).ok()?;
let mut contents = Vec::new();
file.read_to_end(&mut contents).ok()?;
let mut hasher = DefaultHasher::new();
contents.hash(&mut hasher);
Some(hasher.finish())
}
pub fn get_file_metadata(path: &Path) -> Option<(u64, u64)> {
let metadata = fs::metadata(path).ok()?;
let mtime = metadata
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
let size = metadata.len();
Some((mtime, size))
}
pub fn compute_and_store_file_hash(&mut self) {
if let Some(ref path) = self.path {
self.file_hash = Self::compute_file_hash(path);
if let Some((mtime, size)) = Self::get_file_metadata(path) {
self.file_mtime = Some(mtime);
self.file_size = Some(size);
}
}
}
pub fn check_external_modification(&mut self) -> bool {
if self.is_saving {
return false;
}
if self.is_dirty() {
return false;
}
if let Some(ref path) = self.path
&& let Some((current_mtime, current_size)) = Self::get_file_metadata(path) {
let mtime_changed = self.file_mtime.map(|m| m != current_mtime).unwrap_or(false);
let size_changed = self.file_size.map(|s| s != current_size).unwrap_or(false);
if !mtime_changed && !size_changed {
return false;
}
if let Some(current_hash) = Self::compute_file_hash(path)
&& let Some(saved_hash) = self.file_hash
&& current_hash != saved_hash {
self.external_modification_detected = true;
return true;
}
}
false
}
pub fn reload_from_disk(&mut self) -> Result<()> {
let path = self
.path
.clone()
.ok_or_else(|| anyhow!("No path associated with buffer"))?;
let content = fs::read_to_string(&path)?;
self.text = Rope::from_str(&content);
self.file_hash = Self::compute_hash(&self.text);
if let Some((mtime, size)) = Self::get_file_metadata(&path) {
self.file_mtime = Some(mtime);
self.file_size = Some(size);
}
self.external_modification_detected = false;
self.normalize_cursor();
Ok(())
}
pub fn clear_external_modification(&mut self) {
self.external_modification_detected = false;
}
pub fn scroll_to_cursor(&mut self, height: usize) {
if height == 0 {
return;
}
if self.cursor.line < self.scroll {
self.scroll = self.cursor.line;
} else if self.cursor.line >= self.scroll + height {
self.scroll = self.cursor.line + 1 - height;
}
}
pub fn scroll_to_cursor_visual(
&mut self,
height: usize,
_content_width: usize,
_indent_width: usize,
) {
self.scroll_to_cursor(height);
}
pub fn scroll_x_to_cursor(&mut self, _content_width: usize, _indent_width: usize) {}
pub fn slice(&self, range: Range<Position>) -> String {
let start_byte = self.pos_to_byte(range.start);
let end_byte = self.pos_to_byte(range.end);
self.text.slice(start_byte..end_byte).into()
}
pub fn text(&self) -> String {
self.text.to_string()
}
pub fn apply_op_without_history(&mut self, op: &BufferOp) {
match op {
BufferOp::Replace { range, text, old_text, .. } => {
let mut start_byte = self.pos_to_byte(range.start);
let end_byte = self.pos_to_byte(range.end).min(self.text.len());
let mut removed = false;
if start_byte < end_byte {
let range_len = end_byte - start_byte;
let old_len = old_text.len();
if old_len > range_len && !old_text.is_empty() {
let tail: String = self.text.slice(start_byte..self.text.len()).into();
if let Some(pos) = tail.find(old_text.as_str()) {
let found_end_byte = start_byte + pos + old_text.len();
self.text.remove(start_byte..found_end_byte);
removed = true;
} else {
self.text.remove(start_byte..end_byte);
removed = true;
}
} else {
self.text.remove(start_byte..end_byte);
removed = true;
}
} else {
if !old_text.is_empty() {
let tail: String = self.text.slice(start_byte..self.text.len()).into();
if let Some(pos) = tail.find(old_text.as_str()) {
let found_end_byte = start_byte + pos + old_text.len();
self.text.remove(start_byte..found_end_byte);
removed = true;
} else {
let whole = self.text.to_string();
if let Some(pos) = whole.find(old_text.as_str()) {
self.text.remove(pos..(pos + old_text.len()));
start_byte = pos;
removed = true;
}
}
}
}
if removed || old_text.is_empty() {
self.text.insert(start_byte, text);
} else {
}
}
BufferOp::MoveCursor { position } => {
self.set_cursor(*position);
}
BufferOp::SetSelection { selection } => {
self.set_selection(*selection);
}
}
}
}
impl Default for Buffer {
fn default() -> Self {
Self::new()
}
}
impl From<&Rope> for Buffer {
fn from(rope: &Rope) -> Self {
let mut buf = Self::new();
buf.text = rope.clone();
buf
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_position_mapping() {
let buf = Buffer::from_text("hello\nworld\n");
assert_eq!(buf.byte_to_pos(0), Position::new(0, 0));
assert_eq!(buf.byte_to_pos(5), Position::new(0, 5));
assert_eq!(buf.byte_to_pos(6), Position::new(1, 0));
assert_eq!(buf.byte_to_pos(11), Position::new(1, 5));
assert_eq!(buf.pos_to_byte(Position::new(0, 0)), 0);
assert_eq!(buf.pos_to_byte(Position::new(0, 5)), 5);
assert_eq!(buf.pos_to_byte(Position::new(1, 0)), 6);
assert_eq!(buf.pos_to_byte(Position::new(1, 5)), 11);
}
#[test]
fn test_insert() {
let mut buf = Buffer::from_text("hello");
buf.cursor = Position::new(0, 5);
buf.insert(" world");
assert_eq!(buf.text.to_string(), "hello world");
}
#[test]
fn test_insert_newline() {
let mut buf = Buffer::from_text("hello");
buf.cursor = Position::new(0, 5);
buf.insert_newline();
assert_eq!(buf.text.to_string(), "hello\n");
assert_eq!(buf.cursor, Position::new(1, 0));
}
#[test]
fn test_delete_backward() {
let mut buf = Buffer::from_text("hello world");
buf.cursor = Position::new(0, 5);
buf.delete_backward();
assert_eq!(buf.text.to_string(), "hell world");
assert_eq!(buf.cursor, Position::new(0, 4));
}
#[test]
fn test_delete_selection() {
let mut buf = Buffer::from_text("hello world");
buf.selection = Some(Selection::new(Position::new(0, 0), Position::new(0, 6)));
buf.delete_selection();
assert_eq!(buf.text.to_string(), "world");
}
#[test]
fn test_cursor_movement() {
let mut buf = Buffer::from_text("hello\nworld");
buf.cursor = Position::new(0, 3);
assert_eq!(buf.cursor_right(), Position::new(0, 4));
assert_eq!(buf.cursor_left(), Position::new(0, 3));
buf.cursor = Position::new(0, 0);
assert_eq!(buf.cursor_up(), Position::new(0, 0));
buf.cursor = Position::new(0, 3);
buf.cursor_up();
assert_eq!(buf.cursor.line, 0);
buf.cursor = Position::new(1, 3);
buf.cursor_up();
assert_eq!(buf.cursor.line, 0);
assert_eq!(buf.cursor.column, 3);
}
#[test]
fn test_undo_multiple_steps() {
let mut buf = Buffer::from_text("");
buf.insert("a");
buf.insert("b");
buf.insert("c");
assert_eq!(buf.text(), "abc");
assert_eq!(buf.history_position(), 1);
assert!(buf.undo());
assert_eq!(buf.text(), "");
assert!(!buf.undo());
}
#[test]
fn test_undo_stack_persist_and_restore() {
use std::path::PathBuf;
let mut buf = Buffer::from_text_no_merge("");
buf.begin_transaction(ChangeKind::InsertText);
buf.insert("a");
buf.end_transaction();
buf.begin_transaction(ChangeKind::InsertText);
buf.insert("b");
buf.end_transaction();
assert_eq!(buf.text(), "ab");
let undo_stack = buf.undo_stack();
let redo_stack = buf.redo_stack();
let restored = Buffer::restore(
PathBuf::from("dummy"),
buf.lines(),
buf.selection,
buf.scroll,
buf.dirty,
buf.markers.clone(),
undo_stack.clone(),
redo_stack.clone(),
);
let mut r = restored;
assert!(r.undo());
assert_eq!(r.text(), "a");
assert!(r.undo());
assert_eq!(r.text(), "");
}
#[test]
fn test_reopen_and_excess_undos_do_not_duplicate_tail() {
use tempfile::tempdir;
use crate::project::Project;
use crate::editor::fold::FoldState;
use crate::editor::history::ChangeKind;
use std::fs;
let tmp = tempdir().expect("tempdir");
let project_path = tmp.path().to_path_buf();
let file_a = project_path.join("a.txt");
let file_b = project_path.join("b.txt");
fs::write(&file_a, "alpha\nbeta\n").expect("write a");
fs::write(&file_b, "other\n").expect("write b");
let mut project = Project::new(project_path).expect("project new");
let mut buf_a = project.take_buffer(file_a.clone()).expect("take A");
buf_a.begin_transaction(ChangeKind::InsertText);
buf_a.insert_text_raw("// edit1\n");
buf_a.end_transaction();
buf_a.begin_transaction(ChangeKind::InsertText);
buf_a.insert_text_raw("// edit2\n");
buf_a.end_transaction();
project.stash_buffer(buf_a, FoldState::default());
let buf_b = project.take_buffer(file_b.clone()).expect("take B");
project.stash_buffer(buf_b, FoldState::default());
let mut buf_a2 = project.take_buffer(file_a.clone()).expect("reopen A");
let undo_snapshots = buf_a2.undo_stack().len();
for _ in 0..(undo_snapshots + 3) {
let _ = buf_a2.undo();
}
let expected = fs::read_to_string(&file_a).expect("read file A");
assert_eq!(buf_a2.text(), expected);
}
#[test]
fn test_undo_redo_multiple_steps() {
let mut buf = Buffer::from_text("");
buf.insert("a");
buf.insert("b");
assert_eq!(buf.text(), "ab");
assert_eq!(buf.history_position(), 1);
buf.undo();
assert_eq!(buf.text(), "");
buf.redo();
assert_eq!(buf.text(), "ab");
assert!(!buf.redo());
}
#[test]
fn test_undo_after_new_edit() {
let mut buf = Buffer::from_text("");
buf.insert("a");
buf.insert("b");
buf.undo();
assert_eq!(buf.text(), "");
buf.insert("c");
assert_eq!(buf.text(), "c");
buf.undo();
assert_eq!(buf.text(), "");
}
#[test]
fn test_line_access() {
let buf = Buffer::from_text("hello\nworld\n");
assert_eq!(buf.line_count(), 3);
assert_eq!(buf.line(0).unwrap().to_string(), "hello");
assert_eq!(buf.line(1).unwrap().to_string(), "world");
}
#[test]
fn test_cursor_clamping() {
let mut buf = Buffer::from_text("hello");
buf.cursor = Position::new(100, 100);
buf.normalize_cursor();
assert!(buf.cursor.line < buf.line_count());
assert!(buf.cursor.column <= buf.line(0).unwrap().chars().count());
}
#[test]
fn test_unicode() {
let mut buf = Buffer::from_text("héllo\nwörld");
assert_eq!(buf.line_count(), 2);
buf.cursor = Position::new(0, 6);
buf.insert("!");
let line0 = buf.text.line(0, LineType::LF).to_string();
assert!(
line0.starts_with("héllo"),
"line0 should start with héllo, got: {}",
line0
);
}
#[test]
fn test_snapshot() {
let buf = Buffer::from_text("hello");
let snap = buf.snapshot();
assert_eq!(snap.text.to_string(), "hello");
}
#[test]
fn test_delete_forward() {
let mut buf = Buffer::from_text("hello");
buf.cursor = Position::new(0, 4);
buf.delete_forward();
assert_eq!(buf.text.to_string(), "hell");
}
#[test]
fn test_empty_buffer() {
let buf = Buffer::new();
assert!(buf.is_empty());
assert_eq!(buf.line_count(), 1);
assert_eq!(buf.cursor, Position::new(0, 0));
}
#[test]
fn test_buffer_id() {
let buf1 = Buffer::new();
let buf2 = Buffer::new();
assert_ne!(buf1.id(), buf2.id());
}
#[test]
fn test_version_increment() {
let mut buf = Buffer::from_text("hello");
let v1 = buf.version();
buf.insert(" world");
let v2 = buf.version();
assert!(v2 > v1);
}
#[test]
fn test_fast_typing_merges_into_single_undo() {
let mut buf = Buffer::from_text("");
buf.insert("h");
buf.insert("e");
buf.insert("l");
buf.insert("l");
buf.insert("o");
assert_eq!(buf.text(), "hello");
assert_eq!(buf.history_position(), 1);
buf.undo();
assert_eq!(buf.text(), "");
}
#[test]
fn test_fast_backspace_merges_into_single_undo() {
let mut buf = Buffer::from_text("hello");
buf.set_cursor(Position::new(0, 5));
buf.delete_backward();
buf.delete_backward();
buf.delete_backward();
assert_eq!(buf.text(), "he");
assert_eq!(buf.history_position(), 1);
buf.undo();
assert_eq!(buf.text(), "hello");
}
#[test]
fn test_structural_ops_break_merge_chain() {
let mut buf = Buffer::from_text("");
buf.insert("a");
buf.insert("b"); buf.insert_newline(); buf.insert("c");
assert_eq!(buf.text(), "ab\nc");
assert_eq!(buf.history_position(), 3);
buf.undo();
assert_eq!(buf.text(), "ab\n");
buf.undo();
assert_eq!(buf.text(), "ab");
buf.undo();
assert_eq!(buf.text(), "");
buf.undo();
assert_eq!(buf.text(), "");
}
#[test]
fn test_mixed_undo_steps() {
let mut buf = Buffer::from_text("");
buf.insert("hello");
buf.insert_newline();
buf.insert("world");
buf.undo();
assert_eq!(buf.text(), "hello\n");
buf.undo();
assert_eq!(buf.text(), "hello");
buf.undo();
assert_eq!(buf.text(), "");
}
#[test]
fn test_cross_line_insert_over_selection() {
let mut buf = Buffer::from_text("hello\nworld");
buf.set_selection(Some(Selection::new(
Position::new(0, 2),
Position::new(1, 3),
)));
buf.insert("X");
assert_eq!(buf.text(), "heXld");
}
#[test]
fn test_cross_line_delete_backward_with_selection() {
let mut buf = Buffer::from_text("hello\nworld");
buf.set_selection(Some(Selection::new(
Position::new(0, 5),
Position::new(1, 0),
)));
buf.delete_backward();
assert_eq!(buf.text(), "helloworld");
}
#[test]
fn test_cross_line_selection_delete_all() {
let mut buf = Buffer::from_text("abc\ndef\nghi");
buf.set_selection(Some(Selection::new(
Position::new(0, 0),
Position::new(2, 3),
)));
buf.insert("X");
assert_eq!(buf.text(), "X");
}
#[test]
fn test_cross_line_selection_undo_redo() {
let mut buf = Buffer::from_text("hello\nworld");
buf.set_selection(Some(Selection::new(
Position::new(0, 3),
Position::new(1, 2),
)));
buf.insert("X");
assert_eq!(buf.text(), "helXrld");
assert_eq!(buf.cursor(), Position::new(0, 4)); buf.undo();
assert_eq!(buf.text(), "hello\nworld");
assert_eq!(buf.cursor(), Position::new(1, 2));
buf.redo();
assert_eq!(buf.text(), "helXrld");
assert_eq!(buf.cursor(), Position::new(0, 4));
}
#[test]
fn test_offset_up_excludes_trailing_newline() {
let buf = Buffer::from_text("hello\nworld");
let pos = buf.offset_up(Position::new(1, 5));
assert_eq!(pos, Position::new(0, 5));
}
#[test]
fn test_offset_up_clamps_to_shorter_line() {
let buf = Buffer::from_text("hi\nworld");
let pos = buf.offset_up(Position::new(1, 5));
assert_eq!(pos, Position::new(0, 2));
}
#[test]
fn test_delete_forward_multibyte() {
let mut buf = Buffer::from_text("αβγ");
buf.set_cursor(Position::new(0, 1));
buf.delete_forward();
assert_eq!(buf.text(), "αγ");
}
#[test]
fn test_delete_forward_newline() {
let mut buf = Buffer::from_text("abc\ndef");
buf.set_cursor(Position::new(0, 3));
buf.delete_forward();
assert_eq!(buf.text(), "abcdef");
}
#[test]
fn test_pos_to_char_and_char_to_pos() {
let buf = Buffer::from_text("hello\nworld");
assert_eq!(buf.pos_to_char(Position::new(0, 3)), 3);
assert_eq!(buf.pos_to_char(Position::new(1, 2)), 8);
assert_eq!(buf.char_to_pos(3), Position::new(0, 3));
assert_eq!(buf.char_to_pos(8), Position::new(1, 2));
}
#[test]
fn test_set_selection_normalizes_positions() {
let mut buf = Buffer::from_text("hi\nworld");
buf.set_selection(Some(Selection::new(
Position::new(0, 100), Position::new(1, 3),
)));
let sel = buf.selection().unwrap();
assert_eq!(sel.anchor, Position::new(0, 2));
assert_eq!(sel.active, Position::new(1, 3));
}
#[test]
fn test_newline_undo_roundtrip() {
let mut buf = Buffer::from_text("helloworld");
buf.set_cursor(Position::new(0, 5));
buf.insert_newline();
assert_eq!(buf.text(), "hello\nworld");
assert_eq!(buf.cursor(), Position::new(1, 0));
buf.undo();
assert_eq!(buf.text(), "helloworld");
assert_eq!(buf.cursor(), Position::new(0, 5));
}
#[test]
fn test_insert_at_advances_cursor_when_at_position() {
let mut buf = Buffer::from_text("hello");
buf.set_cursor(Position::new(0, 5));
buf.insert_at(Position::new(0, 5), " world");
assert_eq!(buf.text(), "hello world");
assert_eq!(buf.cursor(), Position::new(0, 11));
}
#[test]
fn test_insert_at_does_not_move_cursor_when_before_cursor() {
let mut buf = Buffer::from_text("world");
buf.set_cursor(Position::new(0, 5));
buf.insert_at(Position::new(0, 0), "hello ");
assert_eq!(buf.text(), "hello world");
assert_eq!(buf.cursor(), Position::new(0, 5));
}
#[test]
fn test_selected_text_single_line() {
let mut buf = Buffer::from_text("hello world");
buf.set_selection(Some(Selection {
anchor: Position::new(0, 0),
active: Position::new(0, 5),
}));
assert_eq!(buf.selected_text(), "hello");
}
#[test]
fn test_selected_text_multiline() {
let mut buf = Buffer::from_text("hello\nworld");
buf.set_selection(Some(Selection {
anchor: Position::new(0, 3),
active: Position::new(1, 3),
}));
assert_eq!(buf.selected_text(), "lo\nwor");
}
#[test]
fn test_selected_text_empty_when_no_selection() {
let buf = Buffer::from_text("hello");
assert_eq!(buf.selected_text(), "");
}
#[test]
fn test_word_range_at_middle_of_word() {
let buf = Buffer::from_text("hello world");
let (start, end) = buf.word_range_at(Position::new(0, 2));
assert_eq!(start, 2);
assert_eq!(end, 5);
}
#[test]
fn test_word_range_at_on_space() {
let buf = Buffer::from_text("hello world");
let _ = buf.word_range_at(Position::new(0, 5));
}
#[test]
fn test_delete_range_same_position_is_noop() {
let mut buf = Buffer::from_text("hello");
let pos = Position::new(0, 2);
buf.delete_range(pos, pos);
assert_eq!(buf.text(), "hello");
}
#[test]
fn test_char_count() {
let buf = Buffer::from_text("hello\nworld");
assert_eq!(buf.char_count(), 11);
}
#[test]
fn test_current_line() {
let mut buf = Buffer::from_text("hello\nworld");
buf.set_cursor(Position::new(1, 0));
assert_eq!(buf.current_line(), 1);
}
#[test]
fn test_slice() {
let buf = Buffer::from_text("hello world");
let s = buf.slice(Position::new(0, 0)..Position::new(0, 5));
assert_eq!(s, "hello");
}
#[test]
fn test_history_len() {
let mut buf = Buffer::from_text("");
assert_eq!(buf.history_len(), 0);
buf.insert("a");
assert!(buf.history_len() > 0);
}
#[test]
fn test_scroll_to_cursor_scrolls_down_when_cursor_below_view() {
let mut buf = Buffer::from_text("a\nb\nc\nd\ne");
buf.set_cursor(Position::new(4, 0));
buf.scroll_to_cursor(3);
assert!(buf.scroll >= 2);
}
#[test]
fn test_scroll_to_cursor_scrolls_up_when_cursor_above_view() {
let mut buf = Buffer::from_text("a\nb\nc\nd\ne");
buf.scroll = 4;
buf.set_cursor(Position::new(0, 0));
buf.scroll_to_cursor(3);
assert_eq!(buf.scroll, 0);
}
#[test]
fn test_offset_down_clamps_column_to_shorter_line() {
let buf = Buffer::from_text("hello world\nhi");
let pos = buf.offset_down(Position::new(0, 8));
assert_eq!(pos, Position::new(1, 2));
}
#[test]
fn test_offset_down_at_last_line_stays() {
let buf = Buffer::from_text("hello");
let pos = buf.offset_down(Position::new(0, 3));
assert_eq!(pos, Position::new(0, 3));
}
#[test]
fn test_offset_down_n_stops_at_last_line() {
let buf = Buffer::from_text("a\nb\nc");
let pos = buf.offset_down_n(Position::new(0, 0), 100);
assert_eq!(pos.line, 2);
}
#[test]
fn test_offset_left_wraps_to_end_of_previous_line() {
let buf = Buffer::from_text("hello\nworld");
let pos = buf.offset_left(Position::new(1, 0));
assert_eq!(pos, Position::new(0, 5));
}
#[test]
fn test_offset_right_wraps_to_start_of_next_line() {
let buf = Buffer::from_text("hello\nworld");
let pos = buf.offset_right(Position::new(0, 5));
assert_eq!(pos, Position::new(1, 0));
}
#[test]
fn test_insert_text_raw_normalizes_crlf() {
let mut buf = Buffer::from_text("");
buf.insert_text_raw("hello\r\nworld");
assert_eq!(buf.text(), "hello\nworld");
}
#[test]
fn test_insert_text_raw_normalizes_cr_only() {
let mut buf = Buffer::from_text("");
buf.insert_text_raw("hello\rworld");
assert_eq!(buf.text(), "hello\nworld");
}
#[test]
fn test_replace_range_same_text_at_cursor_is_noop_for_history() {
let mut buf = Buffer::from_text("hello");
buf.set_cursor(Position::new(0, 0));
let before_history_pos = buf.history_position();
buf.replace_range(Position::new(0, 0), Position::new(0, 5), "hello");
assert_eq!(buf.history_position(), before_history_pos);
assert_eq!(buf.text(), "hello");
}
#[test]
fn test_delete_word_forward_from_whitespace() {
let mut buf = Buffer::from_text("hello world");
buf.set_cursor(Position::new(0, 5));
buf.delete_word_forward();
assert_eq!(buf.text(), "helloworld");
}
#[test]
fn test_delete_line_last_line_leaves_empty() {
let mut buf = Buffer::from_text("hello");
buf.set_cursor(Position::new(0, 0));
buf.delete_line();
assert_eq!(buf.line_count(), 1);
assert_eq!(buf.line(0).unwrap(), "");
}
fn settings_with_trim(trim: bool) -> (crate::settings::Settings, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let project_config = dir.path().join(".oo").join("config.yaml");
std::fs::create_dir(dir.path().join(".oo")).unwrap();
let override_yaml = format!("editor:\n trim_on_save: {}\n", trim);
std::fs::write(&project_config, &override_yaml).unwrap();
let s = crate::settings::Settings::new(&project_config).unwrap();
(s, dir)
}
fn save_content(content: &str, trim: bool) -> String {
let (settings, dir) = settings_with_trim(trim);
let file = dir.path().join("test.txt");
let mut buf = Buffer::from_text(content);
buf.path = Some(file.clone());
buf.save(&settings).expect("save should succeed");
std::fs::read_to_string(&file).expect("file should exist after save")
}
#[test]
fn save_no_trailing_newline_is_preserved() {
let on_disk = save_content("hello\nworld", false);
assert_eq!(on_disk, "hello\nworld");
}
#[test]
fn save_single_trailing_newline_is_preserved() {
let on_disk = save_content("hello\nworld\n", false);
assert_eq!(on_disk, "hello\nworld\n");
}
#[test]
fn save_multiple_trailing_newlines_collapsed_to_one() {
let on_disk = save_content("hello\nworld\n\n", false);
assert_eq!(on_disk, "hello\nworld\n");
let on_disk = save_content("hello\nworld\n\n\n", false);
assert_eq!(on_disk, "hello\nworld\n");
}
#[test]
fn save_trim_on_save_preserves_trailing_newline() {
let on_disk = save_content("hello \nworld \n", true);
assert_eq!(on_disk, "hello\nworld\n");
}
#[test]
fn save_trim_on_save_no_trailing_newline_not_added() {
let on_disk = save_content("hello \nworld ", true);
assert_eq!(on_disk, "hello\nworld");
}
#[test]
fn save_trim_on_save_collapses_extra_trailing_newlines() {
let on_disk = save_content("hello\nworld\n\n\n", true);
assert_eq!(on_disk, "hello\nworld\n");
}
#[test]
fn restore_clamps_stale_cursor_to_shorter_content() {
use crate::editor::position::Position;
use crate::editor::selection::Selection;
let stale_selection = Some(Selection {
anchor: Position::new(10, 3),
active: Position::new(10, 5),
});
let short_lines = vec!["line0".to_owned(), "line1".to_owned(), "line2".to_owned()];
let buf = Buffer::restore(
std::path::PathBuf::from("/fake/path.rs"),
short_lines,
stale_selection,
0,
true, vec![],
vec![],
vec![],
);
assert!(buf.cursor().line < 3, "cursor.line OOB: {}", buf.cursor().line);
let _ = buf.offset_right(buf.cursor());
let _ = buf.offset_down(buf.cursor());
let _ = buf.offset_line_end(buf.cursor());
}
}