prompt and buffer drawing appears functional

This commit is contained in:
2025-05-27 02:41:19 -04:00
parent 45b7a16cae
commit 1e3715d353
9 changed files with 1114 additions and 1122 deletions

View File

@@ -24,7 +24,8 @@ use crate::signal::sig_setup;
use crate::state::source_rc; use crate::state::source_rc;
use crate::prelude::*; use crate::prelude::*;
use clap::Parser; use clap::Parser;
use state::{read_vars, write_vars}; use shopt::FernEditMode;
use state::{read_shopts, read_vars, write_shopts, write_vars};
#[derive(Parser,Debug)] #[derive(Parser,Debug)]
struct FernArgs { struct FernArgs {
@@ -98,7 +99,11 @@ fn fern_interactive() {
let mut readline_err_count: u32 = 0; let mut readline_err_count: u32 = 0;
loop { // Main loop loop { // Main loop
let input = match prompt::read_line() { let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode"))
.unwrap()
.map(|mode| mode.parse::<FernEditMode>().unwrap_or_default())
.unwrap();
let input = match prompt::read_line(edit_mode) {
Ok(line) => { Ok(line) => {
readline_err_count = 0; readline_err_count = 0;
line line

View File

@@ -3,9 +3,9 @@ pub mod highlight;
use std::path::Path; use std::path::Path;
use readline::FernVi; use readline::{FernVi, Readline};
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, state::read_shopts}; use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, shopt::FernEditMode, state::read_shopts};
/// Initialize the line editor /// Initialize the line editor
fn get_prompt() -> ShResult<String> { fn get_prompt() -> ShResult<String> {
@@ -20,8 +20,13 @@ fn get_prompt() -> ShResult<String> {
Ok(format!("\n{}",expand_prompt(&prompt)?)) Ok(format!("\n{}",expand_prompt(&prompt)?))
} }
pub fn read_line() -> ShResult<String> { pub fn read_line(edit_mode: FernEditMode) -> ShResult<String> {
dbg!("hi");
let prompt = get_prompt()?; let prompt = get_prompt()?;
let mut reader = FernVi::new(Some(prompt)); let mut reader: Box<dyn Readline> = match edit_mode {
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))),
FernEditMode::Emacs => todo!()
};
dbg!("there");
reader.readline() reader.readline()
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use std::{collections::HashMap, sync::Mutex}; use std::time::Duration;
use linebuf::{strip_ansi_codes_and_escapes, LineBuf, TermCharBuf}; use linebuf::{strip_ansi_codes_and_escapes, LineBuf};
use mode::{CmdReplay, ViInsert, ViMode, ViNormal}; use mode::{CmdReplay, ViInsert, ViMode, ViNormal};
use term::Terminal; use term::Terminal;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@@ -16,6 +16,11 @@ pub mod vicmd;
pub mod mode; pub mod mode;
pub mod register; pub mod register;
/// Unified interface for different line editing methods
pub trait Readline {
fn readline(&mut self) -> ShResult<String>;
}
pub struct FernVi { pub struct FernVi {
term: Terminal, term: Terminal,
line: LineBuf, line: LineBuf,
@@ -25,91 +30,29 @@ pub struct FernVi {
last_movement: Option<MotionCmd>, last_movement: Option<MotionCmd>,
} }
impl FernVi { impl Readline for FernVi {
pub fn new(prompt: Option<String>) -> Self { fn readline(&mut self) -> ShResult<String> {
let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold)); /*
let line = LineBuf::new().with_initial("The quick brown fox jumps over the lazy dog");//\nThe quick brown fox jumps over the lazy dog\nThe quick brown fox jumps over the lazy dog\n"); self.term.writeln("This is a line!");
Self { self.term.writeln("This is a line!");
term: Terminal::new(), self.term.writeln("This is a line!");
line, let prompt_thing = "prompt thing -> ";
prompt, self.term.write(prompt_thing);
mode: Box::new(ViInsert::new()), let line = "And another!";
last_action: None, let mut iters: usize = 0;
last_movement: None, let mut newlines_written = 0;
} loop {
} iters += 1;
pub fn calculate_prompt_offset(&self) -> usize { for i in 0..iters {
if self.prompt.ends_with('\n') {
return 0
}
strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width()
}
pub fn clear_line(&self) {
let prompt_lines = self.prompt.lines().count();
let last_line_len = strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width();
let buf_lines = if self.prompt.ends_with('\n') {
self.line.count_lines(last_line_len)
} else {
// The prompt does not end with a newline, so one of the buffer's lines overlaps with it
self.line.count_lines(last_line_len).saturating_sub(1)
};
let total = prompt_lines + buf_lines;
self.term.write_bytes(b"\r\n");
self.term.write_bytes(format!("\r\x1b[{total}B").as_bytes());
for _ in 0..total {
self.term.write_bytes(b"\r\x1b[2K\x1b[1A");
}
self.term.write_bytes(b"\r\x1b[2K");
}
pub fn print_buf(&self, refresh: bool) {
if refresh {
self.clear_line()
}
let mut prompt_lines = self.prompt.lines().peekable();
let mut last_line_len = 0;
let lines = self.line.split_lines();
while let Some(line) = prompt_lines.next() {
if prompt_lines.peek().is_none() {
last_line_len = strip_ansi_codes_and_escapes(line).width();
self.term.write(line);
} else {
self.term.writeln(line); self.term.writeln(line);
} }
std::thread::sleep(Duration::from_secs(1));
self.clear_lines(iters,prompt_thing.len() + 1);
} }
let mut lines_iter = lines.into_iter().peekable(); panic!()
*/
let pos = self.term.cursor_pos(); self.print_buf(false)?;
while let Some(line) = lines_iter.next() {
if lines_iter.peek().is_some() {
self.term.writeln(&line);
} else {
self.term.write(&line);
}
}
self.term.move_cursor_to(pos);
let (x, y) = self.line.cursor_display_coords(Some(last_line_len));
if y > 0 {
self.term.write(&format!("\r\x1b[{}B", y));
}
let cursor_x = if y == 0 { x + last_line_len } else { x };
if cursor_x > 0 {
self.term.write(&format!("\r\x1b[{}C", cursor_x));
}
self.term.write(&self.mode.cursor_style());
}
pub fn readline(&mut self) -> ShResult<String> {
self.line.set_first_line_offset(self.calculate_prompt_offset());
let dims = self.term.get_dimensions()?;
self.line.update_term_dims(dims.0, dims.1);
self.print_buf(false);
loop { loop {
let dims = self.term.get_dimensions()?;
self.line.update_term_dims(dims.0, dims.1);
let key = self.term.read_key(); let key = self.term.read_key();
let Some(cmd) = self.mode.handle_key(key) else { let Some(cmd) = self.mode.handle_key(key) else {
@@ -121,9 +64,45 @@ impl FernVi {
} }
self.exec_cmd(cmd.clone())?; self.exec_cmd(cmd.clone())?;
self.print_buf(true); self.print_buf(true)?;
} }
} }
}
impl FernVi {
pub fn new(prompt: Option<String>) -> Self {
let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold));
let line = LineBuf::new().with_initial("The quick brown fox jumps over the lazy dog");//\nThe quick brown fox jumps over the lazy dog\nThe quick brown fox jumps over the lazy dog\n");
let term = Terminal::new();
Self {
term,
line,
prompt,
mode: Box::new(ViInsert::new()),
last_action: None,
last_movement: None,
}
}
pub fn print_buf(&mut self, refresh: bool) -> ShResult<()> {
let (_,width) = self.term.get_dimensions()?;
if refresh {
self.term.unwrite()?;
}
let offset = self.calculate_prompt_offset();
let mut line_buf = self.prompt.clone();
line_buf.push_str(self.line.as_str());
self.term.recorded_write(&line_buf, offset)?;
self.term.position_cursor(self.line.cursor_display_coords(offset,width))?;
Ok(())
}
pub fn calculate_prompt_offset(&self) -> usize {
if self.prompt.ends_with('\n') {
return 0
}
strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width() + 1 // 1 indexed
}
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
if cmd.is_mode_transition() { if cmd.is_mode_transition() {
let count = cmd.verb_count(); let count = cmd.verb_count();

View File

@@ -4,7 +4,6 @@ use std::str::Chars;
use nix::NixPath; use nix::NixPath;
use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
use super::linebuf::TermChar;
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, MotionCmd, RegisterName, TextObj, To, Verb, VerbBuilder, VerbCmd, ViCmd, Word}; use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, MotionCmd, RegisterName, TextObj, To, Verb, VerbBuilder, VerbCmd, ViCmd, Word};
use crate::prelude::*; use crate::prelude::*;
@@ -72,14 +71,8 @@ impl ViInsert {
impl ViMode for ViInsert { impl ViMode for ViInsert {
fn handle_key(&mut self, key: E) -> Option<ViCmd> { fn handle_key(&mut self, key: E) -> Option<ViCmd> {
match key { match key {
E(K::Grapheme(ch), M::NONE) => {
let ch = TermChar::from(ch);
self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(ch)));
self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar));
self.register_and_return()
}
E(K::Char(ch), M::NONE) => { E(K::Char(ch), M::NONE) => {
self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(TermChar::from(ch)))); self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(ch)));
self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar));
self.register_and_return() self.register_and_return()
} }

View File

@@ -1,20 +1,18 @@
use std::sync::Mutex; use std::sync::Mutex;
use super::linebuf::TermCharBuf;
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new()); pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
pub fn read_register(ch: Option<char>) -> Option<TermCharBuf> { pub fn read_register(ch: Option<char>) -> Option<String> {
let lock = REGISTERS.lock().unwrap(); let lock = REGISTERS.lock().unwrap();
lock.get_reg(ch).map(|r| r.buf().clone()) lock.get_reg(ch).map(|r| r.buf().clone())
} }
pub fn write_register(ch: Option<char>, buf: TermCharBuf) { pub fn write_register(ch: Option<char>, buf: String) {
let mut lock = REGISTERS.lock().unwrap(); let mut lock = REGISTERS.lock().unwrap();
if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) } if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) }
} }
pub fn append_register(ch: Option<char>, buf: TermCharBuf) { pub fn append_register(ch: Option<char>, buf: String) {
let mut lock = REGISTERS.lock().unwrap(); let mut lock = REGISTERS.lock().unwrap();
if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) } if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) }
} }
@@ -53,33 +51,33 @@ pub struct Registers {
impl Registers { impl Registers {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
default: Register(TermCharBuf(vec![])), default: Register(String::new()),
a: Register(TermCharBuf(vec![])), a: Register(String::new()),
b: Register(TermCharBuf(vec![])), b: Register(String::new()),
c: Register(TermCharBuf(vec![])), c: Register(String::new()),
d: Register(TermCharBuf(vec![])), d: Register(String::new()),
e: Register(TermCharBuf(vec![])), e: Register(String::new()),
f: Register(TermCharBuf(vec![])), f: Register(String::new()),
g: Register(TermCharBuf(vec![])), g: Register(String::new()),
h: Register(TermCharBuf(vec![])), h: Register(String::new()),
i: Register(TermCharBuf(vec![])), i: Register(String::new()),
j: Register(TermCharBuf(vec![])), j: Register(String::new()),
k: Register(TermCharBuf(vec![])), k: Register(String::new()),
l: Register(TermCharBuf(vec![])), l: Register(String::new()),
m: Register(TermCharBuf(vec![])), m: Register(String::new()),
n: Register(TermCharBuf(vec![])), n: Register(String::new()),
o: Register(TermCharBuf(vec![])), o: Register(String::new()),
p: Register(TermCharBuf(vec![])), p: Register(String::new()),
q: Register(TermCharBuf(vec![])), q: Register(String::new()),
r: Register(TermCharBuf(vec![])), r: Register(String::new()),
s: Register(TermCharBuf(vec![])), s: Register(String::new()),
t: Register(TermCharBuf(vec![])), t: Register(String::new()),
u: Register(TermCharBuf(vec![])), u: Register(String::new()),
v: Register(TermCharBuf(vec![])), v: Register(String::new()),
w: Register(TermCharBuf(vec![])), w: Register(String::new()),
x: Register(TermCharBuf(vec![])), x: Register(String::new()),
y: Register(TermCharBuf(vec![])), y: Register(String::new()),
z: Register(TermCharBuf(vec![])), z: Register(String::new()),
} }
} }
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> { pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
@@ -153,17 +151,16 @@ impl Registers {
} }
#[derive(Clone,Default,Debug)] #[derive(Clone,Default,Debug)]
pub struct Register(TermCharBuf); pub struct Register(String);
impl Register { impl Register {
pub fn buf(&self) -> &TermCharBuf { pub fn buf(&self) -> &String {
&self.0 &self.0
} }
pub fn write(&mut self, buf: TermCharBuf) { pub fn write(&mut self, buf: String) {
self.0 = buf self.0 = buf
} }
pub fn append(&mut self, mut buf: TermCharBuf) { pub fn append(&mut self, buf: String) {
self.0.0.append(&mut buf.0) self.0.push_str(&buf)
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.0.clear() self.0.clear()

View File

@@ -1,6 +1,7 @@
use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}}; use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}};
use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::{self, STDIN_FILENO}, sys::termios, unistd::{isatty, read, write}}; use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::{self, STDIN_FILENO}, sys::termios, unistd::{isatty, read, write}};
use nix::libc::{winsize, TIOCGWINSZ}; use nix::libc::{winsize, TIOCGWINSZ};
use unicode_width::UnicodeWidthChar;
use std::mem::zeroed; use std::mem::zeroed;
use std::io; use std::io;
@@ -8,10 +9,20 @@ use crate::libsh::error::ShResult;
use super::keys::{KeyCode, KeyEvent, ModKeys}; use super::keys::{KeyCode, KeyEvent, ModKeys};
#[derive(Default,Debug)]
struct WriteMap {
lines: usize,
cols: usize,
offset: usize
}
#[derive(Debug)] #[derive(Debug)]
pub struct Terminal { pub struct Terminal {
stdin: RawFd, stdin: RawFd,
stdout: RawFd, stdout: RawFd,
recording: bool,
write_records: WriteMap,
cursor_records: WriteMap
} }
impl Terminal { impl Terminal {
@@ -20,6 +31,13 @@ impl Terminal {
Self { Self {
stdin: STDIN_FILENO, stdin: STDIN_FILENO,
stdout: 1, stdout: 1,
recording: false,
// Records for buffer writes
// Used to find the start of the buffer
write_records: WriteMap::default(),
// Records for cursor movements after writes
// Used to find the end of the buffer
cursor_records: WriteMap::default(),
} }
} }
@@ -53,15 +71,24 @@ impl Terminal {
Ok((ws.ws_row as usize, ws.ws_col as usize)) Ok((ws.ws_row as usize, ws.ws_col as usize))
} }
pub fn save_cursor_pos(&self) { pub fn start_recording(&mut self, offset: usize) {
self.recording = true;
self.write_records.offset = offset;
}
pub fn stop_recording(&mut self) {
self.recording = false;
}
pub fn save_cursor_pos(&mut self) {
self.write("\x1b[s") self.write("\x1b[s")
} }
pub fn restore_cursor_pos(&self) { pub fn restore_cursor_pos(&mut self) {
self.write("\x1b[u") self.write("\x1b[u")
} }
pub fn move_cursor_to(&self, (row,col): (usize,usize)) { pub fn move_cursor_to(&mut self, (row,col): (usize,usize)) {
self.write(&format!("\x1b[{row};{col}H",)) self.write(&format!("\x1b[{row};{col}H",))
} }
@@ -118,7 +145,7 @@ impl Terminal {
return n return n
} }
Ok(_) => {} Ok(_) => {}
Err(e) if e == Errno::EAGAIN => {} Err(Errno::EAGAIN) => {}
Err(e) => panic!("nonblocking read failed: {e}") Err(e) => panic!("nonblocking read failed: {e}")
} }
@@ -142,23 +169,126 @@ impl Terminal {
fcntl(self.stdin, FcntlArg::F_SETFL(new_flags)).unwrap(); fcntl(self.stdin, FcntlArg::F_SETFL(new_flags)).unwrap();
} }
pub fn write_bytes(&self, buf: &[u8]) { pub fn reset_records(&mut self) {
Self::with_raw_mode(|| { self.write_records = Default::default();
write(unsafe{BorrowedFd::borrow_raw(self.stdout)}, buf).expect("Failed to write to stdout"); self.cursor_records = Default::default();
}); }
pub fn recorded_write(&mut self, buf: &str, offset: usize) -> ShResult<()> {
self.start_recording(offset);
self.write(buf);
self.stop_recording();
Ok(())
}
pub fn unwrite(&mut self) -> ShResult<()> {
self.unposition_cursor()?;
let WriteMap { lines, cols, offset } = self.write_records;
for _ in 0..lines {
self.write("\x1b[2K\x1b[A")
}
let col = offset;
self.write(&format!("\x1b[{col}G\x1b[0K"));
self.reset_records();
Ok(())
}
pub fn position_cursor(&mut self, (lines,col): (usize,usize)) -> ShResult<()> {
dbg!(self.cursor_pos());
self.cursor_records.lines = lines;
self.cursor_records.cols = col;
self.cursor_records.offset = self.cursor_pos().1;
for _ in 0..lines {
self.write("\x1b[A")
}
self.write(&format!("\x1b[{col}G"));
dbg!("done moving");
dbg!(self.cursor_pos());
Ok(())
}
pub fn unposition_cursor(&mut self) ->ShResult<()> {
dbg!(self.cursor_pos());
let WriteMap { lines, cols, offset } = self.cursor_records;
for _ in 0..lines {
self.write("\x1b[B")
}
self.write(&format!("\x1b[{offset}G"));
dbg!("done moving back");
dbg!(self.cursor_pos());
Ok(())
}
pub fn write_bytes(&mut self, buf: &[u8]) {
if self.recording {
let (_, width) = self.get_dimensions().unwrap();
let mut bytes = buf.iter().map(|&b| b as char).peekable();
while let Some(ch) = bytes.next() {
match ch {
'\n' => {
self.write_records.lines += 1;
self.write_records.cols = 0;
}
'\r' => {
self.write_records.cols = 0;
}
// Consume escape sequences
'\x1b' if bytes.peek() == Some(&'[') => {
bytes.next();
while let Some(&ch) = bytes.peek() {
if ch.is_ascii_alphabetic() {
bytes.next();
break
} else {
bytes.next();
}
}
}
'\t' => {
let tab_size = 8;
let next_tab = tab_size - (self.write_records.cols % tab_size);
self.write_records.cols += next_tab;
if self.write_records.cols >= width {
self.write_records.lines += 1;
self.write_records.cols = 0;
}
}
_ if ch.is_control() => {
// ignore control characters for visual width
}
_ => {
let ch_width = ch.width().unwrap_or(0);
if self.write_records.cols + ch_width > width {
self.write_records.lines += 1;
self.write_records.cols = 0;
}
self.write_records.cols += ch_width;
}
}
}
}
write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, buf).expect("Failed to write to stdout");
} }
pub fn write(&self, s: &str) { pub fn write(&mut self, s: &str) {
self.write_bytes(s.as_bytes()); self.write_bytes(s.as_bytes());
} }
pub fn writeln(&self, s: &str) { pub fn writeln(&mut self, s: &str) {
self.write(s); self.write(s);
self.write_bytes(b"\r\n"); self.write_bytes(b"\n");
} }
pub fn clear(&self) { pub fn clear(&mut self) {
self.write_bytes(b"\x1b[2J\x1b[H"); self.write_bytes(b"\x1b[2J\x1b[H");
} }
@@ -216,7 +346,7 @@ impl Terminal {
KeyEvent(KeyCode::Null, ModKeys::empty()) KeyEvent(KeyCode::Null, ModKeys::empty())
} }
pub fn cursor_pos(&self) -> (usize, usize) { pub fn cursor_pos(&mut self) -> (usize, usize) {
self.write("\x1b[6n"); self.write("\x1b[6n");
let mut buf = [0u8;32]; let mut buf = [0u8;32];
let n = self.read_byte(&mut buf); let n = self.read_byte(&mut buf);

View File

@@ -1,4 +1,4 @@
use super::{linebuf::{TermChar, TermCharBuf}, register::{append_register, read_register, write_register}}; use super::register::{append_register, read_register, write_register};
#[derive(Clone,Copy,Debug)] #[derive(Clone,Copy,Debug)]
pub struct RegisterName { pub struct RegisterName {
@@ -30,14 +30,14 @@ impl RegisterName {
pub fn count(&self) -> usize { pub fn count(&self) -> usize {
self.count self.count
} }
pub fn write_to_register(&self, buf: TermCharBuf) { pub fn write_to_register(&self, buf: String) {
if self.append { if self.append {
append_register(self.name, buf); append_register(self.name, buf);
} else { } else {
write_register(self.name, buf); write_register(self.name, buf);
} }
} }
pub fn read_from_register(&self) -> Option<TermCharBuf> { pub fn read_from_register(&self) -> Option<String> {
read_register(self.name) read_register(self.name)
} }
} }
@@ -153,7 +153,7 @@ pub enum Verb {
NormalMode, NormalMode,
VisualMode, VisualMode,
JoinLines, JoinLines,
InsertChar(TermChar), InsertChar(char),
Insert(String), Insert(String),
Breakline(Anchor), Breakline(Anchor),
Indent, Indent,
@@ -237,7 +237,7 @@ pub enum Motion {
/// forward-word, vi-end-word, vi-next-word /// forward-word, vi-end-word, vi-next-word
ForwardWord(To, Word), // Forward until start/end of word ForwardWord(To, Word), // Forward until start/end of word
/// character-search, character-search-backward, vi-char-search /// character-search, character-search-backward, vi-char-search
CharSearch(Direction,Dest,TermChar), CharSearch(Direction,Dest,char),
/// backward-char /// backward-char
BackwardChar, BackwardChar,
/// forward-char /// forward-char

View File

@@ -38,8 +38,9 @@ impl Display for FernBellStyle {
} }
} }
#[derive(Clone, Copy, Debug)] #[derive(Default, Clone, Copy, Debug)]
pub enum FernEditMode { pub enum FernEditMode {
#[default]
Vi, Vi,
Emacs Emacs
} }