use serde::{Deserialize, Serialize};
use super::{
cursor::{Cursor, CursorPosition, CursorSet},
history::History,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct EditorConfig {
pub tab_size: usize,
pub insert_spaces: bool,
pub word_wrap: bool,
pub show_line_numbers: bool,
pub highlight_current_line: bool,
pub show_whitespace: bool,
pub match_brackets: bool,
pub auto_indent: bool,
pub auto_close_brackets: bool,
pub font_size: f32,
pub line_height: f32,
pub max_line_width: usize,
pub read_only: bool,
}
impl Default for EditorConfig {
fn default() -> Self {
Self {
tab_size: 4,
insert_spaces: true,
word_wrap: true,
show_line_numbers: true,
highlight_current_line: true,
show_whitespace: false,
match_brackets: true,
auto_indent: true,
auto_close_brackets: true,
font_size: 14.0,
line_height: 1.5,
max_line_width: 0,
read_only: false,
}
}
}
#[derive(Debug, Clone)]
pub struct EditorState {
pub content: String,
pub cursors: CursorSet,
pub history: History,
pub config: EditorConfig,
pub version: u64,
pub is_modified: bool,
pub scroll_line: usize,
pub scroll_offset: f32,
pub language: Option<String>,
}
impl Default for EditorState {
fn default() -> Self {
Self {
content: String::new(),
cursors: CursorSet::new(Cursor::zero()),
history: History::new(),
config: EditorConfig::default(),
version: 0,
is_modified: false,
scroll_line: 0,
scroll_offset: 0.0,
language: None,
}
}
}
impl EditorState {
#[must_use]
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
..Default::default()
}
}
#[must_use]
pub fn with_config(content: impl Into<String>, config: EditorConfig) -> Self {
Self {
content: content.into(),
config,
..Default::default()
}
}
#[must_use]
pub fn content(&self) -> &str {
&self.content
}
pub fn set_content(&mut self, content: impl Into<String>) {
let new_content = content.into();
if new_content != self.content {
self.history
.push(self.content.clone(), self.cursors.clone());
self.content = new_content;
self.version += 1;
self.is_modified = true;
}
}
pub fn replace_content(&mut self, content: impl Into<String>) {
self.content = content.into();
self.version += 1;
}
#[must_use]
pub fn cursor_position(&self) -> CursorPosition {
self.cursors.primary().head
}
pub fn set_cursor(&mut self, position: CursorPosition) {
self.cursors.primary_mut().move_to(position, false);
}
pub fn set_cursor_with_selection(&mut self, head: CursorPosition, anchor: CursorPosition) {
let cursor = self.cursors.primary_mut();
cursor.head = head;
cursor.anchor = anchor;
}
#[must_use]
pub fn line_count(&self) -> usize {
if self.content.is_empty() {
1
} else {
self.content.chars().filter(|&c| c == '\n').count() + 1
}
}
#[must_use]
pub fn get_line(&self, index: usize) -> Option<&str> {
self.content.lines().nth(index)
}
pub fn insert(&mut self, text: &str) {
if self.config.read_only {
return;
}
let position = self.cursor_position();
if let Some(offset) = self.position_to_offset(position) {
self.history
.push(self.content.clone(), self.cursors.clone());
let cursor = self.cursors.primary();
if cursor.has_selection() {
let (start, end) = (
self.position_to_offset(cursor.selection_start()),
self.position_to_offset(cursor.selection_end()),
);
if let (Some(start), Some(end)) = (start, end) {
self.content =
format!("{}{}{}", &self.content[..start], text, &self.content[end..]);
let new_offset = start + text.len();
if let Some(new_pos) = self.offset_to_position(new_offset) {
self.set_cursor(new_pos);
}
}
} else {
self.content.insert_str(offset, text);
let new_offset = offset + text.len();
if let Some(new_pos) = self.offset_to_position(new_offset) {
self.set_cursor(new_pos);
}
}
self.version += 1;
self.is_modified = true;
}
}
pub fn delete_backward(&mut self) {
if self.config.read_only {
return;
}
let cursor = self.cursors.primary();
if cursor.has_selection() {
self.delete_selection();
return;
}
let position = cursor.head;
if let Some(offset) = self.position_to_offset(position) {
if offset == 0 {
return;
}
self.history
.push(self.content.clone(), self.cursors.clone());
let prev_offset = self.content[..offset]
.char_indices()
.last()
.map_or(0, |(i, _)| i);
self.content = format!(
"{}{}",
&self.content[..prev_offset],
&self.content[offset..]
);
if let Some(new_pos) = self.offset_to_position(prev_offset) {
self.set_cursor(new_pos);
}
self.version += 1;
self.is_modified = true;
}
}
pub fn delete_forward(&mut self) {
if self.config.read_only {
return;
}
let cursor = self.cursors.primary();
if cursor.has_selection() {
self.delete_selection();
return;
}
let position = cursor.head;
if let Some(offset) = self.position_to_offset(position) {
if offset >= self.content.len() {
return;
}
self.history
.push(self.content.clone(), self.cursors.clone());
let next_offset = self.content[offset..]
.char_indices()
.nth(1)
.map_or(self.content.len(), |(i, _)| offset + i);
self.content = format!(
"{}{}",
&self.content[..offset],
&self.content[next_offset..]
);
self.version += 1;
self.is_modified = true;
}
}
fn delete_selection(&mut self) {
let cursor = self.cursors.primary();
if !cursor.has_selection() {
return;
}
let start_pos = cursor.selection_start();
let end_pos = cursor.selection_end();
if let (Some(start), Some(end)) = (
self.position_to_offset(start_pos),
self.position_to_offset(end_pos),
) {
self.history
.push(self.content.clone(), self.cursors.clone());
self.content = format!("{}{}", &self.content[..start], &self.content[end..]);
self.set_cursor(start_pos);
self.version += 1;
self.is_modified = true;
}
}
pub fn undo(&mut self) -> bool {
if let Some(entry) = self.history.undo(&self.content, &self.cursors) {
self.content = entry.content;
self.cursors = entry.cursors;
self.version += 1;
true
} else {
false
}
}
pub fn redo(&mut self) -> bool {
if let Some(entry) = self.history.redo(&self.content, &self.cursors) {
self.content = entry.content;
self.cursors = entry.cursors;
self.version += 1;
true
} else {
false
}
}
#[must_use]
pub fn can_undo(&self) -> bool {
self.history.can_undo()
}
#[must_use]
pub fn can_redo(&self) -> bool {
self.history.can_redo()
}
pub fn mark_saved(&mut self) {
self.is_modified = false;
}
#[must_use]
pub fn position_to_offset(&self, position: CursorPosition) -> Option<usize> {
let mut current_line = 0;
let mut offset = 0;
for (i, ch) in self.content.char_indices() {
if current_line == position.line {
let line_start = i;
let mut col = 0;
for (j, c) in self.content[line_start..].char_indices() {
if col == position.column {
return Some(line_start + j);
}
if c == '\n' {
break;
}
col += 1;
}
if col == position.column {
return Some(
line_start
+ self.content[line_start..]
.find('\n')
.unwrap_or(self.content.len() - line_start),
);
}
return None;
}
if ch == '\n' {
current_line += 1;
}
offset = i + ch.len_utf8();
}
if current_line == position.line && position.column == 0 {
return Some(offset);
}
None
}
#[must_use]
pub fn offset_to_position(&self, offset: usize) -> Option<CursorPosition> {
if offset > self.content.len() {
return None;
}
let mut line = 0;
let mut col = 0;
for (i, ch) in self.content.char_indices() {
if i >= offset {
return Some(CursorPosition::new(line, col));
}
if ch == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
}
Some(CursorPosition::new(line, col))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_editor_state_new() {
let state = EditorState::new("Hello, World!");
assert_eq!(state.content(), "Hello, World!");
assert_eq!(state.line_count(), 1);
assert!(!state.is_modified);
}
#[test]
fn test_insert() {
let mut state = EditorState::new("");
state.insert("Hello");
assert_eq!(state.content(), "Hello");
assert!(state.is_modified);
}
#[test]
fn test_undo_redo() {
let mut state = EditorState::new("initial");
state.set_content("modified");
assert!(state.undo());
assert_eq!(state.content(), "initial");
assert!(state.redo());
assert_eq!(state.content(), "modified");
}
#[test]
fn test_position_offset_conversion() {
let state = EditorState::new("hello\nworld\nfoo");
assert_eq!(state.position_to_offset(CursorPosition::new(0, 0)), Some(0));
assert_eq!(state.position_to_offset(CursorPosition::new(1, 0)), Some(6));
assert_eq!(
state.position_to_offset(CursorPosition::new(2, 0)),
Some(12)
);
assert_eq!(state.offset_to_position(0), Some(CursorPosition::new(0, 0)));
assert_eq!(state.offset_to_position(6), Some(CursorPosition::new(1, 0)));
}
}