use crate::error::DraconError;
use crate::input::event::{
Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use crate::utils::highlight_code;
use crate::utils::{get_clipboard_text, set_clipboard_text};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::path::PathBuf;
#[derive(Clone, Copy, Debug, PartialEq)]
enum EditorMode {
Normal,
Search,
Replace,
GotoLine,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct EditorConfig {
tab_size: u8,
show_line_numbers: bool,
word_wrap: bool,
show_indent_guides: bool,
show_status_bar: bool,
}
impl Default for EditorConfig {
fn default() -> Self {
Self {
tab_size: 4,
show_line_numbers: true,
word_wrap: false,
show_indent_guides: false,
show_status_bar: true,
}
}
}
#[derive(Clone, Debug)]
pub struct TextEditor {
pub lines: Vec<String>,
pub cursor_row: usize,
pub cursor_col: usize,
pub scroll_row: usize,
pub scroll_col: usize,
pub style: Style,
pub cursor_style: Style,
pub modified: bool,
pub show_line_numbers: bool,
pub history: Vec<Vec<String>>,
pub redo_stack: Vec<Vec<String>>,
pub filter_query: String,
pub filtered_indices: Vec<usize>,
pub read_only: bool,
pub selection_start: Option<(usize, usize)>,
pub selection_end: Option<(usize, usize)>,
pub is_selecting: bool,
pub is_dragging_selection: bool,
pub language: String,
pub wrap: bool,
pub highlighted_cache: RefCell<Vec<Line<'static>>>,
pub first_invalid_line: RefCell<Option<usize>>,
pub file_path: Option<PathBuf>,
pub show_status_bar: bool,
pub show_indent_guides: bool,
extra_cursors: Vec<(usize, usize)>,
mode: EditorMode,
mode_input: String,
is_replacing: bool,
}
impl Default for TextEditor {
fn default() -> Self {
Self {
lines: vec![String::new()],
cursor_row: 0,
cursor_col: 0,
scroll_row: 0,
scroll_col: 0,
style: Style::default().fg(Color::Rgb(255, 255, 255)),
cursor_style: Style::default()
.bg(Color::Rgb(88, 166, 255))
.fg(Color::Black)
.add_modifier(Modifier::BOLD),
modified: false,
show_line_numbers: true,
history: Vec::new(),
redo_stack: Vec::new(),
filter_query: String::new(),
filtered_indices: Vec::new(),
read_only: false,
selection_start: None,
selection_end: None,
is_selecting: false,
is_dragging_selection: false,
language: String::new(),
wrap: false,
highlighted_cache: RefCell::new(Vec::new()),
first_invalid_line: RefCell::new(Some(0)),
file_path: None,
show_status_bar: true,
show_indent_guides: false,
extra_cursors: Vec::new(),
mode: EditorMode::Normal,
mode_input: String::new(),
is_replacing: false,
}
}
}
impl TextEditor {
pub fn add_cursor(&mut self, row: usize, col: usize) {
if (row, col) != (self.cursor_row, self.cursor_col)
&& !self.extra_cursors.contains(&(row, col))
{
self.extra_cursors.push((row, col));
}
}
pub fn remove_cursor(&mut self, row: usize, col: usize) {
self.extra_cursors.retain(|&(r, c)| !(r == row && c == col));
}
pub fn clear_extra_cursors(&mut self) {
self.extra_cursors.clear();
}
pub fn extra_cursor_count(&self) -> usize {
self.extra_cursors.len()
}
pub fn get_extra_cursors(&self) -> &Vec<(usize, usize)> {
&self.extra_cursors
}
#[allow(dead_code)]
fn move_cursor(&mut self, row: usize, col: usize) {
self.cursor_row = row;
self.cursor_col = col;
}
fn finish_nav_move(&mut self, has_shift: bool, area: Rect) {
if has_shift {
self.update_selection_end();
} else {
self.clear_selection();
}
self.ensure_cursor_visible(area);
}
fn nav_move<F>(&mut self, has_shift: bool, area: Rect, mover: F)
where
F: FnOnce(&mut TextEditor),
{
if has_shift {
self.maybe_start_selection();
}
mover(self);
self.finish_nav_move(has_shift, area);
}
pub fn set_filter(&mut self, query: &str) {
if self.filter_query == query {
return;
}
if query.is_empty() && !self.filter_query.is_empty() {
if self.cursor_row < self.filtered_indices.len() {
self.cursor_row = self.filtered_indices[self.cursor_row];
} else if let Some(&last) = self.filtered_indices.last() {
self.cursor_row = last;
} else {
self.cursor_row = 0;
}
self.filtered_indices.clear();
}
self.filter_query = query.to_string();
if !self.filter_query.is_empty() {
let use_regex = Regex::new(&format!("(?i){}", query)).is_ok();
self.filtered_indices = self
.lines
.iter()
.enumerate()
.filter(|(_, line)| {
if use_regex {
if let Ok(re) = Regex::new(&format!("(?i){}", query)) {
re.is_match(line)
} else {
line.to_lowercase()
.contains(&self.filter_query.to_lowercase())
}
} else {
line.to_lowercase()
.contains(&self.filter_query.to_lowercase())
}
})
.map(|(i, _)| i)
.collect();
self.cursor_row = 0;
self.scroll_row = 0;
}
self.scroll_col = 0;
self.cursor_col = 0;
self.invalidate_from(0);
}
fn effective_len(&self) -> usize {
if !self.filter_query.is_empty() {
self.filtered_indices.len()
} else {
self.lines.len()
}
}
fn get_effective_line(&self, idx: usize) -> &String {
if idx >= self.effective_len() {
return &self.lines[0];
} if !self.filter_query.is_empty() {
&self.lines[self.filtered_indices[idx]]
} else {
&self.lines[idx]
}
}
fn get_real_line_idx(&self, idx: usize) -> usize {
if !self.filter_query.is_empty() {
if idx < self.filtered_indices.len() {
self.filtered_indices[idx]
} else {
0
}
} else {
idx
}
}
pub fn get_visual_x(&self, row: usize, byte_col: usize) -> usize {
if row >= self.effective_len() {
return 0;
}
let line = self.get_effective_line(row);
if byte_col > line.len() {
return 0;
}
line[..byte_col].width()
}
pub fn get_byte_index_from_visual(&self, row: usize, visual_x: usize) -> usize {
if row >= self.effective_len() {
return 0;
}
let line = self.get_effective_line(row);
let mut width = 0;
for (i, c) in line.char_indices() {
if width >= visual_x {
return i;
}
width += c.width().unwrap_or(0);
}
line.len()
}
pub fn new() -> Self {
Self::default()
}
pub fn invalidate_from(&self, row: usize) {
let mut first = self.first_invalid_line.borrow_mut();
if let Some(current) = *first {
if row < current {
*first = Some(row);
}
} else {
*first = Some(row);
}
}
pub fn with_content(content: &str) -> Self {
let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
if lines.is_empty() {
lines.push(String::new());
}
if !lines.last().map(|l| l.is_empty()).unwrap_or(false) {
lines.push(String::new());
}
Self {
lines,
..Default::default()
}
}
pub fn open(path: &PathBuf) -> std::io::Result<Self> {
let content = std::fs::read_to_string(path)?;
let mut editor = Self::with_content(&content);
editor.file_path = Some(path.clone());
editor.modified = false;
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
editor.language = ext.to_string();
}
let _ = editor.load_undo_stack();
Ok(editor)
}
pub fn save(&mut self) -> std::io::Result<()> {
if let Some(ref path) = self.file_path {
std::fs::write(path, self.get_content())?;
self.save_undo_stack()?;
self.modified = false;
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"No file path set. Use save_as() instead.",
))
}
}
pub fn save_err(&mut self) -> Result<(), DraconError> {
if let Some(ref path) = self.file_path {
std::fs::write(path, self.get_content()).map_err(DraconError::from)?;
self.save_undo_stack().map_err(DraconError::from)?;
self.modified = false;
Ok(())
} else {
Err(DraconError::io_msg("No file path set. Use save_as_err() instead."))
}
}
pub fn save_as(&mut self, path: &PathBuf) -> std::io::Result<()> {
std::fs::write(path, self.get_content())?;
self.file_path = Some(path.clone());
self.save_undo_stack()?;
self.modified = false;
Ok(())
}
pub fn save_as_err(&mut self, path: &PathBuf) -> Result<(), DraconError> {
std::fs::write(path, self.get_content()).map_err(DraconError::from)?;
self.file_path = Some(path.clone());
self.save_undo_stack().map_err(DraconError::from)?;
self.modified = false;
Ok(())
}
pub fn file_path(&self) -> Option<&PathBuf> {
self.file_path.as_ref()
}
fn undo_path(&self) -> Option<PathBuf> {
self.file_path.as_ref().map(|p| {
let mut undo_path = p.clone();
let stem = undo_path.file_stem().unwrap_or_default().to_string_lossy();
let ext = undo_path
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default();
let parent = undo_path.parent().unwrap_or(std::path::Path::new("."));
let undo_name = format!(".{}.undo", stem);
if ext.is_empty() {
undo_path = parent.join(&undo_name);
} else {
undo_path = parent.join(format!(".{}.{}", stem, ext));
undo_path.set_extension("undo");
}
undo_path
})
}
fn config_path(&self) -> Option<PathBuf> {
self.file_path.as_ref().map(|p| {
let mut cfg_path = p.clone();
let stem = cfg_path.file_stem().unwrap_or_default().to_string_lossy();
let ext = cfg_path
.extension()
.map(|e| e.to_string_lossy().to_string())
.unwrap_or_default();
let parent = cfg_path.parent().unwrap_or(std::path::Path::new("."));
if ext.is_empty() {
cfg_path = parent.join(format!(".{}.dte.json", stem));
} else {
cfg_path = parent.join(format!(".{}.{}", stem, ext));
cfg_path.set_extension("dte.json");
}
cfg_path
})
}
pub fn load_config(&mut self) -> std::io::Result<()> {
if let Some(cfg_path) = self.config_path() {
if cfg_path.exists() {
let content = std::fs::read_to_string(cfg_path)?;
if let Ok(config) = serde_json::from_str::<EditorConfig>(&content) {
self.show_line_numbers = config.show_line_numbers;
self.wrap = config.word_wrap;
self.show_indent_guides = config.show_indent_guides;
self.show_status_bar = config.show_status_bar;
}
}
}
Ok(())
}
pub fn save_config(&self) -> std::io::Result<()> {
if let Some(cfg_path) = self.config_path() {
let config = EditorConfig {
tab_size: 4,
show_line_numbers: self.show_line_numbers,
word_wrap: self.wrap,
show_indent_guides: self.show_indent_guides,
show_status_bar: self.show_status_bar,
};
let content = serde_json::to_string_pretty(&config).unwrap_or_default();
std::fs::write(cfg_path, content)?;
}
Ok(())
}
pub fn save_config_err(&self) -> Result<(), DraconError> {
if let Some(cfg_path) = self.config_path() {
let config = EditorConfig {
tab_size: 4,
show_line_numbers: self.show_line_numbers,
word_wrap: self.wrap,
show_indent_guides: self.show_indent_guides,
show_status_bar: self.show_status_bar,
};
let content = serde_json::to_string_pretty(&config)
.map_err(|e| DraconError::Serialize(e.to_string()))?;
std::fs::write(cfg_path, content).map_err(DraconError::from)?;
}
Ok(())
}
pub fn save_undo_stack(&self) -> std::io::Result<()> {
if let Some(undo_path) = self.undo_path() {
let encoded: Vec<String> = self.history.iter().map(|lines| lines.join("\n")).collect();
std::fs::write(undo_path, encoded.join("\n---\n"))?;
}
Ok(())
}
pub fn save_undo_stack_err(&self) -> Result<(), DraconError> {
if let Some(undo_path) = self.undo_path() {
let encoded: Vec<String> = self.history.iter().map(|lines| lines.join("\n")).collect();
std::fs::write(undo_path, encoded.join("\n---\n")).map_err(DraconError::from)?;
}
Ok(())
}
pub fn load_undo_stack(&mut self) -> std::io::Result<()> {
if let Some(undo_path) = self.undo_path() {
if undo_path.exists() {
let content = std::fs::read_to_string(undo_path)?;
self.history = content
.split("\n---\n")
.map(|chunk| chunk.lines().map(|l| l.to_string()).collect())
.collect();
if self.history.len() > 100 {
self.history = self.history.split_off(self.history.len() - 100);
}
}
}
Ok(())
}
pub fn filename(&self) -> String {
self.file_path
.as_ref()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.unwrap_or("Untitled")
.to_string()
}
pub fn goto_line(&mut self, line: usize, area: Rect) {
if line > 0 && line <= self.lines.len() {
self.cursor_row = line - 1;
self.cursor_col = 0;
self.ensure_cursor_visible(area);
}
}
pub fn with_show_line_numbers(&mut self, show: bool) {
self.show_line_numbers = show;
}
pub fn with_word_wrap(&mut self, wrap: bool) {
self.wrap = wrap;
}
pub fn with_status_bar(&mut self, show: bool) {
self.show_status_bar = show;
}
pub fn with_indent_guides(&mut self, show: bool) {
self.show_indent_guides = show;
}
pub fn with_language(&mut self, language: &str) {
self.language = language.to_string();
self.invalidate_from(0);
}
pub fn get_content(&self) -> String {
self.lines.join("\n")
}
pub fn replace_all(&mut self, find: &str, replace: &str) {
if find.is_empty() {
return;
}
if let Ok(re) = Regex::new(find) {
for line in &mut self.lines {
*line = re.replace_all(line, replace).to_string();
}
} else {
for line in &mut self.lines {
*line = line.replace(find, replace);
}
}
self.modified = true;
self.invalidate_from(0);
}
pub fn replace_next(&mut self, find: &str, replace: &str) -> bool {
if find.is_empty() {
return false;
}
let start_row = self.cursor_row;
let start_col = self.cursor_col;
let use_regex = Regex::new(find).is_ok();
for r in 0..self.lines.len() {
let row = (start_row + r) % self.lines.len();
let line = &self.lines[row];
let search_from = if r == 0 { start_col } else { 0 };
if search_from < line.len() {
if use_regex {
if let Ok(re) = Regex::new(find) {
if let Some(mat) = re.find(&line[search_from..]) {
let actual_col = search_from + mat.start();
let mut new_line = line.clone();
new_line.replace_range(actual_col..actual_col + mat.len(), replace);
self.lines[row] = new_line;
self.cursor_row = row;
self.cursor_col = actual_col + replace.len();
self.modified = true;
self.invalidate_from(0);
return true;
}
}
} else if let Some(col) = line[search_from..].find(find) {
let actual_col = search_from + col;
let mut new_line = line.clone();
new_line.replace_range(actual_col..actual_col + find.len(), replace);
self.lines[row] = new_line;
self.cursor_row = row;
self.cursor_col = actual_col + replace.len();
self.modified = true;
self.invalidate_from(0);
return true;
}
}
}
false
}
pub fn gutter_width(&self) -> usize {
if !self.show_line_numbers {
return 0;
}
let total_lines = self.lines.len();
let digits = total_lines.to_string().len();
digits + 2
}
pub fn handle_event(&mut self, event: &Event, area: Rect) -> bool {
if !self.filter_query.is_empty() || self.read_only {
if let Event::Key(key) = event {
if key.kind != KeyEventKind::Press {
return false;
}
let has_control = key.modifiers.contains(KeyModifiers::CONTROL);
let has_shift = key.modifiers.contains(KeyModifiers::SHIFT);
match key.code {
KeyCode::Up | KeyCode::Char('p') if has_control => {
self.nav_move(has_shift, area, |s| s.move_cursor_up());
}
KeyCode::Down | KeyCode::Char('n') if has_control => {
self.nav_move(has_shift, area, |s| s.move_cursor_down());
}
KeyCode::Up => {
self.nav_move(has_shift, area, |s| s.move_cursor_up());
}
KeyCode::Down => {
self.nav_move(has_shift, area, |s| s.move_cursor_down());
}
KeyCode::Left => {
self.nav_move(has_shift, area, |s| s.move_cursor_left());
}
KeyCode::Right => {
self.nav_move(has_shift, area, |s| s.move_cursor_right());
}
KeyCode::PageUp => {
if has_shift {
self.maybe_start_selection();
}
self.cursor_row = self.cursor_row.saturating_sub(area.height as usize);
self.finish_nav_move(has_shift, area);
}
KeyCode::PageDown => {
if has_shift {
self.maybe_start_selection();
}
self.cursor_row = std::cmp::min(
self.effective_len().saturating_sub(1),
self.cursor_row + area.height as usize,
);
self.finish_nav_move(has_shift, area);
}
KeyCode::Home => {
if has_shift {
self.maybe_start_selection();
}
let line = self.get_effective_line(self.cursor_row);
let first_non_whitespace = line
.chars()
.enumerate()
.find(|(_, c)| !c.is_whitespace())
.map(|(i, _)| i)
.unwrap_or(0);
if self.cursor_col == first_non_whitespace {
self.cursor_col = 0;
} else {
self.cursor_col = first_non_whitespace;
}
self.finish_nav_move(has_shift, area);
}
KeyCode::End => {
if has_shift {
self.maybe_start_selection();
}
self.cursor_col = self.get_effective_line(self.cursor_row).len();
self.finish_nav_move(has_shift, area);
}
_ => {}
}
}
return false;
}
if self.mode != EditorMode::Normal {
if let Event::Key(key) = event {
if key.kind != KeyEventKind::Press {
return false;
}
match key.code {
KeyCode::Esc => {
self.mode = EditorMode::Normal;
self.mode_input.clear();
return true;
}
KeyCode::Enter => {
match self.mode {
EditorMode::Search => {
let query = self.mode_input.clone();
if !query.is_empty() {
self.set_filter(&query);
}
}
EditorMode::Replace => {
let query = self.mode_input.clone();
if !query.is_empty() {
self.set_filter(&query);
}
}
EditorMode::GotoLine => {
let line_str = self.mode_input.clone();
if let Ok(line) = line_str.parse::<usize>() {
self.goto_line(line, area);
}
}
EditorMode::Normal => {}
}
self.mode = EditorMode::Normal;
self.mode_input.clear();
return true;
}
KeyCode::Backspace => {
self.mode_input.pop();
return true;
}
KeyCode::Char(c)
if !key.modifiers.contains(KeyModifiers::CONTROL)
&& !key.modifiers.contains(KeyModifiers::ALT) =>
{
self.mode_input.push(c);
return true;
}
_ => {}
}
}
return false;
}
match event {
Event::Key(key) => {
if key.kind != KeyEventKind::Press {
return false;
}
let has_control = key.modifiers.contains(KeyModifiers::CONTROL);
let has_alt = key.modifiers.contains(KeyModifiers::ALT);
let has_shift = key.modifiers.contains(KeyModifiers::SHIFT);
match key.code {
KeyCode::Char(c) if !has_control && !has_alt => {
self.push_history();
if self.selection_start.is_some() {
self.delete_selection();
}
self.insert_char(c);
self.modified = true;
self.ensure_cursor_visible(area);
return true;
}
KeyCode::Tab => {
self.push_history();
if self.selection_start.is_some() {
self.delete_selection();
}
for _ in 0..4 {
self.insert_char(' ');
}
self.modified = true;
self.ensure_cursor_visible(area);
return true;
}
KeyCode::BackTab => {
self.push_history();
let line = &mut self.lines[self.cursor_row];
let mut spaces_removed = 0;
while spaces_removed < 4 && line.starts_with(' ') {
line.remove(0);
spaces_removed += 1;
}
if spaces_removed > 0 {
self.cursor_col = self.cursor_col.saturating_sub(spaces_removed);
self.modified = true;
self.invalidate_from(0);
}
self.ensure_cursor_visible(area);
return true;
}
KeyCode::Char('z') if has_control => {
if let Some(prev) = self.history.pop() {
self.redo_stack.push(self.lines.clone());
self.lines = prev;
self.cursor_row = std::cmp::min(self.cursor_row, self.lines.len() - 1);
self.cursor_col =
std::cmp::min(self.cursor_col, self.lines[self.cursor_row].len());
self.clear_selection();
self.modified = true;
self.invalidate_from(0);
self.ensure_cursor_visible(area);
return true;
}
}
KeyCode::Char('y') if has_control => {
if let Some(next) = self.redo_stack.pop() {
self.history.push(self.lines.clone());
self.lines = next;
self.cursor_row = std::cmp::min(self.cursor_row, self.lines.len() - 1);
self.cursor_col =
std::cmp::min(self.cursor_col, self.lines[self.cursor_row].len());
self.clear_selection();
self.modified = true;
self.invalidate_from(0);
self.ensure_cursor_visible(area);
return true;
}
}
KeyCode::Char('a') if has_control => {
self.select_all();
self.ensure_cursor_visible(area);
return true;
}
KeyCode::Char('c') if has_control => {
if let Some(text) = self.get_selected_text() {
set_clipboard_text(&text);
}
return true;
}
KeyCode::Char('x') if has_control => {
if let Some(text) = self.get_selected_text() {
set_clipboard_text(&text);
self.push_history();
self.delete_selection();
self.modified = true;
self.ensure_cursor_visible(area);
}
return true;
}
KeyCode::Char('v') if has_control => {
if let Some(text) = get_clipboard_text() {
self.push_history();
self.insert_string(&text);
self.modified = true;
self.ensure_cursor_visible(area);
}
return true;
}
KeyCode::Char('f') if has_control => {
self.mode = EditorMode::Search;
self.mode_input.clear();
self.is_replacing = false;
return true;
}
KeyCode::Char('h') if has_control => {
self.mode = EditorMode::Replace;
self.mode_input.clear();
self.is_replacing = true;
return true;
}
KeyCode::Char('g') if has_control => {
self.mode = EditorMode::GotoLine;
self.mode_input.clear();
return true;
}
KeyCode::Char('d') if has_control => {
self.push_history();
let current_line = self.lines[self.cursor_row].clone();
self.lines.insert(self.cursor_row + 1, current_line);
self.cursor_row += 1;
self.modified = true;
self.ensure_cursor_visible(area);
return true;
}
KeyCode::Char('k') if has_control => {
self.push_history();
if self.cursor_col >= self.lines[self.cursor_row].len() {
if self.cursor_row < self.lines.len() - 1 {
let next_line = self.lines.remove(self.cursor_row + 1);
self.lines[self.cursor_row].push_str(&next_line);
}
} else {
let line = &mut self.lines[self.cursor_row];
line.truncate(self.cursor_col);
}
self.modified = true;
return true;
}
KeyCode::Char('u') if has_control => {
self.push_history();
let line = &mut self.lines[self.cursor_row];
if self.cursor_col > 0 {
*line = line.split_off(self.cursor_col);
self.cursor_col = 0;
}
self.modified = true;
return true;
}
KeyCode::Char('w') if has_control => {
self.push_history();
if self.selection_start.is_some() {
self.delete_selection();
} else {
self.delete_word_backwards();
}
self.modified = true;
self.ensure_cursor_visible(area);
return true;
}
KeyCode::Enter => {
self.push_history();
if self.selection_start.is_some() {
self.delete_selection();
}
self.insert_newline();
self.modified = true;
self.ensure_cursor_visible(area);
return true;
}
KeyCode::Backspace if has_control || has_alt => {
self.push_history();
if self.selection_start.is_some() {
self.delete_selection();
} else {
self.delete_word_backwards();
}
self.modified = true;
self.ensure_cursor_visible(area);
return true;
}
KeyCode::Backspace => {
if self.selection_start.is_some() {
self.push_history();
self.delete_selection();
self.modified = true;
self.ensure_cursor_visible(area);
return true;
}
if self.delete_backwards() {
self.push_history();
self.modified = true;
self.ensure_cursor_visible(area);
return true;
}
}
KeyCode::Delete if has_control || has_alt => {
self.push_history();
if self.selection_start.is_some() {
self.delete_selection();
} else {
self.delete_word_forwards();
}
self.modified = true;
return true;
}
KeyCode::Delete => {
if self.selection_start.is_some() {
self.push_history();
self.delete_selection();
self.modified = true;
return true;
}
if self.delete_forwards() {
self.push_history();
self.modified = true;
return true;
}
}
KeyCode::Left if has_control || has_alt => {
self.nav_move(has_shift, area, |s| s.move_cursor_word_left());
}
KeyCode::Left => {
self.nav_move(has_shift, area, |s| s.move_cursor_left());
}
KeyCode::Right if has_control || has_alt => {
self.nav_move(has_shift, area, |s| s.move_cursor_word_right());
}
KeyCode::Right => {
self.nav_move(has_shift, area, |s| s.move_cursor_right());
}
KeyCode::Char('b') if has_alt => {
self.nav_move(has_shift, area, |s| s.move_cursor_word_left());
}
KeyCode::Char('f') if has_alt => {
self.nav_move(has_shift, area, |s| s.move_cursor_word_right());
}
KeyCode::Up | KeyCode::Char('p') if has_control => {
self.nav_move(has_shift, area, |s| s.move_cursor_up());
}
KeyCode::Down | KeyCode::Char('n') if has_control => {
self.nav_move(has_shift, area, |s| s.move_cursor_down());
}
KeyCode::Up => {
if has_alt {
self.move_line_up();
self.ensure_cursor_visible(area);
return true;
}
self.nav_move(has_shift, area, |s| s.move_cursor_up());
}
KeyCode::Down => {
if has_alt {
self.move_line_down();
self.ensure_cursor_visible(area);
return true;
}
self.nav_move(has_shift, area, |s| s.move_cursor_down());
}
KeyCode::Char('j') if has_control && has_alt => {
if self.cursor_row < self.lines.len() - 1 {
self.add_cursor(self.cursor_row + 1, self.cursor_col);
self.ensure_cursor_visible(area);
}
return true;
}
KeyCode::Char('k') if has_control && has_alt => {
if self.cursor_row > 0 {
self.add_cursor(self.cursor_row - 1, self.cursor_col);
self.ensure_cursor_visible(area);
}
return true;
}
KeyCode::Char('d') if has_control && has_alt => {
self.clear_extra_cursors();
return true;
}
KeyCode::Char('d') if has_control => {
if has_shift {
self.maybe_start_selection();
}
self.cursor_row = 0;
self.cursor_col = 0;
self.finish_nav_move(has_shift, area);
}
KeyCode::End if has_control => {
if has_shift {
self.maybe_start_selection();
}
self.cursor_row = self.lines.len().saturating_sub(1);
self.cursor_col = self.lines[self.cursor_row].len();
self.finish_nav_move(has_shift, area);
}
KeyCode::Home => {
if has_shift {
self.maybe_start_selection();
}
let line = &self.lines[self.cursor_row];
let first_non_whitespace = line
.chars()
.enumerate()
.find(|(_, c)| !c.is_whitespace())
.map(|(i, _)| i)
.unwrap_or(0);
if self.cursor_col == first_non_whitespace {
self.cursor_col = 0;
} else {
self.cursor_col = first_non_whitespace;
}
self.finish_nav_move(has_shift, area);
}
KeyCode::End => {
if has_shift {
self.maybe_start_selection();
}
self.cursor_col = self.lines[self.cursor_row].len();
self.finish_nav_move(has_shift, area);
}
KeyCode::PageUp => {
if has_shift {
self.maybe_start_selection();
}
self.cursor_row = self.cursor_row.saturating_sub(area.height as usize);
self.finish_nav_move(has_shift, area);
}
KeyCode::PageDown => {
if has_shift {
self.maybe_start_selection();
}
self.cursor_row = std::cmp::min(
self.lines.len().saturating_sub(1),
self.cursor_row + area.height as usize,
);
self.finish_nav_move(has_shift, area);
}
_ => {}
}
}
Event::Mouse(mouse) => {
return self.handle_mouse_event(*mouse, area);
}
_ => {}
}
false
}
pub fn handle_mouse_event(&mut self, mouse: MouseEvent, area: Rect) -> bool {
if mouse.column < area.x
|| mouse.column >= area.x + area.width
|| mouse.row < area.y
|| mouse.row >= area.y + area.height
{
return false;
}
let gutter = self.gutter_width();
let scrollbar_w = if self.effective_len() > area.height as usize {
1
} else {
0
};
let content_width = area
.width
.saturating_sub(gutter as u16 + scrollbar_w as u16);
let rel_row = (mouse.row - area.y) as usize;
match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => {
if mouse.column >= area.x + area.width.saturating_sub(1) {
let total_lines = if self.wrap {
self.effective_len()
} else {
self.effective_len()
};
let view_height = area.height as usize;
if total_lines > view_height {
let percent = rel_row as f32 / view_height as f32;
self.scroll_row = (percent * total_lines as f32) as usize;
self.scroll_row =
self.scroll_row.min(total_lines.saturating_sub(view_height));
}
return true;
}
let (target_row, target_col) = if self.wrap {
let width = content_width as usize;
let screen_row = self.scroll_row + rel_row;
match self.source_row_from_visual(screen_row, width) {
Some((row, segment_idx, _)) => {
let rel_col = mouse.column.saturating_sub(area.x + gutter as u16) as usize;
let visual_x = segment_idx * width + rel_col;
let byte_idx = self.get_byte_index_from_visual(row, visual_x);
(row, byte_idx)
}
None => (self.cursor_row, self.cursor_col),
}
} else {
let row = self.scroll_row + rel_row;
let col = if mouse.column >= area.x + gutter as u16 {
let rel_col = (mouse.column.saturating_sub(area.x + gutter as u16)) as usize;
let target_visual = self.scroll_col + rel_col;
self.get_byte_index_from_visual(row, target_visual)
} else {
0
};
(row, col)
};
if target_row < self.effective_len() {
if self.is_inside_selection(target_row, target_col) {
self.is_dragging_selection = true;
self.is_selecting = false;
self.cursor_row = target_row;
self.cursor_col = target_col;
return true;
}
self.cursor_row = target_row;
self.cursor_col = target_col;
self.selection_start = Some((self.cursor_row, self.cursor_col));
self.selection_end = Some((self.cursor_row, self.cursor_col));
self.is_selecting = true;
self.is_dragging_selection = false;
return true;
}
}
MouseEventKind::Drag(MouseButton::Left) => {
if mouse.column >= area.x + area.width.saturating_sub(1) {
let total_lines = self.effective_len();
let view_height = area.height as usize;
if total_lines > view_height {
let percent = rel_row as f32 / view_height as f32;
self.scroll_row = (percent * total_lines as f32) as usize;
self.scroll_row =
self.scroll_row.min(total_lines.saturating_sub(view_height));
}
return true;
}
let (target_row, target_col) = if self.wrap {
let width = content_width as usize;
let screen_row = self.scroll_row + rel_row;
match self.source_row_from_visual(screen_row, width) {
Some((row, segment_idx, _)) => {
let rel_col =
(mouse.column.saturating_sub(area.x + gutter as u16)) as usize;
let visual_x = segment_idx * width + rel_col;
let byte_idx = self.get_byte_index_from_visual(row, visual_x);
(row, byte_idx)
}
None => (self.cursor_row, self.cursor_col),
}
} else {
let row = self.scroll_row + rel_row;
let col = if mouse.column >= area.x + gutter as u16 {
let rel_col = (mouse.column.saturating_sub(area.x + gutter as u16)) as usize;
let target_visual = self.scroll_col + rel_col;
self.get_byte_index_from_visual(row, target_visual)
} else {
0
};
(row, col)
};
if target_row < self.effective_len() {
self.cursor_row = target_row;
self.cursor_col = target_col;
if self.is_selecting {
self.selection_end = Some((self.cursor_row, self.cursor_col));
}
return true;
}
}
MouseEventKind::Up(MouseButton::Left) => {
if self.is_dragging_selection {
let r = self.cursor_row;
let c = self.cursor_col;
self.move_selection_to(r, c);
self.is_dragging_selection = false;
return true;
}
if self.is_selecting {
self.is_selecting = false;
if self.selection_start == self.selection_end {
self.selection_start = None;
self.selection_end = None;
}
}
return true;
}
MouseEventKind::ScrollUp => {
self.scroll_row = self.scroll_row.saturating_sub(5);
return true;
}
MouseEventKind::ScrollDown => {
let total_lines = if self.wrap {
self.effective_len() * 2
} else {
self.effective_len()
};
let max_scroll = total_lines.saturating_sub(area.height as usize);
self.scroll_row = std::cmp::min(max_scroll, self.scroll_row + 5);
return true;
}
_ => {}
}
false
}
pub fn push_history(&mut self) {
if let Some(last) = self.history.last() {
if last == &self.lines {
return;
}
}
self.history.push(self.lines.clone());
if self.history.len() > 100 {
self.history.remove(0);
}
self.redo_stack.clear();
}
fn delete_word_backwards(&mut self) {
if self.cursor_col == 0 && self.cursor_row == 0 {
return;
}
if self.cursor_col == 0 {
self.delete_backwards();
return;
}
let line = &self.lines[self.cursor_row];
let mut i = self.cursor_col;
while i > 0 {
if let Some(prev) = line[..i].chars().next_back() {
if prev.is_whitespace() {
i -= prev.len_utf8();
} else {
break;
}
} else {
break;
}
}
while i > 0 {
if let Some(prev) = line[..i].chars().next_back() {
if !prev.is_whitespace() {
i -= prev.len_utf8();
} else {
break;
}
} else {
break;
}
}
let to_remove_bytes = self.cursor_col - i;
for _ in 0..to_remove_bytes {
self.delete_backwards();
}
}
fn delete_word_forwards(&mut self) {
let line = &self.lines[self.cursor_row];
if self.cursor_col >= line.len() && self.cursor_row >= self.lines.len() - 1 {
return;
}
if self.cursor_col >= line.len() {
self.delete_forwards();
return;
}
let mut i = self.cursor_col;
while i < line.len() {
if let Some(next) = line[i..].chars().next() {
if next.is_whitespace() {
i += next.len_utf8();
} else {
break;
}
} else {
break;
}
}
while i < line.len() {
if let Some(next) = line[i..].chars().next() {
if !next.is_whitespace() {
i += next.len_utf8();
} else {
break;
}
} else {
break;
}
}
let to_remove_bytes = i - self.cursor_col;
for _ in 0..to_remove_bytes {
self.delete_forwards();
}
}
fn move_cursor_word_left(&mut self) {
if self.cursor_col == 0 {
self.move_cursor_left();
return;
}
let line = &self.lines[self.cursor_row];
let mut i = self.cursor_col;
while i > 0 {
if let Some(prev) = line[..i].chars().next_back() {
if prev.is_whitespace() {
i -= prev.len_utf8();
} else {
break;
}
} else {
break;
}
}
while i > 0 {
if let Some(prev) = line[..i].chars().next_back() {
if !prev.is_whitespace() {
i -= prev.len_utf8();
} else {
break;
}
} else {
break;
}
}
self.cursor_col = i;
}
fn move_cursor_word_right(&mut self) {
let line = &self.lines[self.cursor_row];
if self.cursor_col >= line.len() {
self.move_cursor_right();
return;
}
let mut i = self.cursor_col;
while i < line.len() {
if let Some(next) = line[i..].chars().next() {
if next.is_whitespace() {
i += next.len_utf8();
} else {
break;
}
} else {
break;
}
}
while i < line.len() {
if let Some(next) = line[i..].chars().next() {
if !next.is_whitespace() {
i += next.len_utf8();
} else {
break;
}
} else {
break;
}
}
self.cursor_col = i;
}
fn insert_char(&mut self, c: char) {
if c == '\x1b' {
return;
}
let all_cursors: Vec<(usize, usize)> = std::iter::once((self.cursor_row, self.cursor_col))
.chain(self.extra_cursors.iter().cloned())
.collect();
let pairs: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}')];
let _is_opening = pairs.iter().any(|&(o, _)| o == c);
let mut affected_rows: Vec<usize> = Vec::new();
for &(row, col) in &all_cursors {
let line = &mut self.lines[row];
let mut insert_col = col;
let mut inserted_pair = false;
for &(open, close) in pairs {
if c == open {
line.insert(insert_col, c);
insert_col += c.len_utf8();
line.insert(insert_col, close);
inserted_pair = true;
break;
}
}
if !inserted_pair {
line.insert(insert_col, c);
}
if !affected_rows.contains(&row) {
affected_rows.push(row);
}
}
self.modified = true;
for row in &affected_rows {
self.invalidate_from(*row);
}
if let Some(&(row, col)) = all_cursors.first() {
if row == self.cursor_row && col == self.cursor_col {
self.cursor_col += c.len_utf8();
}
}
if let Some(&(last_row, _)) = all_cursors.last() {
self.cursor_row = last_row;
}
}
fn find_matching_bracket(&self, row: usize, col: usize) -> Option<(usize, usize)> {
if row >= self.lines.len() {
return None;
}
let line = &self.lines[row];
if col >= line.len() {
return None;
}
let c = line.chars().nth(col)?;
let pairs: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}')];
for &(open, close) in pairs {
if c == open {
return self.find_closing_bracket(row, col, open, close);
} else if c == close {
return self.find_opening_bracket(row, col, open, close);
}
}
None
}
fn find_closing_bracket(
&self,
row: usize,
col: usize,
open: char,
close: char,
) -> Option<(usize, usize)> {
let mut depth = 1;
let mut r = row;
let mut c = col + open.len_utf8();
while r < self.lines.len() {
let line = &self.lines[r];
while c < line.len() {
let ch = line.chars().nth(c)?;
if ch == open {
depth += 1;
} else if ch == close {
depth -= 1;
if depth == 0 {
return Some((r, c));
}
}
c += ch.len_utf8();
}
r += 1;
c = 0;
}
None
}
fn find_opening_bracket(
&self,
row: usize,
col: usize,
open: char,
close: char,
) -> Option<(usize, usize)> {
let mut depth = 1;
let mut r = row;
let mut c = col.saturating_sub(1);
while r < self.lines.len() {
let line = &self.lines[r];
while c > 0 {
let ch = line.chars().nth(c)?;
if ch == close {
depth += 1;
} else if ch == open {
depth -= 1;
if depth == 0 {
return Some((r, c));
}
}
if c == 0 {
break;
}
c -= 1;
}
if r == 0 {
break;
}
r -= 1;
c = self.lines[r].len();
}
None
}
pub fn ensure_valid_cursor_col(&mut self) {
if self.cursor_row >= self.lines.len() {
self.cursor_row = self.lines.len().saturating_sub(1);
}
let line = &self.lines[self.cursor_row];
if self.cursor_col > line.len() {
self.cursor_col = line.len();
}
while !line.is_char_boundary(self.cursor_col) {
self.cursor_col = self.cursor_col.saturating_sub(1);
}
}
fn insert_newline(&mut self) {
self.ensure_valid_cursor_col();
let line = &self.lines[self.cursor_row];
let indentation = line
.chars()
.take_while(|c| c.is_whitespace())
.collect::<String>();
let line = &mut self.lines[self.cursor_row];
let remaining = line.split_off(self.cursor_col);
let mut new_line = indentation.clone();
new_line.push_str(&remaining);
self.lines.insert(self.cursor_row + 1, new_line);
self.invalidate_from(self.cursor_row);
self.cursor_row += 1;
self.cursor_col = indentation.len();
}
fn insert_newline_raw(&mut self) {
self.ensure_valid_cursor_col();
let line = &mut self.lines[self.cursor_row];
let remaining = line.split_off(self.cursor_col);
self.lines.insert(self.cursor_row + 1, remaining);
self.invalidate_from(self.cursor_row);
self.cursor_row += 1;
self.cursor_col = 0;
}
fn delete_backwards(&mut self) -> bool {
self.ensure_valid_cursor_col();
if self.cursor_col > 0 {
let line = &mut self.lines[self.cursor_row];
let prefix = &line[..self.cursor_col];
if self.cursor_col >= 4
&& self.cursor_col.is_multiple_of(4)
&& prefix.chars().all(|c| c == ' ')
{
for _ in 0..4 {
line.remove(self.cursor_col - 1);
self.cursor_col -= 1;
}
} else {
if let Some(c) = line[..self.cursor_col].chars().next_back() {
let len = c.len_utf8();
line.remove(self.cursor_col - len);
self.cursor_col -= len;
}
}
self.modified = true;
self.invalidate_from(self.cursor_row);
true
} else if self.cursor_row > 0 {
let current_line = self.lines.remove(self.cursor_row);
self.cursor_row -= 1;
self.cursor_col = self.lines[self.cursor_row].len();
self.lines[self.cursor_row].push_str(¤t_line);
self.modified = true;
self.invalidate_from(self.cursor_row);
true
} else {
false
}
}
fn delete_forwards(&mut self) -> bool {
self.ensure_valid_cursor_col();
let line = &mut self.lines[self.cursor_row];
if self.cursor_col < line.len() {
if let Some(_c) = line[self.cursor_col..].chars().next() {
line.remove(self.cursor_col); }
self.invalidate_from(self.cursor_row);
true
} else if self.cursor_row < self.lines.len() - 1 {
let next_line = self.lines.remove(self.cursor_row + 1);
self.lines[self.cursor_row].push_str(&next_line);
self.invalidate_from(self.cursor_row);
true
} else {
false
}
}
fn move_cursor_left(&mut self) {
if self.cursor_col > 0 {
let line = self.get_effective_line(self.cursor_row);
let mut i = self.cursor_col;
while i > 0 {
i -= 1;
if line.is_char_boundary(i) {
break;
}
}
self.cursor_col = i;
} else if self.cursor_row > 0 {
self.cursor_row -= 1;
self.cursor_col = self.get_effective_line(self.cursor_row).len();
}
}
fn move_cursor_right(&mut self) {
let line = self.get_effective_line(self.cursor_row);
if self.cursor_col < line.len() {
let mut i = self.cursor_col + 1;
while i <= line.len() {
if line.is_char_boundary(i) {
break;
}
i += 1;
}
self.cursor_col = i;
} else if self.cursor_row < self.effective_len().saturating_sub(1) {
self.cursor_row += 1;
self.cursor_col = 0;
}
}
fn move_cursor_up(&mut self) {
if self.cursor_row > 0 {
let current_visual = self.get_visual_x(self.cursor_row, self.cursor_col);
self.cursor_row -= 1;
self.cursor_col = self.get_byte_index_from_visual(self.cursor_row, current_visual);
}
}
fn move_cursor_down(&mut self) {
if self.cursor_row < self.effective_len().saturating_sub(1) {
let current_visual = self.get_visual_x(self.cursor_row, self.cursor_col);
self.cursor_row += 1;
self.cursor_col = self.get_byte_index_from_visual(self.cursor_row, current_visual);
}
}
pub fn move_line_up(&mut self) {
if self.cursor_row > 0 {
self.push_history();
let line = self.lines.remove(self.cursor_row);
self.lines.insert(self.cursor_row - 1, line);
self.cursor_row -= 1;
self.modified = true;
self.invalidate_from(0);
}
}
pub fn move_line_down(&mut self) {
if self.cursor_row < self.lines.len().saturating_sub(1) {
self.push_history();
let line = self.lines.remove(self.cursor_row);
self.lines.insert(self.cursor_row + 1, line);
self.cursor_row += 1;
self.modified = true;
self.invalidate_from(0);
}
}
pub fn ensure_cursor_centered(&mut self, area: Rect) {
let height = area.height as usize;
let target_scroll = self.cursor_row.saturating_sub(height / 2);
let max_scroll = self.effective_len().saturating_sub(height);
self.scroll_row = std::cmp::min(target_scroll, max_scroll);
if self.wrap {
self.scroll_col = 0;
return;
}
let gutter = self.gutter_width();
let width = area.width.saturating_sub(gutter as u16) as usize;
let visual_cursor_x = self.get_visual_x(self.cursor_row, self.cursor_col);
if visual_cursor_x < self.scroll_col {
self.scroll_col = visual_cursor_x;
} else if visual_cursor_x >= self.scroll_col + width {
self.scroll_col = visual_cursor_x - width + 1;
}
}
pub fn get_visual_row_at(&self, row: usize, width: usize) -> usize {
let mut visual_row = 0;
for i in 0..row {
let line = self.get_effective_line(i);
if self.wrap && width > 0 {
let w = line.width();
if w == 0 {
visual_row += 1;
} else {
visual_row += (w.saturating_sub(1) / width) + 1;
}
} else {
visual_row += 1;
}
}
visual_row
}
pub fn get_cursor_visual_row(&self, width: usize) -> usize {
let mut visual_row = self.get_visual_row_at(self.cursor_row, width);
if self.wrap && width > 0 {
let line = self.get_effective_line(self.cursor_row);
let cursor_x = line[..self.cursor_col].width();
visual_row += cursor_x / width;
}
visual_row
}
fn source_row_from_visual(
&self,
visual_row: usize,
width: usize,
) -> Option<(usize, usize, usize)> {
let mut current = 0usize;
for i in 0..self.effective_len() {
let line = self.get_effective_line(i);
let w = line.width();
let segments = if w == 0 || width == 0 {
1
} else {
(w.saturating_sub(1) / width) + 1
};
if visual_row >= current && visual_row < current + segments {
return Some((i, visual_row - current, segments));
}
current += segments;
}
None
}
pub fn ensure_cursor_visible(&mut self, area: Rect) {
let height = area.height as usize;
let gutter = self.gutter_width();
let scrollbar_w = if self.effective_len() > area.height as usize {
1
} else {
0
};
let width = area
.width
.saturating_sub(gutter as u16 + scrollbar_w as u16) as usize;
if self.wrap {
let visual_row = self.get_cursor_visual_row(width);
if visual_row < self.scroll_row {
self.scroll_row = visual_row;
} else if visual_row >= self.scroll_row + height {
self.scroll_row = visual_row - height + 1;
}
self.scroll_col = 0;
return;
}
let margin = (height / 4).min(3);
if self.cursor_row < self.scroll_row + margin {
self.scroll_row = self.cursor_row.saturating_sub(margin);
} else if self.cursor_row >= self.scroll_row + height.saturating_sub(margin) {
let target_scroll = (self.cursor_row + margin + 1).saturating_sub(height);
let max_scroll = self.effective_len().saturating_sub(height);
self.scroll_row = std::cmp::min(target_scroll, max_scroll);
}
let visual_cursor_x = self.get_visual_x(self.cursor_row, self.cursor_col);
if visual_cursor_x < self.scroll_col {
self.scroll_col = visual_cursor_x;
} else if visual_cursor_x >= self.scroll_col + width {
self.scroll_col = (visual_cursor_x + 1).saturating_sub(width);
}
}
pub fn get_selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
let start = self.selection_start?;
let end = self.selection_end?;
if start <= end {
Some((start, end))
} else {
Some((end, start))
}
}
pub fn maybe_start_selection(&mut self) {
if self.selection_start.is_none() {
self.selection_start = Some((self.cursor_row, self.cursor_col));
}
}
pub fn update_selection_end(&mut self) {
self.selection_end = Some((self.cursor_row, self.cursor_col));
}
pub fn clear_selection(&mut self) {
self.selection_start = None;
self.selection_end = None;
self.is_selecting = false;
self.is_dragging_selection = false;
}
pub fn is_inside_selection(&self, row: usize, byte_col: usize) -> bool {
if let Some(((s_row, s_col), (e_row, e_col))) = self.get_selection_range() {
if row > s_row && row < e_row {
return true;
}
if row == s_row && row == e_row {
return byte_col >= s_col && byte_col < e_col;
}
if row == s_row {
return byte_col >= s_col;
}
if row == e_row {
return byte_col < e_col;
}
}
false
}
pub fn get_selected_text(&self) -> Option<String> {
let ((s_row, s_col), (e_row, e_col)) = self.get_selection_range()?;
let mut result = String::new();
for row in s_row..=e_row {
if row >= self.effective_len() {
break;
}
let line = self.get_effective_line(row);
let start = if row == s_row { s_col } else { 0 };
let end = if row == e_row { e_col } else { line.len() };
let safe_start = std::cmp::min(start, line.len());
let safe_end = std::cmp::min(end, line.len());
if safe_start < safe_end {
result.push_str(&line[safe_start..safe_end]);
}
if row < e_row {
result.push('\n');
}
}
Some(result)
}
pub fn delete_selection(&mut self) {
if let Some(((s_row, s_col), (e_row, e_col))) = self.get_selection_range() {
if s_row == e_row {
let line = &mut self.lines[s_row];
if s_col < line.len() && e_col <= line.len() {
line.replace_range(s_col..e_col, "");
}
} else {
let start_part = if s_col < self.lines[s_row].len() {
self.lines[s_row][..s_col].to_string()
} else {
self.lines[s_row].clone()
};
let end_part = if e_col < self.lines[e_row].len() {
self.lines[e_row][e_col..].to_string()
} else {
String::new()
};
self.lines.drain(s_row + 1..=e_row);
self.lines[s_row] = format!("{}{}", start_part, end_part);
}
self.cursor_row = s_row;
self.cursor_col = s_col;
self.selection_start = None;
self.selection_end = None;
self.is_selecting = false;
self.modified = true;
self.invalidate_from(0);
}
}
pub fn move_selection_to(&mut self, target_row: usize, target_col: usize) {
let text = if let Some(t) = self.get_selected_text() {
t
} else {
return;
};
let ((s_row, s_col), (e_row, e_col)) = if let Some(range) = self.get_selection_range() {
range
} else {
return;
};
self.push_history();
let mut new_row = target_row;
let mut new_col = target_col;
if target_row > e_row {
new_row -= e_row - s_row;
} else if target_row == e_row && target_col >= e_col {
new_row = s_row;
new_col = s_col + (target_col - e_col);
} else if target_row == s_row && target_col >= s_col {
new_row = s_row;
new_col = s_col;
} else if target_row > s_row && target_row < e_row {
new_row = s_row;
new_col = s_col;
}
self.delete_selection();
self.cursor_row = new_row;
self.cursor_col = new_col;
self.insert_string(&text);
}
pub fn insert_string(&mut self, text: &str) {
self.push_history();
if self.selection_start.is_some() {
self.delete_selection();
}
for (i, part) in text.split('\n').enumerate() {
if i > 0 {
self.insert_newline_raw();
}
for c in part.chars() {
if c != '\r' {
self.insert_char(c);
}
}
}
self.invalidate_from(0);
}
pub fn select_all(&mut self) {
self.selection_start = Some((0, 0));
let last_row = self.lines.len().saturating_sub(1);
let last_col = self.lines[last_row].len();
self.selection_end = Some((last_row, last_col));
self.cursor_row = last_row;
self.cursor_col = last_col;
self.is_selecting = false;
}
pub fn select_word_at(&mut self, row: usize, col: usize) {
if row >= self.lines.len() {
return;
}
let line = &self.lines[row];
if col > line.len() {
return;
}
let mut start = col;
let mut end = col;
while start > 0 {
if let Some(prev) = line[..start].chars().next_back() {
if prev.is_alphanumeric() || prev == '_' {
start -= prev.len_utf8();
} else {
break;
}
} else {
break;
}
}
while end < line.len() {
if let Some(next) = line[end..].chars().next() {
if next.is_alphanumeric() || next == '_' {
end += next.len_utf8();
} else {
break;
}
} else {
break;
}
}
if start < end {
self.selection_start = Some((row, start));
self.selection_end = Some((row, end));
self.cursor_row = row;
self.cursor_col = end;
self.is_selecting = false;
}
}
pub fn select_line_at(&mut self, row: usize) {
if row >= self.lines.len() {
return;
}
let line_len = self.lines[row].len();
self.selection_start = Some((row, 0));
self.selection_end = Some((row, line_len));
self.cursor_row = row;
self.cursor_col = line_len;
self.is_selecting = false;
}
pub fn delete_line(&mut self, row: usize) {
if self.lines.len() > 1 {
self.push_history();
self.lines.remove(row);
self.cursor_row = std::cmp::min(self.cursor_row, self.lines.len() - 1);
self.cursor_col = std::cmp::min(self.cursor_col, self.lines[self.cursor_row].len());
self.modified = true;
self.invalidate_from(0);
} else {
self.push_history();
self.lines[0].clear();
self.cursor_col = 0;
self.modified = true;
self.invalidate_from(0);
}
}
}
impl Widget for &TextEditor {
fn render(self, area: Rect, buf: &mut Buffer) {
let gutter_w = self.gutter_width();
let status_bar_h: u16 = if self.show_status_bar && area.height >= 2 {
1
} else {
0
};
let scrollbar_w = if self.effective_len()
> (area.height as usize).saturating_sub(status_bar_h as usize)
{
1
} else {
0
};
let content_area = Rect::new(
area.x + gutter_w as u16,
area.y,
area.width
.saturating_sub(gutter_w as u16 + scrollbar_w as u16),
area.height.saturating_sub(status_bar_h),
);
let mut highlighted = {
let mut first_invalid = self.first_invalid_line.borrow_mut();
let mut cache = self.highlighted_cache.borrow_mut();
if let Some(start_line) = *first_invalid {
let content_string = if !self.filter_query.is_empty() {
self.filtered_indices
.iter()
.map(|&i| self.lines[i].as_str())
.collect::<Vec<_>>()
.join("\n")
} else {
self.lines.join("\n")
};
if !self.filter_query.is_empty() || start_line == 0 {
let h_lines = highlight_code(&content_string, &self.language);
*cache = h_lines
.into_iter()
.map(|line| {
let spans: Vec<Span<'static>> = line
.spans
.into_iter()
.map(|span| Span::styled(span.content.to_string(), span.style))
.collect();
Line::from(spans)
})
.collect();
} else {
let h_lines = highlight_code(&content_string, &self.language);
*cache = h_lines
.into_iter()
.map(|line| {
let spans: Vec<Span<'static>> = line
.spans
.into_iter()
.map(|span| Span::styled(span.content.to_string(), span.style))
.collect();
Line::from(spans)
})
.collect();
}
*first_invalid = None;
}
cache.clone()
};
while highlighted.len() < self.effective_len() {
highlighted.push(Line::from(""));
}
let mut screen_lines: Vec<(usize, Line)> = Vec::new();
for (line_idx, line) in highlighted.iter().enumerate() {
if self.wrap {
let mut current_spans = Vec::new();
let mut current_width = 0;
let max_width = content_area.width as usize;
for span in &line.spans {
let mut text = span.content.as_ref();
while !text.is_empty() {
let mut available = max_width.saturating_sub(current_width);
if available == 0 {
screen_lines.push((line_idx, Line::from(current_spans.clone())));
current_spans.clear();
current_width = 0;
available = max_width;
}
let mut break_idx = 0;
let mut break_width = 0;
for (idx, c) in text.char_indices() {
let cw = c.width().unwrap_or(0);
if break_width + cw > available {
break;
}
break_idx = idx + c.len_utf8();
break_width += cw;
}
if break_idx == 0 && !text.is_empty() {
let first_char = text.chars().next().unwrap_or('\u{FFFD}');
break_idx = first_char.len_utf8();
break_width = first_char.width().unwrap_or(0);
}
let part = &text[..break_idx];
current_spans.push(Span::styled(part, span.style));
current_width += break_width;
text = &text[break_idx..];
}
}
screen_lines.push((line_idx, Line::from(current_spans)));
} else {
screen_lines.push((line_idx, line.clone()));
}
}
if self.wrap {
let mut current_screen_row = 0;
#[allow(clippy::explicit_counter_loop)]
for (line_idx, (src_idx, line)) in screen_lines.iter().enumerate() {
if current_screen_row >= self.scroll_row
&& current_screen_row < self.scroll_row + area.height as usize
{
let i = current_screen_row - self.scroll_row;
let real_line_idx = self.get_real_line_idx(*src_idx);
let is_current_src = *src_idx == self.cursor_row;
let base_bg = self.style.bg.unwrap_or(Color::Reset);
let line_bg = if is_current_src {
Color::Rgb(20, 20, 25)
} else {
base_bg
};
let bg_area = Rect::new(area.x, area.y + i as u16, area.width, 1);
for x in bg_area.left()..bg_area.right() {
if let Some(cell) = buf.cell_mut((x, bg_area.top())) {
cell.set_bg(line_bg);
}
}
if self.show_line_numbers {
let is_first_wrap_line =
line_idx == 0 || screen_lines[line_idx - 1].0 != *src_idx;
if is_first_wrap_line {
let num = (real_line_idx + 1).to_string();
let gutter_style = if is_current_src {
Style::default()
.fg(Color::Rgb(88, 166, 255))
.add_modifier(Modifier::BOLD)
.bg(line_bg)
} else {
Style::default().fg(Color::Rgb(110, 118, 129)).bg(line_bg)
};
let x = area.x + (gutter_w as u16).saturating_sub(num.len() as u16 + 2);
buf.set_string(x + 1, area.y + i as u16, &num, gutter_style);
}
let sep_style = Style::default().fg(Color::Rgb(48, 54, 61)).bg(line_bg);
buf.set_string(
area.x + gutter_w as u16 - 1,
area.y + i as u16,
"│",
sep_style,
);
}
let mut current_x = content_area.x;
for span in &line.spans {
let mut span_style = self.style.patch(span.style);
if span_style.bg.is_none() || span_style.bg == Some(base_bg) {
span_style.bg = Some(line_bg);
}
buf.set_string(current_x, area.y + i as u16, &span.content, span_style);
current_x += span.content.width() as u16;
}
if let Some(((s_row, s_col), (e_row, e_col))) = self.get_selection_range() {
if real_line_idx >= s_row && real_line_idx <= e_row {
for vx in 0..content_area.width {
let visual_x = vx as usize + self.scroll_col;
let byte_idx = self.get_byte_index_from_visual(*src_idx, visual_x);
let is_selected = if real_line_idx > s_row && real_line_idx < e_row
{
true
} else if real_line_idx == s_row && real_line_idx == e_row {
byte_idx >= s_col && byte_idx < e_col
} else if real_line_idx == s_row {
byte_idx >= s_col
} else if real_line_idx == e_row {
byte_idx < e_col
} else {
false
};
if is_selected {
let cx = content_area.x + vx;
let cy = area.y + i as u16;
if let Some(cell) = buf.cell_mut((cx, cy)) {
cell.set_bg(Color::Rgb(40, 60, 100));
cell.set_fg(Color::White);
}
}
}
}
}
}
current_screen_row += 1;
}
let mut current_screen_row = 0;
let mut cursor_found = false;
for (line_idx, line) in highlighted.iter().enumerate() {
if line_idx > self.cursor_row {
break;
}
let is_cursor_line = line_idx == self.cursor_row;
let mut current_byte_offset = 0;
let mut current_width = 0;
let max_width = content_area.width as usize;
for span in &line.spans {
let mut text = span.content.as_ref();
while !text.is_empty() {
let mut available = max_width.saturating_sub(current_width);
if available == 0 {
current_screen_row += 1;
current_width = 0;
available = max_width;
}
let mut break_idx = 0;
let mut break_width = 0;
for (idx, c) in text.char_indices() {
let cw = c.width().unwrap_or(0);
if break_width + cw > available {
break;
}
break_idx = idx + c.len_utf8();
break_width += cw;
}
if break_idx == 0 && !text.is_empty() {
let first_char = text.chars().next().unwrap_or('\u{FFFD}');
break_idx = first_char.len_utf8();
break_width = first_char.width().unwrap_or(0);
}
if is_cursor_line && !cursor_found {
if self.cursor_col >= current_byte_offset
&& self.cursor_col < current_byte_offset + break_idx
{
let sub_col = self.cursor_col - current_byte_offset;
let visual_offset = text[..sub_col].width();
let cx = content_area
.x
.saturating_add((current_width + visual_offset) as u16);
let cy = (area.y as i32
+ (current_screen_row as i32 - self.scroll_row as i32))
as u16;
if current_screen_row >= self.scroll_row
&& current_screen_row < self.scroll_row + area.height as usize
{
if let Some(cell) = buf.cell_mut((cx, cy)) {
if !cell.symbol().is_empty() && cell.symbol() != " " {
cell.set_style(self.cursor_style);
} else {
cell.set_style(self.cursor_style);
cell.set_symbol(" ");
}
}
}
cursor_found = true;
} else if self.cursor_col == current_byte_offset + break_idx
&& (current_byte_offset + break_idx == line.width()
|| text.len() == break_idx)
{
}
}
current_byte_offset += break_idx;
current_width += break_width;
text = &text[break_idx..];
}
}
if is_cursor_line
&& !cursor_found
&& self.cursor_col == self.get_effective_line(line_idx).len()
{
let cx = content_area.x.saturating_add(current_width as u16);
let cy = (area.y as i32 + (current_screen_row as i32 - self.scroll_row as i32))
as u16;
if current_screen_row >= self.scroll_row
&& current_screen_row < self.scroll_row + area.height as usize
{
if let Some(cell) = buf.cell_mut((cx, cy)) {
cell.set_style(self.cursor_style);
cell.set_symbol(" ");
}
}
cursor_found = true;
}
current_screen_row += 1;
}
if scrollbar_w > 0 {
let sb = Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("▲"))
.end_symbol(Some("▼"));
let total_screen_lines = screen_lines.len();
let mut ss = ScrollbarState::new(total_screen_lines)
.position(self.scroll_row)
.viewport_content_length(area.height as usize);
StatefulWidget::render(sb, area, buf, &mut ss);
}
if status_bar_h > 0 {
let status_y = area.y + area.height - 1;
let status_style = Style::default()
.bg(Color::Rgb(30, 30, 35))
.fg(Color::Rgb(180, 180, 180));
let status_bg = Color::Rgb(30, 30, 35);
for x in area.x..area.x + area.width {
if let Some(cell) = buf.cell_mut((x, status_y)) {
cell.set_bg(status_bg);
cell.set_fg(Color::Rgb(180, 180, 180));
cell.set_symbol(" ");
}
}
let pos_text = format!("Ln {} Col {}", self.cursor_row + 1, self.cursor_col + 1);
let filename_text = self.filename();
let modified_text = if self.modified { " *" } else { "" };
let lang_text = if !self.language.is_empty() {
format!(" [{}]", self.language)
} else {
String::new()
};
let right_text = format!("{}{}", modified_text, lang_text);
if self.mode == EditorMode::Search {
let search_text = format!("Search: {}_", self.mode_input);
let search_style = Style::default()
.bg(Color::Rgb(40, 40, 20))
.fg(Color::Rgb(255, 200, 100));
buf.set_string(area.x, status_y, &search_text, search_style);
buf.set_string(
area.x + area.width.saturating_sub(pos_text.len() as u16),
status_y,
&pos_text,
status_style,
);
} else if self.mode == EditorMode::Replace {
let replace_text = format!("Replace: {}_", self.mode_input);
let replace_style = Style::default()
.bg(Color::Rgb(40, 20, 20))
.fg(Color::Rgb(255, 100, 100));
buf.set_string(area.x, status_y, &replace_text, replace_style);
buf.set_string(
area.x + area.width.saturating_sub(pos_text.len() as u16),
status_y,
&pos_text,
status_style,
);
} else if self.mode == EditorMode::GotoLine {
let goto_text = format!("Goto Line: {}_", self.mode_input);
let goto_style = Style::default()
.bg(Color::Rgb(20, 40, 40))
.fg(Color::Rgb(100, 200, 255));
buf.set_string(area.x, status_y, &goto_text, goto_style);
buf.set_string(
area.x + area.width.saturating_sub(pos_text.len() as u16),
status_y,
&pos_text,
status_style,
);
} else {
buf.set_string(area.x, status_y, &pos_text, status_style);
let right_width = right_text.len() as u16;
let right_x = area.x + area.width.saturating_sub(right_width);
buf.set_string(right_x, status_y, &right_text, status_style);
if !filename_text.is_empty() && filename_text != "Untitled" {
let total_left_len = pos_text.len() as u16;
let total_right_len = right_text.len() as u16;
let available = area
.width
.saturating_sub(total_left_len + total_right_len + 4);
if available > filename_text.len() as u16 {
let center_x = area.x + total_left_len + 2;
let filename_style = Style::default()
.bg(Color::Rgb(30, 30, 35))
.fg(Color::Rgb(120, 200, 255));
buf.set_string(center_x, status_y, &filename_text, filename_style);
}
}
}
if let Some(cell) = buf.cell_mut((area.x, status_y)) {
cell.set_bg(status_bg);
cell.set_fg(Color::Rgb(88, 166, 255));
cell.set_symbol("●");
}
}
return;
}
for (i, line) in highlighted.iter().skip(self.scroll_row).enumerate() {
if i >= area.height as usize {
break;
}
let line_idx = self.scroll_row + i;
let real_line_idx = self.get_real_line_idx(line_idx);
let is_current = line_idx == self.cursor_row;
let base_bg = self.style.bg.unwrap_or(Color::Reset);
let line_bg = if is_current {
Color::Rgb(20, 20, 25)
} else {
base_bg
};
if is_current || base_bg != Color::Reset {
let bg_area = Rect::new(area.x, area.y + i as u16, area.width, 1);
for x in bg_area.left()..bg_area.right() {
if let Some(cell) = buf.cell_mut((x, bg_area.top())) {
cell.set_bg(line_bg);
}
}
}
if self.show_line_numbers {
let num = (real_line_idx + 1).to_string();
let gutter_style = if is_current {
Style::default()
.fg(Color::Rgb(88, 166, 255))
.add_modifier(Modifier::BOLD)
.bg(line_bg)
} else {
Style::default().fg(Color::Rgb(110, 118, 129)).bg(line_bg)
};
let x = area.x + (gutter_w as u16).saturating_sub(num.len() as u16 + 2);
buf.set_string(x + 1, area.y + i as u16, &num, gutter_style);
let sep_style = Style::default().fg(Color::Rgb(48, 54, 61)).bg(line_bg);
buf.set_string(
area.x + gutter_w as u16 - 1,
area.y + i as u16,
"│",
sep_style,
);
}
if self.show_indent_guides {
let indent_width = 4;
let max_indent = (content_area.width as usize / indent_width).min(16);
for indent_level in 1..=max_indent {
let indent_col = indent_level * indent_width;
if indent_col > self.scroll_col {
let visual_x = indent_col - self.scroll_col;
if visual_x < content_area.width as usize {
let guide_x = content_area.x + visual_x as u16;
let guide_style =
Style::default().fg(Color::Rgb(50, 55, 65)).bg(line_bg);
buf.set_string(guide_x, area.y + i as u16, "│", guide_style);
}
}
}
}
let mut current_visual_x = 0;
for span in &line.spans {
let text = span.content.as_ref();
let span_width = text.width();
if current_visual_x + span_width <= self.scroll_col {
current_visual_x += span_width;
continue;
}
if current_visual_x >= self.scroll_col + content_area.width as usize {
break;
}
let mut draw_text = text;
let draw_x = if current_visual_x < self.scroll_col {
let skip_width = self.scroll_col - current_visual_x;
let mut char_indices = text.char_indices();
let mut w = 0;
let mut start_byte = 0;
while w < skip_width {
if let Some((idx, c)) = char_indices.next() {
w += c.width().unwrap_or(0);
start_byte = idx + c.len_utf8();
} else {
break;
}
}
draw_text = &text[start_byte..];
content_area.x + (current_visual_x + w).saturating_sub(self.scroll_col) as u16
} else {
content_area.x + (current_visual_x - self.scroll_col) as u16
};
let mut combined_style = self.style.patch(span.style);
if combined_style.bg.is_none() || combined_style.bg == Some(base_bg) {
combined_style.bg = Some(line_bg);
}
buf.set_string(draw_x, area.y + i as u16, draw_text, combined_style);
current_visual_x += span_width;
}
if let Some(((s_row, s_col), (e_row, e_col))) = self.get_selection_range() {
if real_line_idx >= s_row && real_line_idx <= e_row {
for visual_x in self.scroll_col..(self.scroll_col + content_area.width as usize)
{
let byte_idx = self.get_byte_index_from_visual(line_idx, visual_x);
let is_selected = if real_line_idx > s_row && real_line_idx < e_row {
true
} else if real_line_idx == s_row && real_line_idx == e_row {
byte_idx >= s_col && byte_idx < e_col
} else if real_line_idx == s_row {
byte_idx >= s_col
} else if real_line_idx == e_row {
byte_idx < e_col
} else {
false
};
if is_selected {
let cx = content_area.x + (visual_x - self.scroll_col) as u16;
let cy = area.y + i as u16;
if let Some(cell) = buf.cell_mut((cx, cy)) {
cell.set_bg(Color::Rgb(40, 60, 100)); cell.set_fg(Color::White);
}
}
}
}
}
if let Some((match_row, match_col)) =
self.find_matching_bracket(self.cursor_row, self.cursor_col)
{
if real_line_idx == match_row {
let visual_x = self.get_visual_x(match_row, match_col);
if visual_x >= self.scroll_col
&& visual_x < self.scroll_col + content_area.width as usize
{
let cx = content_area.x + (visual_x - self.scroll_col) as u16;
let cy = area.y + i as u16;
if let Some(cell) = buf.cell_mut((cx, cy)) {
cell.set_bg(Color::Rgb(255, 200, 0)); cell.set_fg(Color::Black);
}
}
}
}
}
if scrollbar_w > 0 {
let sb = Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("▲"))
.end_symbol(Some("▼"));
let mut ss = ScrollbarState::new(self.effective_len())
.position(self.scroll_row)
.viewport_content_length(area.height as usize);
StatefulWidget::render(sb, area, buf, &mut ss);
}
if status_bar_h > 0 {
let status_y = area.y + area.height - 1;
let status_style = Style::default()
.bg(Color::Rgb(30, 30, 35))
.fg(Color::Rgb(180, 180, 180));
let status_bg = Color::Rgb(30, 30, 35);
for x in area.x..area.x + area.width {
if let Some(cell) = buf.cell_mut((x, status_y)) {
cell.set_bg(status_bg);
cell.set_fg(Color::Rgb(180, 180, 180));
cell.set_symbol(" ");
}
}
let pos_text = format!("Ln {} Col {}", self.cursor_row + 1, self.cursor_col + 1);
let filename_text = self.filename();
let modified_text = if self.modified { " *" } else { "" };
let lang_text = if !self.language.is_empty() {
format!(" [{}]", self.language)
} else {
String::new()
};
let right_text = format!("{}{}", modified_text, lang_text);
if self.mode == EditorMode::Search {
let search_text = format!("Search: {}_", self.mode_input);
let search_style = Style::default()
.bg(Color::Rgb(40, 40, 20))
.fg(Color::Rgb(255, 200, 100));
buf.set_string(area.x, status_y, &search_text, search_style);
buf.set_string(
area.x + area.width.saturating_sub(pos_text.len() as u16),
status_y,
&pos_text,
status_style,
);
} else if self.mode == EditorMode::Replace {
let replace_text = format!("Replace: {}_", self.mode_input);
let replace_style = Style::default()
.bg(Color::Rgb(40, 20, 20))
.fg(Color::Rgb(255, 100, 100));
buf.set_string(area.x, status_y, &replace_text, replace_style);
buf.set_string(
area.x + area.width.saturating_sub(pos_text.len() as u16),
status_y,
&pos_text,
status_style,
);
} else if self.mode == EditorMode::GotoLine {
let goto_text = format!("Goto Line: {}_", self.mode_input);
let goto_style = Style::default()
.bg(Color::Rgb(20, 40, 40))
.fg(Color::Rgb(100, 200, 255));
buf.set_string(area.x, status_y, &goto_text, goto_style);
buf.set_string(
area.x + area.width.saturating_sub(pos_text.len() as u16),
status_y,
&pos_text,
status_style,
);
} else {
buf.set_string(area.x, status_y, &pos_text, status_style);
let right_width = right_text.width() as u16;
let right_x = area.x + area.width.saturating_sub(right_width);
buf.set_string(right_x, status_y, &right_text, status_style);
if !filename_text.is_empty() && filename_text != "Untitled" {
let total_left_len = pos_text.len() as u16;
let total_right_len = right_text.len() as u16;
let available = area
.width
.saturating_sub(total_left_len + total_right_len + 4);
if available > filename_text.len() as u16 {
let center_x = area.x + total_left_len + 2;
let filename_style = Style::default()
.bg(Color::Rgb(30, 30, 35))
.fg(Color::Rgb(120, 200, 255));
buf.set_string(center_x, status_y, &filename_text, filename_style);
}
}
}
if let Some(cell) = buf.cell_mut((area.x, status_y)) {
cell.set_bg(status_bg);
cell.set_fg(Color::Rgb(88, 166, 255));
cell.set_symbol("●");
}
}
let cursor_visual_x = self.get_visual_x(self.cursor_row, self.cursor_col);
let cursor_screen_row = self.cursor_row as i16 - self.scroll_row as i16;
let cursor_screen_col = cursor_visual_x as i16 - self.scroll_col as i16;
if cursor_screen_row >= 0
&& cursor_screen_row < area.height as i16
&& cursor_screen_col >= 0
&& cursor_screen_col < content_area.width as i16
{
let cx = content_area.x + cursor_screen_col as u16;
let cy = area.y + cursor_screen_row as u16;
if let Some(cell) = buf.cell_mut((cx, cy)) {
if !cell.symbol().is_empty() && cell.symbol() != " " {
cell.set_style(self.cursor_style);
} else {
cell.set_style(self.cursor_style);
cell.set_symbol(" ");
}
}
}
}
}