use std::{
borrow::Cow,
cmp::Ordering,
fmt::Display,
ops::Range,
};
use freya_clipboard::clipboard::Clipboard;
use freya_core::events::modifiers::ModifiersExt;
use keyboard_types::{
Key,
Modifiers,
NamedKey,
};
use unicode_segmentation::UnicodeSegmentation;
use crate::editor_history::EditorHistory;
#[derive(PartialEq, Clone, Debug, Copy, Hash)]
pub enum EditorLine {
SingleParagraph,
Paragraph(usize),
}
#[derive(Clone, PartialEq, Debug)]
pub enum TextSelection {
Cursor(usize),
Range { from: usize, to: usize },
}
impl TextSelection {
pub fn new_cursor(pos: usize) -> Self {
Self::Cursor(pos)
}
pub fn new_range((from, to): (usize, usize)) -> Self {
Self::Range { from, to }
}
pub fn pos(&self) -> usize {
self.end()
}
pub fn set_as_cursor(&mut self) {
*self = Self::Cursor(self.end())
}
pub fn set_as_range(&mut self) {
*self = Self::Range {
from: self.start(),
to: self.end(),
}
}
pub fn start(&self) -> usize {
match self {
Self::Cursor(pos) => *pos,
Self::Range { from, .. } => *from,
}
}
pub fn end(&self) -> usize {
match self {
Self::Cursor(pos) => *pos,
Self::Range { to, .. } => *to,
}
}
pub fn move_to(&mut self, position: usize) {
match self {
Self::Cursor(pos) => *pos = position,
Self::Range { to, .. } => {
*to = position;
}
}
}
pub fn is_range(&self) -> bool {
matches!(self, Self::Range { .. })
}
}
#[derive(Clone)]
pub struct Line<'a> {
pub text: Cow<'a, str>,
pub utf16_len: usize,
}
impl Line<'_> {
pub fn utf16_len(&self) -> usize {
self.utf16_len
}
}
impl Display for Line<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.text)
}
}
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct TextEvent: u8 {
const CURSOR_CHANGED = 0x01;
const TEXT_CHANGED = 0x02;
const SELECTION_CHANGED = 0x04;
}
}
pub trait TextEditor {
type LinesIterator<'a>: Iterator<Item = Line<'a>>
where
Self: 'a;
fn set(&mut self, text: &str);
fn lines(&self) -> Self::LinesIterator<'_>;
fn insert_char(&mut self, char: char, char_idx: usize) -> usize;
fn insert(&mut self, text: &str, char_idx: usize) -> usize;
fn remove(&mut self, range: Range<usize>) -> usize;
fn char_to_line(&self, char_idx: usize) -> usize;
fn line_to_char(&self, line_idx: usize) -> usize;
fn utf16_cu_to_char(&self, utf16_cu_idx: usize) -> usize;
fn char_to_utf16_cu(&self, idx: usize) -> usize;
fn line(&self, line_idx: usize) -> Option<Line<'_>>;
fn len_lines(&self) -> usize;
fn len_chars(&self) -> usize;
fn len_utf16_cu(&self) -> usize;
fn selection(&self) -> &TextSelection;
fn selection_mut(&mut self) -> &mut TextSelection;
fn cursor_row(&self) -> usize {
let pos = self.cursor_pos();
let pos_utf8 = self.utf16_cu_to_char(pos);
self.char_to_line(pos_utf8)
}
fn cursor_col(&self) -> usize {
let pos = self.cursor_pos();
let pos_utf8 = self.utf16_cu_to_char(pos);
let line = self.char_to_line(pos_utf8);
let line_char_utf8 = self.line_to_char(line);
let line_char = self.char_to_utf16_cu(line_char_utf8);
pos - line_char
}
fn cursor_down(&mut self) -> bool {
let old_row = self.cursor_row();
let old_col = self.cursor_col();
match old_row.cmp(&(self.len_lines() - 1)) {
Ordering::Less => {
let new_row = old_row + 1;
let new_row_char = self.char_to_utf16_cu(self.line_to_char(new_row));
let new_row_len = self.line(new_row).unwrap().utf16_len();
let new_col = old_col.min(new_row_len.saturating_sub(1));
self.selection_mut().move_to(new_row_char + new_col);
true
}
Ordering::Equal => {
let end = self.len_utf16_cu();
self.selection_mut().move_to(end);
true
}
Ordering::Greater => {
false
}
}
}
fn cursor_up(&mut self) -> bool {
let pos = self.cursor_pos();
let old_row = self.cursor_row();
let old_col = self.cursor_col();
if pos > 0 {
if old_row == 0 {
self.selection_mut().move_to(0);
} else {
let new_row = old_row - 1;
let new_row_char = self.char_to_utf16_cu(self.line_to_char(new_row));
let new_row_len = self.line(new_row).unwrap().utf16_len();
let new_col = old_col.min(new_row_len.saturating_sub(1));
self.selection_mut().move_to(new_row_char + new_col);
}
true
} else {
false
}
}
fn cursor_right(&mut self) -> bool {
if self.cursor_pos() < self.len_utf16_cu() {
let to = self.selection().end() + 1;
self.selection_mut().move_to(to);
true
} else {
false
}
}
fn cursor_left(&mut self) -> bool {
if self.cursor_pos() > 0 {
let to = self.selection().end() - 1;
self.selection_mut().move_to(to);
true
} else {
false
}
}
fn cursor_word_right(&mut self) -> bool {
let pos = self.cursor_pos();
let len = self.len_utf16_cu();
if pos >= len {
return false;
}
let start_char = self.utf16_cu_to_char(pos);
let initial_line = self.char_to_line(start_char);
let initial_offset = start_char - self.line_to_char(initial_line);
for line_idx in initial_line..self.len_lines() {
let Some(line) = self.line(line_idx) else {
continue;
};
let line_char_offset = self.line_to_char(line_idx);
let from = if line_idx == initial_line {
initial_offset
} else {
0
};
let mut char_offset = 0;
for word in line.text.split_word_bounds() {
char_offset += word.chars().count();
if char_offset > from && !word.chars().all(char::is_whitespace) {
let new_pos = self.char_to_utf16_cu(line_char_offset + char_offset);
self.selection_mut().move_to(new_pos);
return true;
}
}
}
self.selection_mut().move_to(len);
true
}
fn cursor_word_left(&mut self) -> bool {
let pos = self.cursor_pos();
if pos == 0 {
return false;
}
let start_char = self.utf16_cu_to_char(pos);
let initial_line = self.char_to_line(start_char);
let initial_offset = start_char - self.line_to_char(initial_line);
for line_idx in (0..=initial_line).rev() {
let Some(line) = self.line(line_idx) else {
continue;
};
let line_char_offset = self.line_to_char(line_idx);
let to = if line_idx == initial_line {
initial_offset
} else {
line.text.chars().count()
};
let mut char_offset = 0;
let mut last_word_start = None;
for word in line.text.split_word_bounds() {
if char_offset >= to {
break;
}
if !word.chars().all(char::is_whitespace) {
last_word_start = Some(char_offset);
}
char_offset += word.chars().count();
}
if let Some(start) = last_word_start {
let new_pos = self.char_to_utf16_cu(line_char_offset + start);
self.selection_mut().move_to(new_pos);
return true;
}
}
self.selection_mut().move_to(0);
true
}
fn cursor_pos(&self) -> usize {
self.selection().pos()
}
fn move_cursor_to(&mut self, pos: usize) {
self.selection_mut().move_to(pos);
}
fn has_any_selection(&self) -> bool;
fn get_selection(&self) -> Option<(usize, usize)>;
fn get_visible_selection(&self, editor_line: EditorLine) -> Option<(usize, usize)> {
let (selected_from, selected_to) = match self.selection() {
TextSelection::Cursor(_) => return None,
TextSelection::Range { from, to } => (*from, *to),
};
match editor_line {
EditorLine::Paragraph(line_index) => {
let selected_from_row = self.char_to_line(self.utf16_cu_to_char(selected_from));
let selected_to_row = self.char_to_line(self.utf16_cu_to_char(selected_to));
let editor_row_idx = self.char_to_utf16_cu(self.line_to_char(line_index));
let selected_from_row_idx =
self.char_to_utf16_cu(self.line_to_char(selected_from_row));
let selected_to_row_idx = self.char_to_utf16_cu(self.line_to_char(selected_to_row));
let selected_from_col_idx = selected_from - selected_from_row_idx;
let selected_to_col_idx = selected_to - selected_to_row_idx;
if (line_index > selected_from_row && line_index < selected_to_row)
|| (line_index < selected_from_row && line_index > selected_to_row)
{
let len = self.line(line_index).unwrap().utf16_len();
return Some((0, len));
}
match selected_from_row.cmp(&selected_to_row) {
Ordering::Greater => {
if selected_from_row == line_index {
Some((0, selected_from_col_idx))
} else if selected_to_row == line_index {
let len = self.line(selected_to_row).unwrap().utf16_len();
Some((selected_to_col_idx, len))
} else {
None
}
}
Ordering::Less => {
if selected_from_row == line_index {
let len = self.line(selected_from_row).unwrap().utf16_len();
Some((selected_from_col_idx, len))
} else if selected_to_row == line_index {
Some((0, selected_to_col_idx))
} else {
None
}
}
Ordering::Equal if selected_from_row == line_index => {
Some((selected_from - editor_row_idx, selected_to - editor_row_idx))
}
_ => None,
}
}
EditorLine::SingleParagraph => Some((selected_from, selected_to)),
}
}
fn clear_selection(&mut self);
fn set_selection(&mut self, selected: (usize, usize));
fn measure_selection(&self, to: usize, line_index: EditorLine) -> TextSelection {
let mut selection = self.selection().clone();
match line_index {
EditorLine::Paragraph(line_index) => {
let row_char = self.line_to_char(line_index);
let pos = self.char_to_utf16_cu(row_char) + to;
selection.move_to(pos);
}
EditorLine::SingleParagraph => {
selection.move_to(to);
}
}
selection
}
fn process_key(
&mut self,
key: &Key,
modifiers: &Modifiers,
allow_tabs: bool,
allow_changes: bool,
allow_read_clipboard: bool,
allow_write_clipboard: bool,
) -> TextEvent {
let mut event = TextEvent::empty();
let selection = self.get_selection();
let skip_arrows_movement = !modifiers.contains(Modifiers::SHIFT) && selection.is_some();
match key {
Key::Named(NamedKey::Shift) => {}
Key::Named(NamedKey::Control) => {}
Key::Named(NamedKey::Alt) => {}
Key::Named(NamedKey::Escape) => {
self.clear_selection();
}
Key::Named(NamedKey::ArrowDown) => {
if modifiers.contains(Modifiers::SHIFT) {
self.selection_mut().set_as_range();
} else {
self.selection_mut().set_as_cursor();
}
if !skip_arrows_movement && self.cursor_down() {
event.insert(TextEvent::CURSOR_CHANGED);
}
}
Key::Named(NamedKey::ArrowLeft) => {
if modifiers.contains(Modifiers::SHIFT) {
self.selection_mut().set_as_range();
} else {
self.selection_mut().set_as_cursor();
}
let word_jump = if cfg!(target_os = "macos") {
modifiers.contains(Modifiers::ALT)
} else {
modifiers.contains(Modifiers::CONTROL)
};
let moved = !skip_arrows_movement
&& if word_jump {
self.cursor_word_left()
} else {
self.cursor_left()
};
if moved {
event.insert(TextEvent::CURSOR_CHANGED);
}
}
Key::Named(NamedKey::ArrowRight) => {
if modifiers.contains(Modifiers::SHIFT) {
self.selection_mut().set_as_range();
} else {
self.selection_mut().set_as_cursor();
}
let word_jump = if cfg!(target_os = "macos") {
modifiers.contains(Modifiers::ALT)
} else {
modifiers.contains(Modifiers::CONTROL)
};
let moved = !skip_arrows_movement
&& if word_jump {
self.cursor_word_right()
} else {
self.cursor_right()
};
if moved {
event.insert(TextEvent::CURSOR_CHANGED);
}
}
Key::Named(NamedKey::ArrowUp) => {
if modifiers.contains(Modifiers::SHIFT) {
self.selection_mut().set_as_range();
} else {
self.selection_mut().set_as_cursor();
}
if !skip_arrows_movement && self.cursor_up() {
event.insert(TextEvent::CURSOR_CHANGED);
}
}
Key::Named(NamedKey::Backspace) if allow_changes => {
let cursor_pos = self.cursor_pos();
let selection = self.get_selection_range();
if let Some((start, end)) = selection {
self.remove(start..end);
self.move_cursor_to(start);
event.insert(TextEvent::TEXT_CHANGED);
} else if cursor_pos > 0 {
let removed_text_len = self.remove(cursor_pos - 1..cursor_pos);
self.move_cursor_to(cursor_pos - removed_text_len);
event.insert(TextEvent::TEXT_CHANGED);
}
}
Key::Named(NamedKey::Delete) if allow_changes => {
let cursor_pos = self.cursor_pos();
let selection = self.get_selection_range();
if let Some((start, end)) = selection {
self.remove(start..end);
self.move_cursor_to(start);
event.insert(TextEvent::TEXT_CHANGED);
} else if cursor_pos < self.len_utf16_cu() {
self.remove(cursor_pos..cursor_pos + 1);
event.insert(TextEvent::TEXT_CHANGED);
}
}
Key::Named(NamedKey::Enter) if allow_changes => {
let cursor_pos = self.cursor_pos();
self.insert_char('\n', cursor_pos);
self.cursor_right();
event.insert(TextEvent::TEXT_CHANGED);
}
Key::Named(NamedKey::Tab) if allow_tabs && allow_changes => {
let text = " ".repeat(self.get_indentation().into());
let cursor_pos = self.cursor_pos();
self.insert(&text, cursor_pos);
self.move_cursor_to(cursor_pos + text.chars().count());
event.insert(TextEvent::TEXT_CHANGED);
}
Key::Character(character) => {
let meta_or_ctrl = modifiers.contains(Modifiers::ctrl_or_meta());
match character.as_str() {
" " if allow_changes => {
let selection = self.get_selection_range();
if let Some((start, end)) = selection {
self.remove(start..end);
self.move_cursor_to(start);
event.insert(TextEvent::TEXT_CHANGED);
}
let cursor_pos = self.cursor_pos();
self.insert_char(' ', cursor_pos);
self.cursor_right();
event.insert(TextEvent::TEXT_CHANGED);
}
"a" if meta_or_ctrl => {
let len = self.len_utf16_cu();
self.set_selection((0, len));
}
"c" if meta_or_ctrl && allow_write_clipboard => {
let selected = self.get_selected_text();
if let Some(selected) = selected {
Clipboard::set(selected).ok();
}
}
"x" if meta_or_ctrl && allow_changes && allow_write_clipboard => {
let selection = self.get_selection_range();
if let Some((start, end)) = selection {
let text = self.get_selected_text().unwrap();
self.remove(start..end);
Clipboard::set(text).ok();
self.move_cursor_to(start);
event.insert(TextEvent::TEXT_CHANGED);
}
}
"v" if meta_or_ctrl && allow_changes && allow_read_clipboard => {
if let Ok(copied_text) = Clipboard::get() {
let selection = self.get_selection_range();
if let Some((start, end)) = selection {
self.remove(start..end);
self.move_cursor_to(start);
}
let cursor_pos = self.cursor_pos();
self.insert(&copied_text, cursor_pos);
let last_idx = copied_text.encode_utf16().count() + cursor_pos;
self.move_cursor_to(last_idx);
event.insert(TextEvent::TEXT_CHANGED);
}
}
"z" if meta_or_ctrl && allow_changes => {
let undo_result = self.undo();
if let Some(selection) = undo_result {
*self.selection_mut() = selection;
event.insert(TextEvent::TEXT_CHANGED);
event.insert(TextEvent::SELECTION_CHANGED);
}
}
"y" if meta_or_ctrl && allow_changes => {
let redo_result = self.redo();
if let Some(selection) = redo_result {
*self.selection_mut() = selection;
event.insert(TextEvent::TEXT_CHANGED);
event.insert(TextEvent::SELECTION_CHANGED);
}
}
_ if allow_changes => {
let selection = self.get_selection_range();
if let Some((start, end)) = selection {
self.remove(start..end);
self.move_cursor_to(start);
event.insert(TextEvent::TEXT_CHANGED);
}
if let Ok(ch) = character.parse::<char>() {
let cursor_pos = self.cursor_pos();
let inserted_text_len = self.insert_char(ch, cursor_pos);
self.move_cursor_to(cursor_pos + inserted_text_len);
event.insert(TextEvent::TEXT_CHANGED);
} else {
let cursor_pos = self.cursor_pos();
let inserted_text_len = self.insert(character, cursor_pos);
self.move_cursor_to(cursor_pos + inserted_text_len);
event.insert(TextEvent::TEXT_CHANGED);
}
}
_ => {}
}
}
_ => {}
}
if event.contains(TextEvent::TEXT_CHANGED) && !event.contains(TextEvent::SELECTION_CHANGED)
{
self.clear_selection();
}
if self.get_selection() != selection {
event.insert(TextEvent::SELECTION_CHANGED);
}
event
}
fn get_selected_text(&self) -> Option<String>;
fn undo(&mut self) -> Option<TextSelection>;
fn redo(&mut self) -> Option<TextSelection>;
fn editor_history(&mut self) -> &mut EditorHistory;
fn get_selection_range(&self) -> Option<(usize, usize)>;
fn get_indentation(&self) -> u8;
fn find_word_boundaries(&self, pos: usize) -> (usize, usize) {
let pos_char = self.utf16_cu_to_char(pos);
let len_chars = self.len_chars();
if len_chars == 0 {
return (pos, pos);
}
let line_idx = self.char_to_line(pos_char);
let line_char = self.line_to_char(line_idx);
let line = self.line(line_idx).unwrap();
let line_str: std::borrow::Cow<str> = line.text;
let pos_in_line = pos_char - line_char;
let mut char_offset = 0;
for word in line_str.split_word_bounds() {
let word_char_len = word.chars().count();
let word_start = char_offset;
let word_end = char_offset + word_char_len;
if pos_in_line >= word_start && pos_in_line < word_end {
let start_char = line_char + word_start;
let end_char = line_char + word_end;
return (
self.char_to_utf16_cu(start_char),
self.char_to_utf16_cu(end_char),
);
}
char_offset = word_end;
}
(pos, pos)
}
}