Added word movement support

This commit is contained in:
Joshua Barretto 2025-06-25 22:12:31 +01:00
parent bc2cff34d4
commit 63c420c65b
6 changed files with 86 additions and 51 deletions

View file

@ -14,7 +14,7 @@ pub enum Dir {
pub enum Action { pub enum Action {
Char(char), // Insert a character Char(char), // Insert a character
Indent(bool), // Indent (indent vs deindent) Indent(bool), // Indent (indent vs deindent)
Move(Dir, bool, bool), // Move the cursor (dir, page, retain_base) Move(Dir, bool, bool, bool), // Move the cursor (dir, page, retain_base, word)
PaneMove(Dir), // Move panes PaneMove(Dir), // Move panes
PaneOpen(Dir), // Create a new pane PaneOpen(Dir), // Create a new pane
PaneClose, // Close the current pane PaneClose, // Close the current pane
@ -158,11 +158,8 @@ impl RawEvent {
return None; return None;
}; };
let retain_base = match *modifiers { let retain_base = modifiers.contains(KeyModifiers::SHIFT);
KeyModifiers::NONE => false, let word = modifiers.contains(KeyModifiers::CONTROL);
KeyModifiers::SHIFT => true,
_ => return None,
};
let (dir, page) = match code { let (dir, page) = match code {
KeyCode::PageUp => (Dir::Up, true), KeyCode::PageUp => (Dir::Up, true),
@ -174,7 +171,7 @@ impl RawEvent {
_ => return None, _ => return None,
}; };
Some(Action::Move(dir, page, retain_base)) Some(Action::Move(dir, page, retain_base, word))
} }
pub fn to_select_token(&self) -> Option<Action> { pub fn to_select_token(&self) -> Option<Action> {

View file

@ -267,6 +267,7 @@ impl Buffer {
dir: Dir, dir: Dir,
dist: [usize; 2], dist: [usize; 2],
retain_base: bool, retain_base: bool,
word: bool,
) { ) {
let Some(cursor) = self.cursors.get_mut(cursor_id) else { let Some(cursor) = self.cursors.get_mut(cursor_id) else {
return; return;
@ -275,6 +276,14 @@ impl Buffer {
Dir::Left => { Dir::Left => {
cursor.pos = if !retain_base && cursor.base < cursor.pos { cursor.pos = if !retain_base && cursor.base < cursor.pos {
cursor.base cursor.base
} else if let (true, Some(mut pos)) = (word, cursor.pos.checked_sub(1)) {
let class = self.text.chars().get(pos).copied().map(classify);
loop {
pos = match pos.checked_sub(1) {
Some(pos) if self.text.chars().get(pos).copied().map(classify) == class => pos,
_ => break pos,
}
}
} else { } else {
cursor.pos.saturating_sub(dist[0]) cursor.pos.saturating_sub(dist[0])
}; };
@ -283,6 +292,16 @@ impl Buffer {
Dir::Right => { Dir::Right => {
cursor.pos = if !retain_base && cursor.base > cursor.pos { cursor.pos = if !retain_base && cursor.base > cursor.pos {
cursor.base cursor.base
} else if word {
let mut pos = cursor.pos;
let class = self.text.chars().get(pos).copied().map(classify);
loop {
pos = if self.text.chars().get(pos).copied().map(classify) == class {
pos + 1
} else {
break pos
};
}
} else { } else {
(cursor.pos + dist[0]).min(self.text.chars.len()) (cursor.pos + dist[0]).min(self.text.chars.len())
}; };
@ -404,6 +423,16 @@ impl Buffer {
} }
} }
// CLassify the character by property
fn classify(c: char) -> u8 {
match c {
// c if c.is_ascii_whitespace() => 0,
c if c.is_alphanumeric() || c == '_' => 1,
_ => 2,
}
}
pub struct State { pub struct State {
pub buffers: HopSlotMap<BufferId, Buffer>, pub buffers: HopSlotMap<BufferId, Buffer>,
pub tick: u64, pub tick: u64,

View file

@ -35,6 +35,7 @@ impl Default for BorderTheme {
pub struct Theme { pub struct Theme {
pub ui_bg: Color, pub ui_bg: Color,
pub select_bg: Color, pub select_bg: Color,
pub line_select_bg: Color,
pub unfocus_select_bg: Color, pub unfocus_select_bg: Color,
pub search_result_bg: Color, pub search_result_bg: Color,
pub margin_bg: Color, pub margin_bg: Color,
@ -69,8 +70,9 @@ impl Default for Theme {
Self { Self {
ui_bg: Color::AnsiValue(235), ui_bg: Color::AnsiValue(235),
select_bg: Color::AnsiValue(23), select_bg: Color::AnsiValue(23),
line_select_bg: Color::AnsiValue(8),
unfocus_select_bg: Color::AnsiValue(240), unfocus_select_bg: Color::AnsiValue(240),
search_result_bg: Color::AnsiValue(124), search_result_bg: Color::AnsiValue(66),
margin_bg: Color::Reset, margin_bg: Color::Reset,
margin_line_num: Color::AnsiValue(245), margin_line_num: Color::AnsiValue(245),
border: BorderTheme::default(), border: BorderTheme::default(),

View file

@ -124,7 +124,7 @@ impl Visual for Doc {
[0, 0], [0, 0],
[frame.size()[0], frame.size()[1].saturating_sub(search_h)], [frame.size()[0], frame.size()[1].saturating_sub(search_h)],
) )
.with_focus(self.search.is_none()) .with_focus(true/*self.search.is_none()*/)
.with(|f| { .with(|f| {
self.input.render( self.input.render(
state, state,
@ -208,7 +208,7 @@ impl Search {
return Ok(Resp::end(None)); return Ok(Resp::end(None));
} }
Some(Action::Go) => return Ok(Resp::end(None)), Some(Action::Go) => return Ok(Resp::end(None)),
Some(Action::Move(dir, false, _)) => { Some(Action::Move(dir, false, false, false)) => {
match dir { match dir {
Dir::Up => { Dir::Up => {
self.selected = (self.selected + self.results.len().saturating_sub(1)) self.selected = (self.selected + self.results.len().saturating_sub(1))

View file

@ -75,13 +75,13 @@ impl Input {
self.refocus(buffer, cursor_id); self.refocus(buffer, cursor_id);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::Move(dir, page, retain_base)) => { Some(Action::Move(dir, page, retain_base, word)) => {
let dist = if page { let dist = if page {
self.last_size.map(|s| s.saturating_sub(3).max(1)) self.last_size.map(|s| s.saturating_sub(3).max(1))
} else { } else {
[1, 1] [1, 1]
}; };
buffer.move_cursor(cursor_id, dir, dist, retain_base); buffer.move_cursor(cursor_id, dir, dist, retain_base, word);
self.refocus(buffer, cursor_id); self.refocus(buffer, cursor_id);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
@ -171,42 +171,49 @@ impl Input {
let mut frame = frame.rect([margin_w, i], [!0, 1]); let mut frame = frame.rect([margin_w, i], [!0, 1]);
for i in 0..frame.size()[0] { for i in 0..frame.size()[0] {
let coord = self.focus[0] + i as isize; let coord = self.focus[0] + i as isize;
if (0..line.len() as isize).contains(&coord) { let line_selected = (line_pos..line_pos + line.len()).contains(&cursor.pos);
let pos = line_pos + coord as usize; let pos = if i < line.len() {
let selected = cursor.selection().map_or(false, |s| s.contains(&pos)); Some(line_pos + coord as usize)
let (fg, c) = match line[coord as usize] { } else {
'\n' if selected => (state.theme.whitespace, '⮠'), None
c => { };
if let Some(fg) = buffer let selected = cursor.selection().zip(pos).map_or(false, |(s, pos)| s.contains(&pos));
.highlights let (fg, c) = match line.get(coord as usize).copied() {
.as_ref() Some('\n') if selected => (state.theme.whitespace, '⮠'),
.and_then(|hl| hl.get_at(pos)) Some(c) => {
.map(|tok| state.theme.token_color(tok.kind)) if let Some(fg) = buffer
{ .highlights
(fg, c) .as_ref()
} else { .and_then(|hl| hl.get_at(pos?))
(state.theme.text, c) .map(|tok| state.theme.token_color(tok.kind))
} {
(fg, c)
} else {
(state.theme.text, c)
} }
}; }
let bg = if let Some(s) = search { None => (Color::Reset, ' '),
match s.contains(pos) { };
Some(true) => state.theme.select_bg, let bg = match search.map(|s| s.contains(pos?)) {
Some(false) => state.theme.search_result_bg, Some(Some(true)) => state.theme.select_bg,
None => Color::Reset, Some(Some(false)) => state.theme.search_result_bg,
Some(None) if line_selected && frame.has_focus() => state.theme.line_select_bg,
_ => if selected {
if frame.has_focus() {
state.theme.select_bg
} else {
state.theme.unfocus_select_bg
} }
} else if !selected { } else if line_selected && frame.has_focus() {
Color::Reset state.theme.line_select_bg
} else if frame.has_focus() {
state.theme.select_bg
} else { } else {
state.theme.unfocus_select_bg Color::Reset
}; },
frame };
.with_bg(bg) frame
.with_fg(fg) .with_bg(bg)
.text([i as isize, 0], c.encode_utf8(&mut [0; 4])); .with_fg(fg)
} .text([i as isize, 0], c.encode_utf8(&mut [0; 4]));
} }
// Set cursor position // Set cursor position

View file

@ -149,12 +149,12 @@ impl<T> Options<T> {
impl<T: Clone> Element<T> for Options<T> { impl<T: Clone> Element<T> for Options<T> {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<T>, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<T>, Event> {
match event.to_action(|e| e.to_go().or_else(|| e.to_move())) { match event.to_action(|e| e.to_go().or_else(|| e.to_move())) {
Some(Action::Move(Dir::Up, false, _)) => { Some(Action::Move(dir, false, false, false)) => {
self.selected = (self.selected + self.ranking.len() - 1) % self.ranking.len(); match dir {
Ok(Resp::handled(None)) Dir::Up => self.selected = (self.selected + self.ranking.len() - 1) % self.ranking.len(),
} Dir::Down => self.selected = (self.selected + 1) % self.ranking.len(),
Some(Action::Move(Dir::Down, false, _)) => { _ => return Err(event),
self.selected = (self.selected + 1) % self.ranking.len(); }
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::Go) => { Some(Action::Go) => {