From 75158ee7715c86ba188da2a4754d6f249194d4cf Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Fri, 10 Apr 2026 20:11:27 -0500 Subject: [PATCH] keep popups for some actions --- src/action.rs | 441 +++++++++++++++++++++++++++++++--------------- src/buffer/mod.rs | 18 +- src/main.rs | 1 - 3 files changed, 310 insertions(+), 150 deletions(-) diff --git a/src/action.rs b/src/action.rs index 4eebbf2..b2b1637 100644 --- a/src/action.rs +++ b/src/action.rs @@ -13,6 +13,18 @@ pub enum Action { Cursor(CursorAction), } +impl Action { + pub const fn clears_popups(self) -> bool { + use Action::*; + + match self { + App(app_action) => app_action.clears_popups(), + Buffer(buffer_action) => buffer_action.clears_popups(), + Cursor(cursor_action) => cursor_action.clears_popups(), + } + } +} + impl From for &str { fn from(action: Action) -> Self { match action { @@ -46,6 +58,23 @@ pub enum AppAction { Yank, } +impl AppAction { + const fn clears_popups(self) -> bool { + use AppAction::*; + + #[allow(clippy::match_same_arms)] + match self { + QuitIfSaved => true, + Quit => true, + + PreviousBuffer => false, + NextBuffer => false, + + Yank => false, + } + } +} + impl From for &str { fn from(app_action: AppAction) -> Self { use AppAction::*; @@ -155,6 +184,78 @@ pub enum BufferAction { InspectSelectionColor, } +impl BufferAction { + const fn clears_popups(self) -> bool { + use BufferAction::*; + + #[allow(clippy::match_same_arms)] + match self { + NormalMode => false, + SelectMode => false, + + Goto => false, + View => false, + Replace => true, + Space => false, + Repeat => true, + To => false, + + ScrollDown => true, + ScrollUp => true, + + PageCursorHalfDown => true, + PageCursorHalfUp => true, + + PageDown => true, + PageUp => true, + + CollapseSelection => true, + FlipSelections => false, + + Delete => true, + + Undo => true, + Redo => true, + + Save => false, + + CopySelectionOnNextLine => true, + + RotateSelectionsBackward => false, + RotateSelectionsForward => false, + + KeepPrimarySelection => true, + RemovePrimarySelection => true, + + SplitSelectionsInto1s => true, + SplitSelectionsInto2s => true, + SplitSelectionsInto3s => true, + SplitSelectionsInto4s => true, + SplitSelectionsInto5s => true, + SplitSelectionsInto6s => true, + SplitSelectionsInto7s => true, + SplitSelectionsInto8s => true, + SplitSelectionsInto9s => true, + + JumpToSelectedOffset => true, + JumpToSelectedOffsetRelativeToMark => true, + + ToggleMark => false, + + AlignViewCenter => false, + AlignViewBottom => false, + AlignViewTop => false, + + ExtendToMark => true, + ExtendToNull => true, + ExtendToFF => true, + + InspectSelection => true, + InspectSelectionColor => true, + } + } +} + impl From for &str { fn from(buffer_action: BufferAction) -> Self { use BufferAction::*; @@ -338,6 +439,41 @@ pub enum CursorAction { ExtendLineAbove, } +impl CursorAction { + const fn clears_popups(self) -> bool { + use CursorAction::*; + + #[allow(clippy::match_same_arms)] + match self { + MoveByteUp => true, + MoveByteDown => true, + MoveByteLeft => true, + MoveByteRight => true, + + ExtendByteUp => true, + ExtendByteDown => true, + ExtendByteLeft => true, + ExtendByteRight => true, + + GotoLineStart => true, + GotoLineEnd => true, + GotoFileStart => true, + GotoFileEnd => true, + + MoveNextWordStart => true, + MoveNextWordEnd => true, + MovePreviousWordStart => true, + + ExtendNextWordStart => true, + ExtendNextWordEnd => true, + ExtendPreviousWordStart => true, + + ExtendLineBelow => true, + ExtendLineAbove => true, + } + } +} + impl From for &str { fn from(cursor_action: CursorAction) -> Self { use CursorAction::*; @@ -1050,125 +1186,22 @@ impl Buffer { self.popups.extend( iter::once(&self.primary_cursor) .chain(&self.cursors) - .map(|cursor| { + .filter_map(|cursor| { let selection = &self.contents[cursor.range()]; - let nat = bytes_to_nat(selection); + let popup_lines = inspect(selection); - let int = nat.and_then(|nat| nat_to_int_if_different(nat, selection.len())); - - let utf8 = str::from_utf8(selection).ok() - .map(|utf8| utf8.trim_suffix('\0')) - .filter(|utf8| !utf8.contains(is_illegal_control_character)) - .map(|utf8| Span::from(format!("\"{utf8}\"")).red()); - - let fixedpoint2012 = nat - .filter(|_| selection.len() == 4) - .map(|nat| f64::from(nat as u32) / f64::from(1 << 12)) - .map(|fixedpoint2012| { - let two_decimals_is_enough = (fixedpoint2012 * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("20.12: {approximate_symbol}{fixedpoint2012:.2}").into() - }); - - let fixedpoint2012_signed = int - .filter(|_| selection.len() == 4) - .map(|int| f64::from(int as i32) / f64::from(1 << 12)) - .map(|fixedpoint2012_signed| { - let two_decimals_is_enough = (fixedpoint2012_signed * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("i20.12: {approximate_symbol}{fixedpoint2012_signed:.2}").into() - }); - - let fixedpoint1616 = nat - .filter(|_| selection.len() == 4) - .map(|nat| f64::from(nat as u32) / f64::from(1 << 16)) - .map(|fixedpoint1616| { - let two_decimals_is_enough = (fixedpoint1616 * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("16.16: {approximate_symbol}{fixedpoint1616:.2}").into() - }); - - let fixedpoint1616_signed = int - .filter(|_| selection.len() == 4) - .map(|int| f64::from(int as i32) / f64::from(1 << 16)) - .map(|fixedpoint1616_signed| { - let two_decimals_is_enough = (fixedpoint1616_signed * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("i16.16: {approximate_symbol}{fixedpoint1616_signed:.2}").into() - }); - - let fixedpoint124 = nat - .filter(|_| selection.len() == 2) - .map(|nat| f64::from(nat as u16) / f64::from(1 << 4)) - .map(|fixedpoint124| { - let two_decimals_is_enough = (fixedpoint124 * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("12.4: {approximate_symbol}{fixedpoint124:.2}").into() - }); - - let fixedpoint88 = nat - .filter(|_| selection.len() == 2) - .map(|nat| f64::from(nat as u16) / f64::from(1 << 8)) - .map(|fixedpoint88| { - let two_decimals_is_enough = (fixedpoint88 * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("8.8: {approximate_symbol}{fixedpoint88:.2}").into() - }); - - let fixedpoint412 = nat - .filter(|_| selection.len() == 2) - .map(|nat| f64::from(nat as u16) / f64::from(1 << 12)) - .map(|fixedpoint412| { - let two_decimals_is_enough = (fixedpoint412 * 100.0).fract() == 0.0; - let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - - format!("4.12: {approximate_symbol}{fixedpoint412:.2}").into() - }); - - let color888 = (selection.len() == 3) - .then(|| [selection[0], selection[1], selection[2]]) - .map(|[red, green, blue]| { - Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) - .fg(Color::Rgb(red, green, blue)) - - }); - - let color555 = nat - .filter(|_| selection.len() == 2) - .filter(|&nat| nat >> 15 == 0) - .map(|nat| color555_to_color888(nat as u16)) - .map(|[red, green, blue]| { - Span::from(format!("555: #{red:02X}{green:02X}{blue:02X}")) - .fg(Color::Rgb(red, green, blue)) - - }); - - Popup::new( - cursor.lower_bound(), - int.map(|int| format!("{int}").into()) - .into_iter() - .chain(nat.map(|nat| format!("{nat}").into())) - .chain(utf8) - .chain(fixedpoint2012_signed) - .chain(fixedpoint2012) - .chain(fixedpoint1616_signed) - .chain(fixedpoint1616) - .chain(fixedpoint124) - .chain(fixedpoint88) - .chain(fixedpoint412) - .chain(color888) - .chain(color555) - .collect() - ) + if popup_lines.is_empty() { + None + } else { + Some(Popup::new(cursor.lower_bound(), popup_lines)) + } }) ); + + if self.popups.is_empty() { + self.inspecting_selection = false; + } } fn inspect_selection_color(&mut self) { @@ -1179,41 +1212,167 @@ impl Buffer { self.popups.extend( iter::once(&self.primary_cursor) .chain(&self.cursors) - .map(|cursor| { + .filter_map(|cursor| { let selection = &self.contents[cursor.range()]; - let nat = bytes_to_nat(selection); + let popup_lines = inspect_color(selection); - let color888 = (selection.len() == 3) - .then(|| [selection[0], selection[1], selection[2]]) - .map(|[red, green, blue]| { - Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) - .fg(Color::Rgb(red, green, blue)) - - }); - - let color555 = nat - .filter(|_| selection.len() == 2) - .filter(|&nat| nat >> 15 == 0) - .map(|nat| color555_to_color888(nat as u16)) - .map(|[red, green, blue]| { - Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) - .fg(Color::Rgb(red, green, blue)) - - }); - - Popup::new( - cursor.lower_bound(), - color888 - .into_iter() - .chain(color555) - .collect() - ) + if popup_lines.is_empty() { + None + } else { + Some(Popup::new(cursor.lower_bound(), popup_lines)) + } }) ); + + if self.popups.is_empty() { + self.inspecting_selection = false; + } } } +fn inspect(selection: &[u8]) -> Vec> { + let nat = bytes_to_nat(selection); + + let int = nat.and_then(|nat| nat_to_int_if_different(nat, selection.len())); + + let utf8 = str::from_utf8(selection).ok() + .filter(|_| selection.len() == 1) + .map(|utf8| utf8.trim_suffix('\0')) + .filter(|utf8| !utf8.contains(is_illegal_control_character)) + .map(|utf8| Span::from(format!("\"{utf8}\"")).red()); + + let fixedpoint2012 = nat + .filter(|_| selection.len() == 4) + .map(|nat| f64::from(nat as u32) / f64::from(1 << 12)) + .map(|fixedpoint2012| { + let two_decimals_is_enough = (fixedpoint2012 * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("20.12: {approximate_symbol}{fixedpoint2012:.2}").into() + }); + + let fixedpoint2012_signed = int + .filter(|_| selection.len() == 4) + .map(|int| f64::from(int as i32) / f64::from(1 << 12)) + .map(|fixedpoint2012_signed| { + let two_decimals_is_enough = (fixedpoint2012_signed * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("i20.12: {approximate_symbol}{fixedpoint2012_signed:.2}").into() + }); + + let fixedpoint1616 = nat + .filter(|_| selection.len() == 4) + .map(|nat| f64::from(nat as u32) / f64::from(1 << 16)) + .map(|fixedpoint1616| { + let two_decimals_is_enough = (fixedpoint1616 * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("16.16: {approximate_symbol}{fixedpoint1616:.2}").into() + }); + + let fixedpoint1616_signed = int + .filter(|_| selection.len() == 4) + .map(|int| f64::from(int as i32) / f64::from(1 << 16)) + .map(|fixedpoint1616_signed| { + let two_decimals_is_enough = (fixedpoint1616_signed * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("i16.16: {approximate_symbol}{fixedpoint1616_signed:.2}").into() + }); + + let fixedpoint124 = nat + .filter(|_| selection.len() == 2) + .map(|nat| f64::from(nat as u16) / f64::from(1 << 4)) + .map(|fixedpoint124| { + let two_decimals_is_enough = (fixedpoint124 * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("12.4: {approximate_symbol}{fixedpoint124:.2}").into() + }); + + let fixedpoint88 = nat + .filter(|_| selection.len() == 2) + .map(|nat| f64::from(nat as u16) / f64::from(1 << 8)) + .map(|fixedpoint88| { + let two_decimals_is_enough = (fixedpoint88 * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("8.8: {approximate_symbol}{fixedpoint88:.2}").into() + }); + + let fixedpoint412 = nat + .filter(|_| selection.len() == 2) + .map(|nat| f64::from(nat as u16) / f64::from(1 << 12)) + .map(|fixedpoint412| { + let two_decimals_is_enough = (fixedpoint412 * 100.0).fract() == 0.0; + let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; + + format!("4.12: {approximate_symbol}{fixedpoint412:.2}").into() + }); + + let color888 = (selection.len() == 3) + .then(|| [selection[0], selection[1], selection[2]]) + .map(|[red, green, blue]| { + Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) + .fg(Color::Rgb(red, green, blue)) + + }); + + let color555 = nat + .filter(|_| selection.len() == 2) + .filter(|&nat| nat >> 15 == 0) + .map(|nat| color555_to_color888(nat as u16)) + .map(|[red, green, blue]| { + Span::from(format!("555: #{red:02X}{green:02X}{blue:02X}")) + .fg(Color::Rgb(red, green, blue)) + + }); + + int.map(|int| format!("{int}").into()) + .into_iter() + .chain(nat.map(|nat| format!("{nat}").into())) + .chain(utf8) + .chain(fixedpoint2012_signed) + .chain(fixedpoint2012) + .chain(fixedpoint1616_signed) + .chain(fixedpoint1616) + .chain(fixedpoint124) + .chain(fixedpoint88) + .chain(fixedpoint412) + .chain(color888) + .chain(color555) + .collect() +} + +fn inspect_color(selection: &[u8]) -> Vec> { + let nat = bytes_to_nat(selection); + + let color888 = (selection.len() == 3) + .then(|| [selection[0], selection[1], selection[2]]) + .map(|[red, green, blue]| { + Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) + .fg(Color::Rgb(red, green, blue)) + + }); + + let color555 = nat + .filter(|_| selection.len() == 2) + .filter(|&nat| nat >> 15 == 0) + .map(|nat| color555_to_color888(nat as u16)) + .map(|[red, green, blue]| { + Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) + .fg(Color::Rgb(red, green, blue)) + + }); + + color888 + .into_iter() + .chain(color555) + .collect() +} + // helpers impl Buffer { pub fn clamp_screen_to_primary_cursor(&mut self, window_size: WindowSize) { diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index ceb83c3..1509c23 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -209,12 +209,8 @@ impl Buffer { window_size: WindowSize ) -> Option { self.alert_message = "".into(); - self.popups.clear(); - // self.logs.push(format!("{event:?}")); - let was_inspecting_selection = self.inspecting_selection; - let app_action = match self.partial_action { Some(PartialAction::Replace) => { self.handle_replace(event, window_size); @@ -233,10 +229,6 @@ impl Buffer { _ => self.handle_other_modes(event, config, window_size), }; - if was_inspecting_selection { - self.inspecting_selection = false; - } - assert!(self.scroll_position.is_multiple_of(BYTES_PER_LINE)); assert!(self.scroll_position < self.contents.len()); assert!(self.primary_cursor.head < self.contents.len()); @@ -289,6 +281,12 @@ impl Buffer { let Some(keybinds) = mode_config.0.get(&self.partial_action) && let Some(action) = keybinds.0.get(&event.into()) { + if action.clears_popups() { + self.popups.clear(); + } + + let was_inspecting_selection = self.inspecting_selection; + match action { Action::App(app_action) => result = Some(*app_action), Action::Buffer(buffer_action) => self.execute(*buffer_action, window_size), @@ -306,6 +304,10 @@ impl Buffer { self.clamp_screen_to_primary_cursor(window_size); }, } + + if was_inspecting_selection { + self.inspecting_selection = false; + } } if should_reset_partial { diff --git a/src/main.rs b/src/main.rs index 43b932a..79434c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,6 @@ const LINES_OF_PADDING: usize = 5; const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE; // TODO: -// - keep popups for some actions // - inspector translations for varint // - search // - ascii and bytes (`/` and `A-/`?)