use hjkl_buffer::is_keyword_char;
use hjkl_engine::{Host, VimMode};
use crate::app::window::WindowId;
use super::App;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum HopKind {
Word,
WordCap,
LineBelow,
LineAbove,
}
#[derive(Debug, Clone)]
pub(crate) struct HopTarget {
pub row: usize,
pub col: usize,
pub label: String,
}
#[derive(Debug, Clone)]
pub(crate) struct HopState {
pub win_id: WindowId,
pub targets: Vec<HopTarget>,
pub typed: String,
#[allow(dead_code)]
pub visual: bool,
}
const LABEL_CHARS: &[char] = &[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
];
pub(crate) fn assign_labels(n: usize) -> Vec<String> {
if n == 0 {
return Vec::new();
}
let alpha = LABEL_CHARS;
let len = alpha.len(); if n <= len {
alpha[..n].iter().map(|c| c.to_string()).collect()
} else {
let mut labels = Vec::with_capacity(n);
'outer: for &first in alpha {
for &second in alpha {
if labels.len() >= n {
break 'outer;
}
labels.push(format!("{first}{second}"));
}
}
labels
}
}
fn first_non_blank(line_str: &str) -> usize {
line_str
.char_indices()
.find(|(_, c)| !c.is_whitespace())
.map(|(byte_idx, _)| line_str[..byte_idx].chars().count())
.unwrap_or(0)
}
#[derive(PartialEq, Eq)]
enum CharKind {
Word,
Punct,
Space,
}
fn char_kind(c: char, iskeyword: &str) -> CharKind {
if c.is_whitespace() {
CharKind::Space
} else if is_keyword_char(c, iskeyword) {
CharKind::Word
} else {
CharKind::Punct
}
}
fn word_starts(line_str: &str, iskeyword: &str) -> Vec<usize> {
let mut targets = Vec::new();
let chars: Vec<char> = line_str.chars().collect();
let n = chars.len();
if n == 0 {
return targets;
}
if !chars[0].is_whitespace() {
targets.push(0);
}
for i in 1..n {
let prev_kind = char_kind(chars[i - 1], iskeyword);
let cur_kind = char_kind(chars[i], iskeyword);
if cur_kind != CharKind::Space && (prev_kind == CharKind::Space || prev_kind != cur_kind) {
targets.push(i);
}
}
targets
}
fn big_word_starts(line_str: &str) -> Vec<usize> {
let mut targets = Vec::new();
let chars: Vec<char> = line_str.chars().collect();
let n = chars.len();
if n == 0 {
return targets;
}
if !chars[0].is_whitespace() {
targets.push(0);
}
for i in 1..n {
if !chars[i].is_whitespace() && chars[i - 1].is_whitespace() {
targets.push(i);
}
}
targets
}
impl App {
fn hop_targets(&self, win: WindowId, kind: HopKind) -> Vec<(usize, usize)> {
let editor = self.window_editor(win);
let vp = editor.host().viewport();
let top = vp.top_row;
let height = vp.height as usize;
let rope = editor.buffer().rope();
let total_lines = rope.len_lines();
let bot = (top + height).min(total_lines);
let iskeyword = editor.settings().iskeyword.clone();
let (cursor_row, _) = editor.cursor();
let mut results = Vec::new();
for row in top..bot {
let line_str = hjkl_buffer::rope_line_str(&rope, row);
let line_str = line_str.trim_end_matches('\n');
match kind {
HopKind::Word => {
for col in word_starts(line_str, &iskeyword) {
results.push((row, col));
}
}
HopKind::WordCap => {
for col in big_word_starts(line_str) {
results.push((row, col));
}
}
HopKind::LineBelow => {
if row > cursor_row {
let col = first_non_blank(line_str);
if !line_str.is_empty() {
results.push((row, col));
}
}
}
HopKind::LineAbove => {
if row < cursor_row {
let col = first_non_blank(line_str);
if !line_str.is_empty() {
results.push((row, col));
}
}
}
}
}
results
}
pub(crate) fn start_hop(&mut self, kind: HopKind) {
let win = self.focused_window();
let visual = matches!(
self.active_editor().vim_mode(),
VimMode::Visual | VimMode::VisualLine | VimMode::VisualBlock
);
let raw = self.hop_targets(win, kind);
if raw.is_empty() {
return;
}
let labels = assign_labels(raw.len());
let targets: Vec<HopTarget> = raw
.into_iter()
.zip(labels)
.map(|((row, col), label)| HopTarget { row, col, label })
.collect();
self.hop = Some(HopState {
win_id: win,
targets,
typed: String::new(),
visual,
});
self.pending_recompute = true;
}
pub(crate) fn hop_handle_key(&mut self, ch: Option<char>, esc: bool) {
if esc || ch.is_none() {
self.hop = None;
self.pending_recompute = true;
return;
}
let c = ch.unwrap();
if let Some(h) = self.hop.as_mut() {
h.typed.push(c);
}
let (typed, targets_snap) = {
let h = self.hop.as_ref().unwrap();
(h.typed.clone(), h.targets.clone())
};
let possible: Vec<&HopTarget> = targets_snap
.iter()
.filter(|t| t.label.starts_with(&typed))
.collect();
let exact: Vec<&HopTarget> = possible
.iter()
.copied()
.filter(|t| t.label == typed)
.collect();
if possible.is_empty() {
self.hop = None;
self.pending_recompute = true;
return;
}
if exact.len() == 1 && (exact[0].label.len() == typed.len()) {
let target_row = exact[0].row;
let target_col = exact[0].col;
let h = self.hop.take().unwrap();
self.resolve_hop(h, target_row, target_col);
return;
}
self.pending_recompute = true;
}
fn resolve_hop(&mut self, _h: HopState, target_row: usize, target_col: usize) {
self.active_editor_mut().jump_cursor(target_row, target_col);
self.sync_after_engine_mutation();
self.pending_recompute = true;
}
}