From a699c0a371e9f29e6d46dec6cb1d9c0173c2946a Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Wed, 18 Mar 2026 19:18:08 -0500 Subject: [PATCH] fix handling empty files --- src/action.rs | 56 +++++++++++++++++++++++++++------------------- src/app/mod.rs | 7 +++++- src/app/widget.rs | 16 +++++++++---- src/cursor.rs | 12 +++++----- src/edit_action.rs | 2 +- src/main.rs | 3 +-- 6 files changed, 59 insertions(+), 37 deletions(-) diff --git a/src/action.rs b/src/action.rs index f984389..8984d05 100644 --- a/src/action.rs +++ b/src/action.rs @@ -139,7 +139,9 @@ impl App { } const fn replace(&mut self) { - self.partial_action = Some(PartialAction::Replace); + if !self.contents.is_empty() { + self.partial_action = Some(PartialAction::Replace); + } } const fn space(&mut self) { @@ -156,7 +158,7 @@ impl App { } const fn move_byte_down(&mut self) { - if self.contents.len() - 1 - self.cursor.head >= BYTES_PER_LINE { + if self.max_contents_index() - self.cursor.head >= BYTES_PER_LINE { self.cursor.head += BYTES_PER_LINE; self.cursor.collapse(); @@ -174,7 +176,7 @@ impl App { } const fn move_byte_right(&mut self) { - if self.contents.len() - 1 - self.cursor.head >= 1 { + if self.max_contents_index() - self.cursor.head >= 1 { self.cursor.head += 1; self.cursor.collapse(); @@ -190,7 +192,7 @@ impl App { } const fn extend_byte_down(&mut self) { - if self.contents.len() - 1 - self.cursor.head >= BYTES_PER_LINE { + if self.max_contents_index() - self.cursor.head >= BYTES_PER_LINE { self.cursor.head += BYTES_PER_LINE; self.clamp_screen_to_cursor(); } @@ -204,7 +206,7 @@ impl App { } const fn extend_byte_right(&mut self) { - if self.contents.len() - 1 - self.cursor.head >= 1 { + if self.max_contents_index() - self.cursor.head >= 1 { self.cursor.head += 1; self.clamp_screen_to_cursor(); } @@ -235,6 +237,8 @@ impl App { } fn scroll_down(&mut self) { + if self.contents.len() <= 5 * BYTES_PER_LINE { return; } + self.scroll_position = min( self.scroll_position + BYTES_PER_LINE, self.contents.len() - (5 * BYTES_PER_LINE) @@ -248,6 +252,8 @@ impl App { } fn page_cursor_half_down(&mut self) { + if self.contents.len() <= 5 * BYTES_PER_LINE { return; } + let head_offset = self.cursor.head - self.scroll_position; let tail_offset = self.cursor.tail - self.scroll_position; @@ -256,8 +262,8 @@ impl App { self.contents.len() - (5 * BYTES_PER_LINE) ); - self.cursor.head = (self.scroll_position + head_offset).min(self.contents.len() - 1); - self.cursor.tail = (self.scroll_position + tail_offset).min(self.contents.len() - 1); + self.cursor.head = (self.scroll_position + head_offset).min(self.max_contents_index()); + self.cursor.tail = (self.scroll_position + tail_offset).min(self.max_contents_index()); } fn page_cursor_half_up(&mut self) { @@ -268,11 +274,13 @@ impl App { (self.screen_size() / 2).next_multiple_of(BYTES_PER_LINE) ); - self.cursor.head = (self.scroll_position + head_offset).min(self.contents.len() - 1); - self.cursor.tail = (self.scroll_position + tail_offset).min(self.contents.len() - 1); + self.cursor.head = (self.scroll_position + head_offset).min(self.max_contents_index()); + self.cursor.tail = (self.scroll_position + tail_offset).min(self.max_contents_index()); } fn page_down(&mut self) { + if self.contents.len() <= 5 * BYTES_PER_LINE { return; } + self.scroll_position = min( self.scroll_position + self.screen_size(), self.contents.len() - (5 * BYTES_PER_LINE) @@ -288,12 +296,12 @@ impl App { } fn move_next_word_start(&mut self) { - self.cursor.move_to_next_word(self.contents.len() - 1); + self.cursor.move_to_next_word(self.max_contents_index()); self.clamp_screen_to_cursor(); } fn move_next_word_end(&mut self) { - self.cursor.move_to_next_end(self.contents.len() - 1); + self.cursor.move_to_next_end(self.max_contents_index()); self.clamp_screen_to_cursor(); } @@ -303,12 +311,12 @@ impl App { } fn extend_next_word_start(&mut self) { - self.cursor.extend_to_next_word(self.contents.len() - 1); + self.cursor.extend_to_next_word(self.max_contents_index()); self.clamp_screen_to_cursor(); } fn extend_next_word_end(&mut self) { - self.cursor.extend_to_next_end(self.contents.len() - 1); + self.cursor.extend_to_next_end(self.max_contents_index()); self.clamp_screen_to_cursor(); } @@ -331,7 +339,7 @@ impl App { { self.cursor.head = min( self.cursor.head + BYTES_PER_LINE, - self.contents.len() - 1 + self.max_contents_index() ); } else { self.cursor.tail -= self.cursor.tail % BYTES_PER_LINE; @@ -346,7 +354,7 @@ impl App { if self.cursor.head.is_multiple_of(BYTES_PER_LINE) && (self.cursor.tail % BYTES_PER_LINE == BYTES_PER_LINE - 1 || - self.cursor.tail == self.contents.len() - 1) + self.cursor.tail == self.max_contents_index()) { self.cursor.head = self.cursor.head.saturating_sub(BYTES_PER_LINE); } else { @@ -356,12 +364,14 @@ impl App { } fn delete(&mut self) { - self.execute_and_add( - EditAction::Delete { - cursor: self.cursor, - old_data: self.contents[self.cursor.range()].into() - } - ); + if !self.contents.is_empty() { + self.execute_and_add( + EditAction::Delete { + cursor: self.cursor, + old_data: self.contents[self.cursor.range()].into() + } + ); + } if self.mode == Mode::Select { self.mode = Mode::Normal; @@ -369,7 +379,7 @@ impl App { } fn undo(&mut self) { - if self.time_traveling == Some(0) || self.edit_history.is_empty() { return } + if self.time_traveling == Some(0) || self.edit_history.is_empty() { return; } let current_date = self.time_traveling .map_or(self.edit_history.len() - 1, |date| date - 1); @@ -387,7 +397,7 @@ impl App { } fn redo(&mut self) { - let Some(previous_date) = self.time_traveling else { return }; + let Some(previous_date) = self.time_traveling else { return; }; let current_date = previous_date + 1; diff --git a/src/app/mod.rs b/src/app/mod.rs index 28e4384..43b94b4 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -183,10 +183,15 @@ impl App { false } } + + // returns 0 if empty + pub const fn max_contents_index(&self) -> usize { + self.contents.len().saturating_sub(1) + } } fn nybble_from_hex(hex: char) -> Option { - if !hex.is_ascii() { return None } + if !hex.is_ascii() { return None; } match hex { '0'..='9' => Some(u8::try_from(hex).unwrap() - u8::try_from('0').unwrap()), diff --git a/src/app/widget.rs b/src/app/widget.rs index 90451eb..a4304d6 100644 --- a/src/app/widget.rs +++ b/src/app/widget.rs @@ -33,6 +33,10 @@ impl Widget for &App { let hex_area = Rect::new(area.x, area.y, area.width, area.height - 1); hex_text.render(hex_area, buf); + if self.contents.is_empty() { + Line::from("empty file").render(area, buf); + } + let status_line_area = Rect::new(area.x, area.bottom() - 1, area.width, 1); self.render_status_line().render(status_line_area, buf); @@ -416,14 +420,18 @@ mod extra_statuses { impl App { pub fn render_extra_statuses(&self) -> Line<'_> { - #[allow(clippy::cast_precision_loss)] - let percentage = self.cursor.head as f64 / (self.contents.len() - 1) as f64 * 100.0; - let partial_action = self.partial_action .as_ref() .map_or("", |partial_action| partial_action.label()); - format!("{partial_action} {percentage:.0}% ").into() + if self.contents.is_empty() { + format!("{partial_action} ").into() + } else { + #[allow(clippy::cast_precision_loss)] + let percentage = self.cursor.head as f64 / self.max_contents_index() as f64 * 100.0; + + format!("{partial_action} {percentage:.0}% ").into() + } } } } diff --git a/src/cursor.rs b/src/cursor.rs index 86c716c..444028b 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -50,7 +50,7 @@ impl Cursor { } pub fn move_to_next_word(&mut self, max: usize) { - if self.head == max { return } + if self.head == max { return; } if self.head.is_multiple_of(4) { // at the beginning of a word self.head = (self.head + 4).min(max); @@ -61,7 +61,7 @@ impl Cursor { } pub fn move_to_next_end(&mut self, max: usize) { - if self.head == max { return } + if self.head == max { return; } self.collapse(); if self.head % 4 == 3 { // at the end of a word @@ -73,7 +73,7 @@ impl Cursor { } pub const fn move_to_previous_beginning(&mut self) { - if self.head == 0 { return } + if self.head == 0 { return; } self.collapse(); if self.head.is_multiple_of(4) { // at the beginning of a word @@ -85,7 +85,7 @@ impl Cursor { } pub fn extend_to_next_word(&mut self, max: usize) { - if self.head == max { return } + if self.head == max { return; } if self.head.is_multiple_of(4) { // at the beginning of a word self.head = (self.head + 4).min(max); @@ -95,7 +95,7 @@ impl Cursor { } pub fn extend_to_next_end(&mut self, max: usize) { - if self.head == max { return } + if self.head == max { return; } if self.head % 4 == 3 { // at the end of a word self.head = (self.head + 4).min(max); @@ -105,7 +105,7 @@ impl Cursor { } pub const fn extend_to_previous_beginning(&mut self) { - if self.head == 0 { return } + if self.head == 0 { return; } if self.head.is_multiple_of(4) { // at the beginning of a word self.head -= 4; diff --git a/src/edit_action.rs b/src/edit_action.rs index bf32149..720e233 100644 --- a/src/edit_action.rs +++ b/src/edit_action.rs @@ -66,7 +66,7 @@ impl App { fn delete_at(&mut self, cursor: Cursor) { self.contents.drain(cursor.range()); - self.cursor.head = min(min(cursor.head, cursor.tail), self.contents.len() - 1); + self.cursor.head = min(min(cursor.head, cursor.tail), self.max_contents_index()); self.cursor.collapse(); } diff --git a/src/main.rs b/src/main.rs index 6e5878b..f270f57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,8 +50,7 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // - utf8? // - diffing -// TODO: opening empty file crashes (or deleting entire file) -// - cursor is NOT guaranteed to be in-bounds..? +// TODO: quit should confirm if unsaved changes // when AsciiChar is stabilized, use it instead of char everywhere