use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::editor::{Buffer, Cursor};
use super::{App, Toast};
const ALPHABET: &[char] = &[
'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l',
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p',
'z', 'x', 'c', 'v', 'b', 'n', 'm',
];
#[derive(Debug, Clone)]
pub struct JumpLabel {
pub pos: Cursor,
pub first: char,
pub second: Option<char>,
}
#[derive(Debug)]
pub struct JumpState {
pub labels: Vec<JumpLabel>,
pub typed_first: Option<char>,
}
impl App {
pub(super) fn start_jump_label(&mut self) {
let targets = collect_jump_targets(&self.buffer);
if targets.is_empty() {
self.toast = Toast::info("no jump targets");
return;
}
let labels = assign_labels(targets);
self.jump_state = Some(JumpState {
labels,
typed_first: None,
});
self.toast = Toast::info("jump: type label (Esc to cancel)");
}
pub(super) fn handle_jump_key(&mut self, key: KeyEvent) {
if key.code == KeyCode::Esc
|| (key.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(key.code, KeyCode::Char('c') | KeyCode::Char('g')))
{
self.cancel_jump();
return;
}
let KeyCode::Char(ch) = key.code else {
self.cancel_jump();
return;
};
let Some(state) = self.jump_state.as_mut() else {
return;
};
match state.typed_first {
None => {
let mut matched: Vec<&JumpLabel> =
state.labels.iter().filter(|l| l.first == ch).collect();
if matched.is_empty() {
self.cancel_jump();
return;
}
if matched.len() == 1 {
let pos = matched.remove(0).pos;
self.finish_jump(pos);
return;
}
state.typed_first = Some(ch);
}
Some(first) => {
let target = state
.labels
.iter()
.find(|l| l.first == first && l.second == Some(ch))
.map(|l| l.pos);
match target {
Some(pos) => self.finish_jump(pos),
None => self.cancel_jump(),
}
}
}
}
fn finish_jump(&mut self, pos: Cursor) {
self.buffer.cursor = pos;
self.jump_state = None;
self.toast = Toast::info("");
}
fn cancel_jump(&mut self) {
self.jump_state = None;
self.toast = Toast::info("jump cancelled");
}
}
fn collect_jump_targets(buffer: &Buffer) -> Vec<Cursor> {
let scroll = buffer.scroll.get();
let height = buffer.viewport_height.get();
if height == 0 {
return Vec::new();
}
let last = (scroll + height).min(buffer.lines.len());
let is_word = |c: char| c.is_alphanumeric() || c == '_';
let mut out = Vec::new();
for row in scroll..last {
let mut prev_word = false;
for (col, c) in buffer.lines[row].chars().enumerate() {
let cur_word = is_word(c);
if cur_word && !prev_word {
out.push(Cursor { row, col });
}
prev_word = cur_word;
}
}
out
}
fn assign_labels(targets: Vec<Cursor>) -> Vec<JumpLabel> {
let a = ALPHABET.len();
let n = targets.len();
let max = a * a;
targets
.into_iter()
.take(max)
.enumerate()
.map(|(i, pos)| {
let (first, second) = if n <= a {
(ALPHABET[i], None)
} else {
(ALPHABET[i % a], Some(ALPHABET[i / a]))
};
JumpLabel { pos, first, second }
})
.collect()
}