pim

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 17ae495843fcb799eae28dc7ba39fab1bd7ecff1
parent 8e197646ed86e20c8cda95a7e057b84830e8fdae
Author: minerva-jupiter <ryouturn@gmail.com>
Date:   Sun,  7 Jun 2026 11:42:40 +0900

feat: implement core editor functionality with swap and recovery support

- Add `Buffer` for on-demand file I/O, delta management, and swap file persistence.
- Implement `Editor` state machine for navigation and mode switching.
- Add swap file detection and recovery prompts to `main.rs`.
- Implement basic text editing (Insert mode) and atomic saving.
- Update `README.md` with new features and roadmap progress.

Diffstat:
MREADME.md | 154+++++++++++++++++++++++++++++++++++++------------------------------------------
Asrc/buffer.rs | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/editor.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.rs | 343++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 670 insertions(+), 84 deletions(-)

diff --git a/README.md b/README.md @@ -1,82 +1,72 @@ -# pim - A Lightweight Minimalist Text Editor written in Rust - -`pim` は、Rustで開発されるテキストエディタである。初期実装として `nano` のようなミニマリズムと省メモリ設計(オンデマンド読み込み)を採用しつつ、内部構造は `vim` のようなモード管理(ステートマシン)が可能なアーキテクチャを採用する。 - -## 1. 動作環境・技術スタック - -* **Language**: Rust -* **TUI Framework**: `ratatui` (v0.26以降想定) -* **Terminal Control (Backend)**: `crossterm` -* **Target OS**: Linux, macOS, Windows - ---- - -## 2. コア仕様 (Specifications) - -### 2.1. モード管理 (State Machine) -起動時は常に `Normal` モードから開始する。入力されたキーイベントは直接テキストバッファに渡されず、現在のモードに応じて `Action` に変換されてから処理される。 - -* **Normal モード** - * `i`: `Insert` モードへ移行 - * `j`: カーソルを下へ移動 - * `k`: カーソルを上へ移動 - * `:q`: エディタの終了 (コマンドラインモード実装までの暫定) -* **Insert モード** - * `Esc`: `Normal` モードへ移行 - * 一般文字キー: テキストバッファへの文字挿入 - * `Backspace`: 文字の削除 - -### 2.2. 省メモリ設計 (On-demand File I/O) -数GB規模の巨大ファイルを開いた際、メモリのパンクを防ぐために以下の仕様を満たす。 -1. **行インデックス化**: ファイルオープン時、ファイル全体はメモリに読み込まず、各行の開始バイト位置(オフセット)のみを `Vec<usize>` に記録する。 -2. **オンデマンド描画**: 画面のスクロール位置に対応する行のバイト範囲のみをファイルから `seek` して読み込み、`ratatui` の描画バッファへ渡す。 -3. **差分バッファ管理**: 編集内容(挿入・削除)は元のファイルバッファを書き換えず、どの行のどの位置に変更があったかを別構造(Delta Queue)で管理する。画面描画時は、ファイルから読み込んだデータにこの差分を動的にパッチ当てして表示する。 - ---- - -## 3. アーキテクチャとデータ構造 - -### 3.1. モードおよびアクションの定義 -```rust -pub enum Mode { - Normal, - Insert, -} -``` - -### 3.2. テキストインデックスと差分管理 - ---- - -## 4. 実装ステップ (Implementation Roadmap) - -### フェーズ 1: ターミナル制御とモード遷移の確立 (土台) - -`crossterm` と `ratatui` を統合し、イベントループと画面の基本レイアウトを構築する。 - -* [ ] Rawモードの有効化とオルタネイトスクリーンの展開 -* [ ] 画面最下部へのステータスバー(現在の `Mode` 表示)の配置 -* [ ] キー入力イベントから `Action` への変換およびモード遷移のテスト - -### フェーズ 2: 巨大ファイルのインデックス化と部分読み込み (閲覧) - -テキストの編集を行わず、任意のサイズのファイルを最小のメモリで表示・スクロールできる状態を実装する。 - -* [ ] ファイルをスキャンし、改行コードを元に `LineIndex` を生成するロジックの実装 -* [ ] ターミナルの行数分だけファイルを `seek` して読み込む描画ロジックの実装 -* [ ] 矢印キー(または `j`/`k`)によるスクロール移動の実装 - -### フェーズ 3: 差分バッファの動的適用 (編集) - -メモリ上のファイルデータに対して変更履歴をオーバーレイさせ、画面に反映する。 - -* [ ] `DeltaBuffer` のデータ構造の定義 -* [ ] ファイルから読み込んだ生の行データに対し、`DeltaBuffer` 内の `EditOp` を適用して文字列を合成する関数の実装 -* [ ] Insertモード時における文字入力の `DeltaBuffer` への記録 - -### フェーズ 4: アトミックな書き込み (保存) - -編集されたデータを元のファイル、あるいは新しいファイルへ安全に出力する。 - -* [ ] `LineIndex` と `DeltaBuffer` を走査し、1行ずつストリームとしてディスクへ書き出すロジックの実装 -* [ ] 書き込み完了後の `LineIndex` の再構築と `DeltaBuffer` のクリア +# pim - A Lightweight Minimalist Text Editor written in Rust + +`pim` は、Rustで開発されるテキストエディタである。初期実装として `nano` のようなミニマリズムと省メモリ設計(オンデマンド読み込み)を採用しつつ、内部構造は `vim` のようなモード管理(ステートマシン)が可能なアーキテクチャを採用する。 + +## 1. 動作環境・技術スタック + +* **Language**: Rust (Edition 2024) +* **TUI Framework**: `ratatui` +* **Terminal Control (Backend)**: `crossterm` +* **Target OS**: Linux, macOS, Windows + +--- + +## 2. コア仕様 (Specifications) + +### 2.1. モード管理 (State Machine) +起動時は常に `Normal` モードから開始する。入力されたキーイベントは直接テキストバッファに渡されず、現在のモードに応じて `Action` に変換されてから処理される。 + +* **Normal モード** + * `i`: `Insert` モードへ移行 + * `h`, `j`, `k`, `l`: カーソル移動 + * `w`: ファイルの保存 + * `u`: Undo (前回の保存時点までロールバック) + * `q`: エディタの終了 +* **Insert モード** + * `Esc`: `Normal` モードへ移行 + * 一般文字キー: カーソル位置への文字挿入 + * `Backspace`: 文字の削除 + +### 2.2. 省メモリ設計 (On-demand File I/O) +1. **行インデックス化**: ファイルオープン時、各行の開始バイト位置のみを記録し、メモリ消費を抑える。 +2. **オンデマンド描画**: 画面の表示範囲のみを `seek` して読み込む。 +3. **差分バッファ管理**: 編集内容は `HashMap` で管理し、元の巨大ファイルを読み書きする負荷を最小化する。 + +### 2.3. データ保護 (Swap File & Safety) +1. **スワップファイル**: 編集内容は自動的に `.swp` ファイルへ同期(Insertモード終了時にバッチ書き込み)。 +2. **リカバリ機能**: クラッシュ後の再起動時に未保存の変更を復元可能。 +3. **アトミック保存**: 保存時は一時ファイルを作成してから置換し、改行コードは常に `LF` に統一される。 + +--- + +## 3. 実装ステップ (Implementation Roadmap) + +### フェーズ 1: ターミナル制御とモード遷移の確立 (土台) +* [x] Rawモードの有効化とオルタネイトスクリーンの展開 +* [x] 画面最下部へのステータスバー(現在の `Mode` 表示)の配置 +* [x] キー入力イベントから `Action` への変換およびモード遷移のテスト + +### フェーズ 2: 巨大ファイルのインデックス化と部分読み込み (閲覧) +* [x] ファイルをスキャンし、改行コードを元に `LineIndex` を生成するロジックの実装 +* [x] ターミナルの行数分だけファイルを `seek` して読み込む描画ロジックの実装 +* [x] 矢印キー(または `j`/`k`)によるスクロール移動の実装 +* [x] 行番号の表示実装 + +### フェーズ 3: 差分バッファの動的適用 (編集) +* [x] `DeltaBuffer` のデータ構造の定義 +* [x] スワップファイル(`.swp`)による自動保存とリカバリプロンプトの実装 +* [x] ビジュアルカーソルの実装と行内移動(`h`/`l`)の対応 +* [x] Insertモード終了時の一括スワップ同期(パフォーマンス最適化) + +### フェーズ 4: アトミックな書き込み (保存) +* [x] 常に `LF` 形式での保存ロジックの実装 +* [x] `u` キーによる保存単位のロールバック(ハイブリッド・スワップ管理) +* [x] 終了時の未保存変更チェックとクリーンアップ + +--- + +## 4. 使い方 + +```bash +cargo run -- <filename> +``` diff --git a/src/buffer.rs b/src/buffer.rs @@ -0,0 +1,198 @@ +use std::collections::HashMap; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, BufRead, BufReader, Seek, SeekFrom, Write}; +use std::path::PathBuf; + +pub struct Buffer { + file_path: PathBuf, + swap_path: PathBuf, + line_offsets: Vec<u64>, + deltas: HashMap<usize, String>, + modified: bool, +} + +impl Buffer { + pub fn new_empty(path: PathBuf) -> Self { + let swap_path = path.with_extension(format!( + "{}.swp", + path.extension().and_then(|e| e.to_str()).unwrap_or("txt") + )); + Self { + file_path: path, + swap_path, + line_offsets: vec![0], + deltas: HashMap::new(), + modified: false, + } + } + + pub fn from_path(path: PathBuf, should_recover: bool) -> io::Result<Self> { + let file = File::open(&path)?; + let mut reader = BufReader::new(file); + let mut line_offsets = vec![0]; + let mut offset = 0; + + let mut buf = Vec::new(); + while reader.read_until(b'\n', &mut buf)? > 0 { + offset += buf.len() as u64; + line_offsets.push(offset); + buf.clear(); + } + + let swap_path = path.with_extension(format!( + "{}.swp", + path.extension().and_then(|e| e.to_str()).unwrap_or("txt") + )); + + let mut deltas = HashMap::new(); + let mut modified = false; + if swap_path.exists() && should_recover { + let swap_file = File::open(&swap_path)?; + let swap_reader = BufReader::new(swap_file); + for line in swap_reader.lines() { + let line = line?; + if line == "SAVE" { + modified = false; + continue; + } + if let Some((idx_str, content)) = line.split_once(':') + && let Ok(idx) = idx_str.parse::<usize>() + { + deltas.insert(idx, content.to_string()); + modified = true; + } + } + } else if swap_path.exists() { + fs::remove_file(&swap_path)?; + } + + Ok(Self { + file_path: path, + swap_path, + line_offsets, + deltas, + modified, + }) + } + + pub fn read_line(&self, index: usize) -> io::Result<String> { + if let Some(delta) = self.deltas.get(&index) { + return Ok(delta.clone()); + } + + if index >= self.line_offsets.len().saturating_sub(1) { + return Ok(String::new()); + } + + let mut file = File::open(&self.file_path)?; + file.seek(SeekFrom::Start(self.line_offsets[index]))?; + + let length = self.line_offsets[index + 1] - self.line_offsets[index]; + let mut buf = vec![0; length as usize]; + io::Read::read_exact(&mut file, &mut buf)?; + + Ok(String::from_utf8_lossy(&buf).to_string()) + } + + pub fn update_line(&mut self, index: usize, content: String) { + self.deltas.insert(index, content); + self.modified = true; + } + + pub fn sync_to_swap(&self) -> io::Result<()> { + let mut swap_file = OpenOptions::new() + .create(true) + .append(true) + .open(&self.swap_path)?; + + // 現在の全ての差分をスワップファイルに書き出す + // 本来は「今回の編集セッションの変更点」だけに絞るのが効率的だが、 + // 現状のログ形式を維持するため、全ての deltas を再出力する + for (idx, content) in &self.deltas { + writeln!(swap_file, "{}:{}", idx, content.replace(['\n', '\r'], ""))?; + } + Ok(()) + } + + pub fn save(&mut self) -> io::Result<()> { + let temp_path = self.file_path.with_extension("tmp"); + let mut temp_file = File::create(&temp_path)?; + + for i in 0..self.total_lines().max(1) { + let line = self.read_line(i)?; + let content = line.replace(['\n', '\r'], ""); + #[allow(clippy::write_with_newline)] + write!(temp_file, "{}\n", content)?; + } + + fs::rename(temp_path, &self.file_path)?; + + let mut swap_file = OpenOptions::new() + .create(true) + .append(true) + .open(&self.swap_path)?; + writeln!(swap_file, "SAVE")?; + + self.modified = false; + Ok(()) + } + + pub fn rollback(&mut self) -> io::Result<()> { + if !self.swap_path.exists() { + return Ok(()); + } + + let swap_file = File::open(&self.swap_path)?; + let lines: Vec<String> = BufReader::new(swap_file) + .lines() + .collect::<Result<_, _>>()?; + + let last_save_idx = lines.iter().rposition(|l| l == "SAVE"); + let target_idx = if let Some(idx) = last_save_idx { + lines[..idx].iter().rposition(|l| l == "SAVE") + } else { + None + }; + + let mut new_deltas = HashMap::new(); + let limit = target_idx.map(|i| i + 1).unwrap_or(0); + + for line in &lines[..limit] { + if line == "SAVE" { + continue; + } + if let Some((idx_str, content)) = line.split_once(':') + && let Ok(idx) = idx_str.parse::<usize>() + { + new_deltas.insert(idx, content.to_string()); + } + } + + let mut swap_file = File::create(&self.swap_path)?; + for line in &lines[..limit] { + writeln!(swap_file, "{}", line)?; + } + + self.deltas = new_deltas; + self.modified = false; + Ok(()) + } + + pub fn is_modified(&self) -> bool { + self.modified + } + + pub fn total_lines(&self) -> usize { + self.line_offsets + .len() + .saturating_sub(1) + .max(self.deltas.keys().map(|&k| k + 1).max().unwrap_or(0)) + } + + pub fn remove_swap_file(&self) -> io::Result<()> { + if self.swap_path.exists() { + fs::remove_file(&self.swap_path)?; + } + Ok(()) + } +} diff --git a/src/editor.rs b/src/editor.rs @@ -0,0 +1,59 @@ +#[derive(PartialEq)] +pub enum Mode { + Normal, + Insert, +} + +pub struct Editor { + pub mode: Mode, + pub scroll_offset: usize, + pub cursor_line: usize, + pub cursor_column: usize, +} + +impl Editor { + pub fn new() -> Self { + Self { + mode: Mode::Normal, + scroll_offset: 0, + cursor_line: 0, + cursor_column: 0, + } + } + + pub fn scroll_down(&mut self, total_lines: usize, view_height: usize) { + if self.scroll_offset + view_height < total_lines { + self.scroll_offset += 1; + } + } + + pub fn scroll_up(&mut self) { + if self.scroll_offset > 0 { + self.scroll_offset -= 1; + } + } + + pub fn move_cursor_down(&mut self, total_lines: usize) { + if self.cursor_line + 1 < total_lines { + self.cursor_line += 1; + } + } + + pub fn move_cursor_up(&mut self) { + if self.cursor_line > 0 { + self.cursor_line -= 1; + } + } + + pub fn move_cursor_left(&mut self) { + if self.cursor_column > 0 { + self.cursor_column -= 1; + } + } + + pub fn move_cursor_right(&mut self, line_len: usize) { + if self.cursor_column < line_len { + self.cursor_column += 1; + } + } +} diff --git a/src/main.rs b/src/main.rs @@ -1,3 +1,342 @@ -fn main() { - println!("Hello, world!"); +mod buffer; +mod editor; + +use crossterm::{ + event::{self, Event, KeyCode}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, +}; +use ratatui::{ + Terminal, + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout}, + widgets::Paragraph, +}; +use std::io::{self, Stdout, stdout}; +use std::path::PathBuf; + +use crate::buffer::Buffer; +use crate::editor::{Editor, Mode}; + +enum AppState { + Running, + SwapDetected(PathBuf), + NewFilePrompt(PathBuf), + UnsavedChangesPrompt, + Quitting, +} + +fn main() -> io::Result<()> { + let mut terminal = setup_terminal()?; + + let path = std::env::args().nth(1).map(PathBuf::from); + let mut buffer = None; + let mut state = AppState::Running; + + if let Some(ref p) = path { + let swap_path = p.with_extension(format!( + "{}.swp", + p.extension().and_then(|e| e.to_str()).unwrap_or("txt") + )); + if swap_path.exists() { + state = AppState::SwapDetected(p.clone()); + } else if !p.exists() { + state = AppState::NewFilePrompt(p.clone()); + } else { + buffer = Some(Buffer::from_path(p.clone(), true)?); + } + } + + let result = run(&mut terminal, buffer, state, path); + restore_terminal(&mut terminal)?; + result +} + +fn setup_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> { + enable_raw_mode()?; + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + Terminal::new(backend) +} + +fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> io::Result<()> { + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor() +} + +fn run( + terminal: &mut Terminal<CrosstermBackend<Stdout>>, + mut buffer: Option<Buffer>, + mut state: AppState, + _path: Option<PathBuf>, +) -> io::Result<()> { + let mut editor = Editor::new(); + + loop { + match state { + AppState::Quitting => { + if let Some(ref b) = buffer { + let _ = b.remove_swap_file(); + } + break; + } + AppState::SwapDetected(ref p) => { + terminal.draw(|f| { + let content = format!( + "Swap file detected for {:?}. \nRecover? (y: Yes, n: No/Discard)", + p + ); + f.render_widget(Paragraph::new(content), f.area()); + })?; + + if let Ok(Event::Key(key)) = event::read() + && key.kind == event::KeyEventKind::Press + { + match key.code { + KeyCode::Char('y') => { + buffer = Some(Buffer::from_path(p.clone(), true)?); + state = AppState::Running; + } + KeyCode::Char('n') => { + buffer = Some(Buffer::from_path(p.clone(), false)?); + state = AppState::Running; + } + _ => {} + } + } + } + AppState::NewFilePrompt(ref p) => { + terminal.draw(|f| { + let content = format!( + "File {:?} does not exist. \nCreate new file? (y: Yes, n: No/Quit)", + p + ); + f.render_widget(Paragraph::new(content), f.area()); + })?; + + if let Ok(Event::Key(key)) = event::read() + && key.kind == event::KeyEventKind::Press + { + match key.code { + KeyCode::Char('y') => { + buffer = Some(Buffer::new_empty(p.clone())); + state = AppState::Running; + } + KeyCode::Char('n') => { + state = AppState::Quitting; + } + _ => {} + } + } + } + AppState::UnsavedChangesPrompt => { + terminal.draw(|f| { + let content = "You have unsaved changes. \nSave and quit? (y: Save & Quit, n: Discard & Quit, c: Cancel)"; + f.render_widget(Paragraph::new(content), f.area()); + })?; + + if let Ok(Event::Key(key)) = event::read() + && key.kind == event::KeyEventKind::Press + { + match key.code { + KeyCode::Char('y') => { + if let Some(ref mut b) = buffer { + b.save()?; + } + state = AppState::Quitting; + } + KeyCode::Char('n') => { + state = AppState::Quitting; + } + KeyCode::Char('c') => { + state = AppState::Running; + } + _ => {} + } + } + } + AppState::Running => { + let mut line_num_width = 0; + terminal.draw(|f| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(1)]) + .split(f.area()); + + let mut display_content = String::new(); + if let Some(ref b) = buffer { + let view_height = chunks[0].height as usize; + let total_lines = b.total_lines(); + line_num_width = total_lines.to_string().len().max(1); + + let start = editor.scroll_offset; + let end = (start + view_height).min(total_lines.max(1)); + + for i in start..end { + if let Ok(line) = b.read_line(i) { + let line_content = line.replace(['\n', '\r'], ""); + let line_display = format!( + " {:>width$} | {}\n", + i + 1, + line_content, + width = line_num_width + ); + display_content.push_str(&line_display); + } + } + } + + f.render_widget(Paragraph::new(display_content), chunks[0]); + + let mode_str = match editor.mode { + Mode::Normal => "NORMAL", + Mode::Insert => "INSERT", + }; + + let line_count = buffer.as_ref().map(|b| b.total_lines()).unwrap_or(0); + let modified_flag = if buffer.as_ref().map(|b| b.is_modified()).unwrap_or(false) + { + " [modified]" + } else { + "" + }; + + let key_hint = match editor.mode { + Mode::Normal => "'w': Save, 'u': Undo, 'q': Quit, 'h/j/k/l': Move", + Mode::Insert => "'ESC': Exit Insert mode", + }; + + let status = format!( + "Mode: {}{} | Lines: {} | Cursor: {}:{} | {}", + mode_str, + modified_flag, + line_count, + editor.cursor_line + 1, + editor.cursor_column + 1, + key_hint + ); + + f.render_widget(Paragraph::new(status), chunks[1]); + })?; + + // カーソル位置の調整 + if let Some(ref b) = buffer { + let line = b.read_line(editor.cursor_line).unwrap_or_default(); + let line_len = line.replace(['\n', '\r'], "").len(); + let max_column = if editor.mode == Mode::Normal { + line_len.saturating_sub(1) + } else { + line_len + }; + if editor.cursor_column > max_column { + editor.cursor_column = max_column; + } + + let x = (2 + line_num_width + 3 + editor.cursor_column) as u16; + let y = (editor.cursor_line - editor.scroll_offset) as u16; + terminal.set_cursor_position((x, y))?; + } + terminal.show_cursor()?; + + if let Event::Key(key) = event::read()? { + if key.kind != event::KeyEventKind::Press { + continue; + } + + match editor.mode { + Mode::Normal => match key.code { + KeyCode::Char('q') => { + if buffer.as_ref().map(|b| b.is_modified()).unwrap_or(false) { + state = AppState::UnsavedChangesPrompt; + } else { + state = AppState::Quitting; + } + } + KeyCode::Char('w') => { + if let Some(ref mut b) = buffer { + b.save()?; + } + } + KeyCode::Char('u') => { + if let Some(ref mut b) = buffer { + b.rollback()?; + let line = b.read_line(editor.cursor_line).unwrap_or_default(); + let line_len = line.replace(['\n', '\r'], "").len(); + if editor.cursor_column >= line_len { + editor.cursor_column = line_len.saturating_sub(1); + } + } + } + KeyCode::Char('i') => editor.mode = Mode::Insert, + KeyCode::Char('h') => editor.move_cursor_left(), + KeyCode::Char('l') => { + if let Some(ref b) = buffer { + let line = b.read_line(editor.cursor_line).unwrap_or_default(); + let line_len = line.replace(['\n', '\r'], "").len(); + editor.move_cursor_right(line_len.saturating_sub(1)); + } + } + KeyCode::Char('j') => { + if let Some(ref b) = buffer { + let total = b.total_lines().max(1); + editor.move_cursor_down(total); + if editor.cursor_line + >= editor.scroll_offset + terminal.size()?.height as usize + - 1 + { + editor.scroll_down(total, 1); + } + } + } + KeyCode::Char('k') => { + if buffer.is_some() { + editor.move_cursor_up(); + if editor.cursor_line < editor.scroll_offset { + editor.scroll_up(); + } + } + } + _ => {} + }, + Mode::Insert => { + if key.code == KeyCode::Esc { + editor.mode = Mode::Normal; + if let Some(ref mut b) = buffer { + // Insertモード終了時にスワップファイルに書き出す + b.sync_to_swap()?; + + let line = b.read_line(editor.cursor_line).unwrap_or_default(); + let line_len = line.replace(['\n', '\r'], "").len(); + if editor.cursor_column >= line_len && line_len > 0 { + editor.cursor_column = line_len - 1; + } + } + } else if let KeyCode::Char(c) = key.code { + if let Some(ref mut b) = buffer { + let mut current_line = + b.read_line(editor.cursor_line).unwrap_or_default(); + current_line = current_line.replace(['\n', '\r'], ""); + current_line.insert(editor.cursor_column, c); + b.update_line(editor.cursor_line, current_line); + editor.cursor_column += 1; + } + } else if let (KeyCode::Backspace, Some(b)) = (key.code, &mut buffer) + && editor.cursor_column > 0 + { + let mut current_line = + b.read_line(editor.cursor_line).unwrap_or_default(); + current_line = current_line.replace(['\n', '\r'], ""); + current_line.remove(editor.cursor_column - 1); + b.update_line(editor.cursor_line, current_line); + editor.cursor_column -= 1; + } + } + } + } + } + } + } + Ok(()) }