From 5716ae804d5f1007acfbb0ee4a99bc840c5b982b Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Sat, 21 Mar 2026 01:10:24 -0500 Subject: [PATCH] jump to selected offset --- src/action.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/config.rs | 4 ++++ src/main.rs | 4 +++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/action.rs b/src/action.rs index 2798d38..8e7b2e1 100644 --- a/src/action.rs +++ b/src/action.rs @@ -81,6 +81,8 @@ pub enum Action { SplitSelectionsInto7s, SplitSelectionsInto8s, SplitSelectionsInto9s, + + JumpToSelectedOffset, } // actions that act on the app as a whole, not just one buffer @@ -171,6 +173,8 @@ impl Buffer { Action::SplitSelectionsInto7s => self.split_selections_into_size(7), Action::SplitSelectionsInto8s => self.split_selections_into_size(8), Action::SplitSelectionsInto9s => self.split_selections_into_size(9), + + Action::JumpToSelectedOffset => self.jump_to_selected_offset(window_size), } None @@ -600,12 +604,48 @@ impl Buffer { cursor .range() .step_by(size) - .map(|head| Cursor { head, tail: head + size - 1 }) + .map(|tail| Cursor { head: tail + size - 1, tail }) }); self.primary_cursor = new_cursors.next().unwrap(); self.cursors = new_cursors.collect(); } + + fn jump_to_selected_offset(&mut self, window_size: WindowSize) { + if !iter::once(&self.primary_cursor) + .chain(&self.cursors) + .all(|cursor| { + bytes_as_nat(&self.contents[cursor.range()]) + .is_some_and(|offset| offset < self.contents.len()) + }) + { + if self.cursors.is_empty() { + self.alert_message = Span::from( + "selection is not a valid offset" + ).red(); + } else { + self.alert_message = Span::from( + "not all selections are valid offsets" + ).red(); + } + return; + } + + self.primary_cursor = Cursor::at( + bytes_as_nat(&self.contents[self.primary_cursor.range()]).unwrap() + ); + + for cursor in &mut self.cursors { + *cursor = Cursor::at( + bytes_as_nat(&self.contents[cursor.range()]).unwrap() + ); + } + + self.cursors.sort_by_key(|cursor| cursor.head); + + self.combine_cursors_if_overlapping(); + self.clamp_screen_to_primary_cursor(window_size); + } } // helpers @@ -622,3 +662,13 @@ impl Buffer { } } } + +fn bytes_as_nat(bytes: &[u8]) -> Option { + bytes + .iter() + .rev() // little-endian + .skip_while(|&&byte| byte == 0) + .try_fold(usize::default(), |result, &byte| { + Some(result.shl_exact(8)? | (byte as usize)) + }) +} diff --git a/src/config.rs b/src/config.rs index c0280f6..ea42a00 100644 --- a/src/config.rs +++ b/src/config.rs @@ -161,6 +161,8 @@ impl Default for Config { ("7".try_into().unwrap(), Action::SplitSelectionsInto7s), ("8".try_into().unwrap(), Action::SplitSelectionsInto8s), ("9".try_into().unwrap(), Action::SplitSelectionsInto9s), + + ("J".try_into().unwrap(), Action::JumpToSelectedOffset), ].into()), (Some(PartialAction::Goto), [ ("j".try_into().unwrap(), Action::GotoLineStart), @@ -230,6 +232,8 @@ impl Default for Config { ("7".try_into().unwrap(), Action::SplitSelectionsInto7s), ("8".try_into().unwrap(), Action::SplitSelectionsInto8s), ("9".try_into().unwrap(), Action::SplitSelectionsInto9s), + + ("J".try_into().unwrap(), Action::JumpToSelectedOffset), ].into()), (Some(PartialAction::Space), [ ("w".try_into().unwrap(), Action::Save), diff --git a/src/main.rs b/src/main.rs index 3abd807..0403add 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #![warn(clippy::pedantic, clippy::nursery)] #![allow(clippy::cast_possible_truncation)] #![feature(get_disjoint_mut_helpers)] +#![feature(exact_bitshifts)] use app::App; @@ -20,7 +21,6 @@ const BYTES_PER_CHUNK: usize = 4; const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // TODO: -// - J jump to offset under cursor // - m mark offset // - search // - s/A-k/A-K @@ -40,6 +40,8 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // - y/p // - [/] to cycle view offset? // - gj jump to entered offset +// - repeat X times (dec and hex) +// - jump relative to last marker? // future directions // - 'views' for bytes (i8/16/etc u8/16/etc 20.12/8.4/etc)