use std::path::{Path, PathBuf};
use miette::Result;
use mq_markdown::{Markdown, Node};
use unicode_width::UnicodeWidthChar;
use super::history::{EditAction, EditHistory};
use super::{Cursor, CursorMovement, DocumentType, FileType, LineMap};
#[derive(Debug, Clone)]
pub struct DocumentBuffer {
document_type: DocumentType,
file_type: FileType,
file_path: Option<PathBuf>,
cursor: Cursor,
lines: Vec<String>,
modified: bool,
history: EditHistory,
recording: bool,
}
impl DocumentBuffer {
pub fn new() -> Self {
let document_type = DocumentType::new_markdown("").unwrap_or_else(|_| {
DocumentType::new_markdown(" ").expect("Failed to create empty markdown")
});
Self {
document_type,
file_type: FileType::Markdown,
file_path: None,
cursor: Cursor::new(),
lines: vec![String::new()],
modified: false,
history: EditHistory::new(),
recording: true,
}
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)
.map_err(|e| miette::miette!("Failed to read file: {}", e))?;
let file_type = FileType::from_path(path);
let document_type = match &file_type {
FileType::Markdown => DocumentType::new_markdown(&content)?,
FileType::Code(lang) => DocumentType::new_code(lang.clone()),
FileType::PlainText => DocumentType::new_plain_text(),
};
let lines = Self::extract_lines(&content);
Ok(Self {
document_type,
file_type,
file_path: Some(path.to_path_buf()),
cursor: Cursor::new(),
lines,
modified: false,
history: EditHistory::new(),
recording: true,
})
}
pub fn from_string(content: &str) -> Result<Self> {
let document_type = DocumentType::new_markdown(content)?;
let lines = Self::extract_lines(content);
Ok(Self {
document_type,
file_type: FileType::Markdown,
file_path: None,
cursor: Cursor::new(),
lines,
modified: false,
history: EditHistory::new(),
recording: true,
})
}
fn extract_lines(content: &str) -> Vec<String> {
if content.is_empty() {
vec![String::new()]
} else {
content.lines().map(|s| s.to_string()).collect()
}
}
pub fn cursor(&self) -> &Cursor {
&self.cursor
}
pub fn cursor_mut(&mut self) -> &mut Cursor {
&mut self.cursor
}
pub fn file_path(&self) -> Option<&Path> {
self.file_path.as_deref()
}
pub fn is_modified(&self) -> bool {
self.modified
}
pub fn file_type(&self) -> &FileType {
&self.file_type
}
pub fn document_type(&self) -> &DocumentType {
&self.document_type
}
pub fn markdown(&self) -> Option<&Markdown> {
self.document_type.markdown_ast()
}
pub fn line_map(&self) -> Option<&LineMap> {
self.document_type.line_map()
}
pub fn lines(&self) -> &[String] {
&self.lines
}
pub fn line(&self, index: usize) -> Option<&str> {
self.lines.get(index).map(|s| s.as_str())
}
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn node_at_cursor(&self) -> Option<&Node> {
match &self.document_type {
DocumentType::Markdown { ast, line_map } => {
line_map.get_node_at_line(ast, self.cursor.line)
}
_ => None,
}
}
fn char_to_byte_idx(&self, line_idx: usize, char_pos: usize) -> usize {
if let Some(line) = self.line(line_idx) {
line.char_indices()
.nth(char_pos)
.map(|(byte_idx, _)| byte_idx)
.unwrap_or(line.len())
} else {
0
}
}
fn line_char_count(&self, line_idx: usize) -> usize {
self.line(line_idx).map(|s| s.chars().count()).unwrap_or(0)
}
pub fn move_cursor(&mut self, movement: CursorMovement) {
match movement {
CursorMovement::Up => {
if self.cursor.line > 0 {
self.cursor.line -= 1;
self.clamp_cursor_column();
}
}
CursorMovement::Down => {
if self.cursor.line + 1 < self.line_count() {
self.cursor.line += 1;
self.clamp_cursor_column();
}
}
CursorMovement::Left => {
if self.cursor.column > 0 {
self.cursor.column -= 1;
self.cursor.update_desired_column();
} else if self.cursor.line > 0 {
self.cursor.line -= 1;
self.cursor.column = self.line_char_count(self.cursor.line);
self.cursor.update_desired_column();
}
}
CursorMovement::Right => {
let line_len = self.line_char_count(self.cursor.line);
if self.cursor.column < line_len {
self.cursor.column += 1;
self.cursor.update_desired_column();
} else if self.cursor.line + 1 < self.line_count() {
self.cursor.line += 1;
self.cursor.column = 0;
self.cursor.update_desired_column();
}
}
CursorMovement::StartOfLine => {
self.cursor.column = 0;
self.cursor.update_desired_column();
}
CursorMovement::EndOfLine => {
self.cursor.column = self.line_char_count(self.cursor.line);
self.cursor.update_desired_column();
}
CursorMovement::PageUp => {
self.cursor.line = self.cursor.line.saturating_sub(20);
self.clamp_cursor_column();
}
CursorMovement::PageDown => {
self.cursor.line = (self.cursor.line + 20).min(self.line_count().saturating_sub(1));
self.clamp_cursor_column();
}
CursorMovement::StartOfDocument => {
self.cursor.line = 0;
self.cursor.column = 0;
self.cursor.update_desired_column();
}
CursorMovement::EndOfDocument => {
self.cursor.line = self.line_count().saturating_sub(1);
self.cursor.column = self.line_char_count(self.cursor.line);
self.cursor.update_desired_column();
}
}
}
fn clamp_cursor_column(&mut self) {
let line_len = self.line_char_count(self.cursor.line);
self.cursor.column = self.cursor.desired_column.min(line_len);
}
pub fn insert_char(&mut self, c: char) {
if self.recording {
let cursor_before = self.cursor;
self.history.push(
EditAction::InsertChar {
line: self.cursor.line,
column: self.cursor.column,
c,
},
cursor_before,
);
}
let byte_idx = self.char_to_byte_idx(self.cursor.line, self.cursor.column);
if let Some(line) = self.lines.get_mut(self.cursor.line) {
line.insert(byte_idx, c);
self.cursor.column += 1;
self.cursor.update_desired_column();
self.modified = true;
self.rebuild_document();
}
}
pub fn insert_str(&mut self, s: &str) {
if s.is_empty() {
return;
}
if self.recording {
let cursor_before = self.cursor;
self.history.push(
EditAction::InsertStr {
line: self.cursor.line,
column: self.cursor.column,
text: s.to_string(),
},
cursor_before,
);
}
let lines_to_insert: Vec<&str> = s.split('\n').collect();
if lines_to_insert.len() == 1 {
let byte_idx = self.char_to_byte_idx(self.cursor.line, self.cursor.column);
if let Some(line) = self.lines.get_mut(self.cursor.line) {
line.insert_str(byte_idx, s);
self.cursor.column += s.chars().count();
self.cursor.update_desired_column();
self.modified = true;
self.rebuild_document();
}
} else {
let byte_idx = self.char_to_byte_idx(self.cursor.line, self.cursor.column);
if let Some(current_line) = self.lines.get_mut(self.cursor.line) {
let rest = current_line.split_off(byte_idx);
current_line.push_str(lines_to_insert[0]);
for (i, &line) in lines_to_insert[1..lines_to_insert.len() - 1]
.iter()
.enumerate()
{
self.lines
.insert(self.cursor.line + 1 + i, line.to_string());
}
let last_line_idx = self.cursor.line + lines_to_insert.len() - 1;
let mut last_line = lines_to_insert.last().unwrap().to_string();
let new_cursor_column = last_line.chars().count();
last_line.push_str(&rest);
self.lines.insert(last_line_idx, last_line);
self.cursor.line = last_line_idx;
self.cursor.column = new_cursor_column;
self.cursor.update_desired_column();
self.modified = true;
self.rebuild_document();
}
}
}
pub fn delete_range(&mut self, start_col: usize) {
let end_col = self.cursor.column;
if start_col >= end_col {
return;
}
let line_idx = self.cursor.line;
if line_idx >= self.lines.len() {
return;
}
let start_byte = self.char_to_byte_idx(line_idx, start_col);
let end_byte = self.char_to_byte_idx(line_idx, end_col);
if end_byte <= self.lines[line_idx].len() {
if self.recording {
let cursor_before = self.cursor;
let deleted = self.lines[line_idx][start_byte..end_byte].to_string();
self.history.push(
EditAction::DeleteRange {
line: line_idx,
start_col,
deleted,
},
cursor_before,
);
}
self.lines[line_idx].replace_range(start_byte..end_byte, "");
self.cursor.column = start_col;
self.cursor.update_desired_column();
self.modified = true;
self.rebuild_document();
}
}
pub fn delete_char(&mut self) {
if self.cursor.column > 0 {
let cursor_before = self.cursor;
self.cursor.column -= 1;
let byte_idx = self.char_to_byte_idx(self.cursor.line, self.cursor.column);
if let Some(line) = self.lines.get_mut(self.cursor.line) {
if let Some((_, ch)) = line[byte_idx..].char_indices().next() {
if self.recording {
self.history.push(
EditAction::DeleteChar {
line: self.cursor.line,
column: self.cursor.column,
deleted: ch,
},
cursor_before,
);
}
line.replace_range(byte_idx..byte_idx + ch.len_utf8(), "");
}
self.cursor.update_desired_column();
self.modified = true;
self.rebuild_document();
}
} else if self.cursor.line > 0 {
if self.recording {
let cursor_before = self.cursor;
self.history.push(
EditAction::JoinLines {
line: self.cursor.line,
column: self.lines[self.cursor.line - 1].chars().count(),
},
cursor_before,
);
}
let current_line = self.lines.remove(self.cursor.line);
self.cursor.line -= 1;
self.cursor.column = self.lines[self.cursor.line].chars().count();
self.lines[self.cursor.line].push_str(¤t_line);
self.cursor.update_desired_column();
self.modified = true;
self.rebuild_document();
}
}
pub fn insert_newline(&mut self) {
if self.recording {
let cursor_before = self.cursor;
self.history.push(
EditAction::InsertNewline {
line: self.cursor.line,
column: self.cursor.column,
},
cursor_before,
);
}
let byte_idx = self.char_to_byte_idx(self.cursor.line, self.cursor.column);
if let Some(line) = self.lines.get_mut(self.cursor.line) {
let rest = line.split_off(byte_idx);
self.cursor.line += 1;
self.lines.insert(self.cursor.line, rest);
self.cursor.column = 0;
self.cursor.update_desired_column();
self.modified = true;
self.rebuild_document();
}
}
fn rebuild_document(&mut self) {
let content = self.lines.join("\n");
let _ = self.document_type.rebuild(&content);
}
pub fn undo(&mut self) {
if let Some(entry) = self.history.undo() {
self.recording = false;
self.apply_reverse(&entry.action);
self.cursor = entry.cursor_before;
self.cursor.update_desired_column();
self.modified = true;
self.rebuild_document();
self.recording = true;
}
}
pub fn redo(&mut self) {
if let Some(entry) = self.history.redo() {
self.recording = false;
self.apply_forward(&entry.action);
self.modified = true;
self.rebuild_document();
self.recording = true;
}
}
fn apply_reverse(&mut self, action: &EditAction) {
match action {
EditAction::InsertChar { line, column, .. } => {
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(line_content) = self.lines.get_mut(*line)
&& let Some((_, ch)) = line_content[byte_idx..].char_indices().next()
{
line_content.replace_range(byte_idx..byte_idx + ch.len_utf8(), "");
}
}
EditAction::InsertStr { line, column, text } => {
let inserted_lines: Vec<&str> = text.split('\n').collect();
if inserted_lines.len() == 1 {
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(line_content) = self.lines.get_mut(*line) {
let end_byte = byte_idx + text.len();
if end_byte <= line_content.len() {
line_content.replace_range(byte_idx..end_byte, "");
}
}
} else {
let first_line_byte = self.char_to_byte_idx(*line, *column);
let last_inserted_line_idx = *line + inserted_lines.len() - 1;
let last_inserted_chars = inserted_lines.last().unwrap().chars().count();
let last_line_rest_byte =
self.char_to_byte_idx(last_inserted_line_idx, last_inserted_chars);
let before = self.lines[*line][..first_line_byte].to_string();
let after =
self.lines[last_inserted_line_idx][last_line_rest_byte..].to_string();
for _ in 0..inserted_lines.len() - 1 {
if *line + 1 < self.lines.len() {
self.lines.remove(*line + 1);
}
}
self.lines[*line] = format!("{}{}", before, after);
}
}
EditAction::InsertNewline { line, column } => {
if *line + 1 < self.lines.len() {
let next_line = self.lines.remove(*line + 1);
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(line_content) = self.lines.get_mut(*line) {
line_content.truncate(byte_idx);
line_content.push_str(&next_line);
}
}
}
EditAction::DeleteChar {
line,
column,
deleted,
} => {
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(line_content) = self.lines.get_mut(*line) {
line_content.insert(byte_idx, *deleted);
}
}
EditAction::JoinLines { line, column } => {
let byte_idx = self.char_to_byte_idx(*line - 1, *column);
if let Some(prev_line) = self.lines.get_mut(*line - 1) {
let rest = prev_line.split_off(byte_idx);
self.lines.insert(*line, rest);
}
}
EditAction::DeleteRange {
line,
start_col,
deleted,
} => {
let byte_idx = self.char_to_byte_idx(*line, *start_col);
if let Some(line_content) = self.lines.get_mut(*line) {
line_content.insert_str(byte_idx, deleted);
}
}
EditAction::ReplaceAt {
line,
column,
old_text,
new_text,
} => {
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(line_content) = self.lines.get_mut(*line) {
let end_byte = byte_idx + new_text.len();
if end_byte <= line_content.len() {
line_content.replace_range(byte_idx..end_byte, old_text);
}
}
}
}
}
fn apply_forward(&mut self, action: &EditAction) {
match action {
EditAction::InsertChar { line, column, c } => {
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(line_content) = self.lines.get_mut(*line) {
line_content.insert(byte_idx, *c);
}
self.cursor.line = *line;
self.cursor.column = *column + 1;
self.cursor.update_desired_column();
}
EditAction::InsertStr { line, column, text } => {
let inserted_lines: Vec<&str> = text.split('\n').collect();
if inserted_lines.len() == 1 {
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(line_content) = self.lines.get_mut(*line) {
line_content.insert_str(byte_idx, text);
}
self.cursor.line = *line;
self.cursor.column = *column + text.chars().count();
} else {
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(current_line) = self.lines.get_mut(*line) {
let rest = current_line.split_off(byte_idx);
current_line.push_str(inserted_lines[0]);
for (i, &il) in inserted_lines[1..inserted_lines.len() - 1]
.iter()
.enumerate()
{
self.lines.insert(*line + 1 + i, il.to_string());
}
let last_line_idx = *line + inserted_lines.len() - 1;
let mut last_line = inserted_lines.last().unwrap().to_string();
let new_cursor_column = last_line.chars().count();
last_line.push_str(&rest);
self.lines.insert(last_line_idx, last_line);
self.cursor.line = last_line_idx;
self.cursor.column = new_cursor_column;
}
}
self.cursor.update_desired_column();
}
EditAction::InsertNewline { line, column } => {
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(line_content) = self.lines.get_mut(*line) {
let rest = line_content.split_off(byte_idx);
self.lines.insert(*line + 1, rest);
}
self.cursor.line = *line + 1;
self.cursor.column = 0;
self.cursor.update_desired_column();
}
EditAction::DeleteChar {
line,
column,
deleted,
} => {
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(line_content) = self.lines.get_mut(*line) {
line_content.replace_range(byte_idx..byte_idx + deleted.len_utf8(), "");
}
self.cursor.line = *line;
self.cursor.column = *column;
self.cursor.update_desired_column();
}
EditAction::JoinLines { line, column } => {
let current_line = self.lines.remove(*line);
self.lines[*line - 1].push_str(¤t_line);
self.cursor.line = *line - 1;
self.cursor.column = *column;
self.cursor.update_desired_column();
}
EditAction::DeleteRange {
line,
start_col,
deleted,
} => {
let start_byte = self.char_to_byte_idx(*line, *start_col);
let end_byte = start_byte + deleted.len();
if end_byte <= self.lines[*line].len() {
self.lines[*line].replace_range(start_byte..end_byte, "");
}
self.cursor.line = *line;
self.cursor.column = *start_col;
self.cursor.update_desired_column();
}
EditAction::ReplaceAt {
line,
column,
old_text,
new_text,
} => {
let byte_idx = self.char_to_byte_idx(*line, *column);
if let Some(line_content) = self.lines.get_mut(*line) {
let end_byte = byte_idx + old_text.len();
if end_byte <= line_content.len() {
line_content.replace_range(byte_idx..end_byte, new_text);
}
}
}
}
}
pub fn save(&mut self) -> Result<()> {
if let Some(path) = &self.file_path {
let content = self.lines.join("\n");
std::fs::write(path, content)
.map_err(|e| miette::miette!("Failed to write file: {}", e))?;
self.modified = false;
Ok(())
} else {
Err(miette::miette!("No file path set"))
}
}
pub fn save_as(&mut self, path: impl AsRef<Path>) -> Result<()> {
let content = self.lines.join("\n");
std::fs::write(path.as_ref(), content)
.map_err(|e| miette::miette!("Failed to write file: {}", e))?;
self.file_path = Some(path.as_ref().to_path_buf());
self.modified = false;
Ok(())
}
pub fn content(&self) -> String {
self.lines.join("\n")
}
pub fn word_start_column(&self, line: usize, column: usize) -> usize {
if let Some(line_content) = self.line(line) {
let chars: Vec<char> = line_content.chars().collect();
if column == 0 || chars.is_empty() {
return column;
}
let mut start = column.min(chars.len());
while start > 0 {
let c = chars[start - 1];
if c.is_alphanumeric() || c == '_' {
start -= 1;
} else {
break;
}
}
start
} else {
column
}
}
pub fn display_width_to_column(&self, line: usize, column: usize) -> usize {
if let Some(line_content) = self.line(line) {
line_content
.chars()
.take(column)
.map(|c| c.width().unwrap_or(0))
.sum()
} else {
0
}
}
pub fn find_all(&self, query: &str) -> Vec<(usize, usize)> {
if query.is_empty() {
return Vec::new();
}
let mut results = Vec::new();
for (line_idx, line) in self.lines.iter().enumerate() {
let mut search_start = 0;
while let Some(byte_pos) = line[search_start..].find(query) {
let abs_byte_pos = search_start + byte_pos;
let char_pos = line[..abs_byte_pos].chars().count();
results.push((line_idx, char_pos));
search_start = abs_byte_pos + query.len();
}
}
results
}
pub fn find_next(
&self,
query: &str,
from_line: usize,
from_column: usize,
) -> Option<(usize, usize)> {
if query.is_empty() {
return None;
}
for (line_idx, line) in self.lines.iter().enumerate().skip(from_line) {
let start_col = if line_idx == from_line {
self.char_to_byte_idx(line_idx, from_column + 1)
} else {
0
};
if start_col < line.len()
&& let Some(byte_pos) = line[start_col..].find(query)
{
let abs_byte_pos = start_col + byte_pos;
let char_pos = line[..abs_byte_pos].chars().count();
return Some((line_idx, char_pos));
}
}
for (line_idx, line) in self.lines.iter().enumerate().take(from_line + 1) {
let end_col = if line_idx == from_line {
self.char_to_byte_idx(line_idx, from_column)
} else {
line.len()
};
if let Some(byte_pos) = line[..end_col].find(query) {
let char_pos = line[..byte_pos].chars().count();
return Some((line_idx, char_pos));
}
}
None
}
pub fn find_prev(
&self,
query: &str,
from_line: usize,
from_column: usize,
) -> Option<(usize, usize)> {
if query.is_empty() {
return None;
}
for line_idx in (0..=from_line).rev() {
let line = &self.lines[line_idx];
let end_col = if line_idx == from_line {
self.char_to_byte_idx(line_idx, from_column)
} else {
line.len()
};
if let Some(byte_pos) = line[..end_col].rfind(query) {
let char_pos = line[..byte_pos].chars().count();
return Some((line_idx, char_pos));
}
}
for line_idx in (from_line..self.lines.len()).rev() {
let line = &self.lines[line_idx];
let start_col = if line_idx == from_line {
self.char_to_byte_idx(line_idx, from_column + 1)
} else {
0
};
if start_col < line.len()
&& let Some(byte_pos) = line[start_col..].rfind(query)
{
let abs_byte_pos = start_col + byte_pos;
let char_pos = line[..abs_byte_pos].chars().count();
return Some((line_idx, char_pos));
}
}
None
}
pub fn replace_at(
&mut self,
line: usize,
column: usize,
old_text: &str,
new_text: &str,
) -> bool {
if line >= self.lines.len() {
return false;
}
let byte_idx = self.char_to_byte_idx(line, column);
let line_content = &self.lines[line];
if byte_idx + old_text.len() > line_content.len() {
return false;
}
if &line_content[byte_idx..byte_idx + old_text.len()] != old_text {
return false;
}
if self.recording {
let cursor_before = self.cursor;
self.history.push(
EditAction::ReplaceAt {
line,
column,
old_text: old_text.to_string(),
new_text: new_text.to_string(),
},
cursor_before,
);
}
let new_line = format!(
"{}{}{}",
&line_content[..byte_idx],
new_text,
&line_content[byte_idx + old_text.len()..]
);
self.lines[line] = new_line;
self.modified = true;
self.rebuild_document();
true
}
pub fn replace_all(&mut self, old_text: &str, new_text: &str) -> usize {
if old_text.is_empty() {
return 0;
}
let mut count = 0;
for line_idx in 0..self.lines.len() {
let line = &self.lines[line_idx];
if line.contains(old_text) {
let new_line = line.replace(old_text, new_text);
count += line.matches(old_text).count();
self.lines[line_idx] = new_line;
}
}
if count > 0 {
self.modified = true;
self.rebuild_document();
}
count
}
}
impl Default for DocumentBuffer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_buffer() {
let buffer = DocumentBuffer::new();
assert_eq!(buffer.line_count(), 1);
assert_eq!(buffer.cursor().line, 0);
assert_eq!(buffer.cursor().column, 0);
assert!(!buffer.is_modified());
}
#[test]
fn test_from_string() {
let buffer = DocumentBuffer::from_string("# Hello\n\nWorld").unwrap();
assert_eq!(buffer.line_count(), 3);
assert_eq!(buffer.line(0), Some("# Hello"));
assert_eq!(buffer.line(1), Some(""));
assert_eq!(buffer.line(2), Some("World"));
}
#[test]
fn test_cursor_movement() {
let mut buffer = DocumentBuffer::from_string("Line 1\nLine 2\nLine 3").unwrap();
buffer.move_cursor(CursorMovement::Down);
assert_eq!(buffer.cursor().line, 1);
buffer.move_cursor(CursorMovement::Right);
assert_eq!(buffer.cursor().column, 1);
buffer.move_cursor(CursorMovement::EndOfLine);
assert_eq!(buffer.cursor().column, 6); }
#[test]
fn test_insert_char() {
let mut buffer = DocumentBuffer::from_string("Hello").unwrap();
buffer.cursor_mut().column = 5;
buffer.insert_char('!');
assert_eq!(buffer.line(0), Some("Hello!"));
assert_eq!(buffer.cursor().column, 6);
assert!(buffer.is_modified());
}
#[test]
fn test_delete_char() {
let mut buffer = DocumentBuffer::from_string("Hello!").unwrap();
buffer.cursor_mut().column = 6;
buffer.delete_char();
assert_eq!(buffer.line(0), Some("Hello"));
assert_eq!(buffer.cursor().column, 5);
assert!(buffer.is_modified());
}
#[test]
fn test_insert_newline() {
let mut buffer = DocumentBuffer::from_string("Hello").unwrap();
buffer.cursor_mut().column = 2;
buffer.insert_newline();
assert_eq!(buffer.line_count(), 2);
assert_eq!(buffer.line(0), Some("He"));
assert_eq!(buffer.line(1), Some("llo"));
assert_eq!(buffer.cursor().line, 1);
assert_eq!(buffer.cursor().column, 0);
}
#[test]
fn test_insert_str_single_line() {
let mut buffer = DocumentBuffer::from_string("Hello").unwrap();
buffer.cursor_mut().column = 5;
buffer.insert_str(" World");
assert_eq!(buffer.line(0), Some("Hello World"));
assert_eq!(buffer.cursor().column, 11);
assert!(buffer.is_modified());
}
#[test]
fn test_insert_str_empty() {
let mut buffer = DocumentBuffer::from_string("Hello").unwrap();
buffer.cursor_mut().column = 5;
buffer.insert_str("");
assert_eq!(buffer.line(0), Some("Hello"));
assert_eq!(buffer.cursor().column, 5);
}
#[test]
fn test_insert_str_multi_line() {
let mut buffer = DocumentBuffer::from_string("Hello").unwrap();
buffer.cursor_mut().column = 2;
buffer.insert_str("XXX\nYYY\nZZZ");
assert_eq!(buffer.line_count(), 3);
assert_eq!(buffer.line(0), Some("HeXXX"));
assert_eq!(buffer.line(1), Some("YYY"));
assert_eq!(buffer.line(2), Some("ZZZllo"));
assert_eq!(buffer.cursor().line, 2);
assert_eq!(buffer.cursor().column, 3);
assert!(buffer.is_modified());
}
#[test]
fn test_insert_str_japanese() {
let mut buffer = DocumentBuffer::from_string("Hello").unwrap();
buffer.cursor_mut().column = 5;
buffer.insert_str("こんにちは");
assert_eq!(buffer.line(0), Some("Helloこんにちは"));
assert_eq!(buffer.cursor().column, 10); assert!(buffer.is_modified());
}
#[test]
fn test_insert_char_japanese() {
let mut buffer = DocumentBuffer::from_string("Hello").unwrap();
buffer.cursor_mut().column = 5;
buffer.insert_char('あ');
buffer.insert_char('い');
assert_eq!(buffer.line(0), Some("Helloあい"));
assert_eq!(buffer.cursor().column, 7); assert!(buffer.is_modified());
}
#[test]
fn test_delete_char_japanese() {
let mut buffer = DocumentBuffer::from_string("こんにちは世界").unwrap();
buffer.cursor_mut().column = 7; buffer.delete_char();
assert_eq!(buffer.line(0), Some("こんにちは世"));
assert_eq!(buffer.cursor().column, 6);
}
#[test]
fn test_cursor_movement_japanese() {
let mut buffer = DocumentBuffer::from_string("こんにちは").unwrap();
buffer.move_cursor(CursorMovement::EndOfLine);
assert_eq!(buffer.cursor().column, 5);
buffer.move_cursor(CursorMovement::Left);
assert_eq!(buffer.cursor().column, 4);
buffer.move_cursor(CursorMovement::Left);
assert_eq!(buffer.cursor().column, 3);
}
#[test]
fn test_insert_str_mixed_content() {
let mut buffer = DocumentBuffer::from_string("Hello世界").unwrap();
buffer.cursor_mut().column = 5; buffer.insert_str("ありがとう");
assert_eq!(buffer.line(0), Some("Helloありがとう世界"));
assert_eq!(buffer.cursor().column, 10); }
#[test]
fn test_word_start_column() {
let buffer = DocumentBuffer::from_string("hello world").unwrap();
assert_eq!(buffer.word_start_column(0, 3), 0);
assert_eq!(buffer.word_start_column(0, 5), 0);
assert_eq!(buffer.word_start_column(0, 6), 6);
assert_eq!(buffer.word_start_column(0, 8), 6);
assert_eq!(buffer.word_start_column(0, 11), 6);
}
#[test]
fn test_word_start_column_with_symbols() {
let buffer = DocumentBuffer::from_string("self.method()").unwrap();
assert_eq!(buffer.word_start_column(0, 4), 0);
assert_eq!(buffer.word_start_column(0, 5), 5);
assert_eq!(buffer.word_start_column(0, 8), 5);
assert_eq!(buffer.word_start_column(0, 11), 5);
}
#[test]
fn test_word_start_column_with_underscore() {
let buffer = DocumentBuffer::from_string("my_variable = 10").unwrap();
assert_eq!(buffer.word_start_column(0, 5), 0);
assert_eq!(buffer.word_start_column(0, 11), 0);
}
#[test]
fn test_word_start_column_at_start() {
let buffer = DocumentBuffer::from_string("hello").unwrap();
assert_eq!(buffer.word_start_column(0, 0), 0);
}
#[test]
fn test_display_width_ascii() {
let buffer = DocumentBuffer::from_string("hello world").unwrap();
assert_eq!(buffer.display_width_to_column(0, 0), 0);
assert_eq!(buffer.display_width_to_column(0, 5), 5);
assert_eq!(buffer.display_width_to_column(0, 11), 11);
}
#[test]
fn test_display_width_japanese() {
let buffer = DocumentBuffer::from_string("こんにちは").unwrap();
assert_eq!(buffer.display_width_to_column(0, 0), 0);
assert_eq!(buffer.display_width_to_column(0, 1), 2); assert_eq!(buffer.display_width_to_column(0, 2), 4); assert_eq!(buffer.display_width_to_column(0, 5), 10); }
#[test]
fn test_display_width_mixed() {
let buffer = DocumentBuffer::from_string("Hello世界").unwrap();
assert_eq!(buffer.display_width_to_column(0, 5), 5); assert_eq!(buffer.display_width_to_column(0, 6), 7); assert_eq!(buffer.display_width_to_column(0, 7), 9); }
}