Skip to main content

hjkl_buffer/
motion.rs

1//! Motion vocabulary helpers.
2//!
3//! Patch C (0.0.30) relocated the 24 inherent vim motion helpers
4//! that lived here onto [`hjkl_engine::motions`] free functions
5//! over `&mut hjkl_buffer::Buffer`. Motions don't belong on `Buffer`
6//! — they're computed over the buffer, not delegated to it; the
7//! relocation is a step toward 0.1.0's full motion-as-trait-bound
8//! generic-ification.
9//!
10//! What stays in this module: [`is_keyword_char`] — the
11//! `iskeyword`-spec parser. Keyword classification is data over the
12//! `iskeyword` string and a single `char`; it has no buffer
13//! dependency, so the engine motions module re-exports it from here.
14
15/// Match `c` against a vim-style `iskeyword` spec. Tokens are
16/// comma-separated; understood forms: `@` (any alphabetic),
17/// `_` (literal underscore), `N-M` (decimal char-code range, inclusive),
18/// bare integer `N` (single char code), single ASCII punctuation char
19/// (literal). Unknown tokens are ignored.
20pub fn is_keyword_char(c: char, spec: &str) -> bool {
21    for raw in spec.split(',') {
22        let token = raw.trim();
23        if token.is_empty() {
24            continue;
25        }
26        if token == "@" {
27            if c.is_alphabetic() {
28                return true;
29            }
30            continue;
31        }
32        if let Some((lo, hi)) = token.split_once('-')
33            && let (Ok(lo), Ok(hi)) = (lo.parse::<u32>(), hi.parse::<u32>())
34        {
35            if (lo..=hi).contains(&(c as u32)) {
36                return true;
37            }
38            continue;
39        }
40        if let Ok(n) = token.parse::<u32>() {
41            if c as u32 == n {
42                return true;
43            }
44            continue;
45        }
46        let mut chars = token.chars();
47        if let (Some(only), None) = (chars.next(), chars.next())
48            && c == only
49        {
50            return true;
51        }
52    }
53    false
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn iskeyword_alphabetic_via_at() {
62        assert!(is_keyword_char('a', "@"));
63        assert!(is_keyword_char('Z', "@"));
64        assert!(!is_keyword_char('1', "@"));
65    }
66
67    #[test]
68    fn iskeyword_numeric_range() {
69        assert!(is_keyword_char('0', "48-57"));
70        assert!(is_keyword_char('9', "48-57"));
71        assert!(!is_keyword_char('a', "48-57"));
72    }
73
74    #[test]
75    fn iskeyword_literal_punctuation() {
76        assert!(is_keyword_char('_', "_"));
77        assert!(!is_keyword_char('.', "_"));
78    }
79
80    #[test]
81    fn iskeyword_default_spec() {
82        // Matches vim default `@,48-57,_,192-255` and engine's
83        // `Settings::default()`.
84        let spec = "@,48-57,_,192-255";
85        assert!(is_keyword_char('a', spec));
86        assert!(is_keyword_char('5', spec));
87        assert!(is_keyword_char('_', spec));
88        assert!(!is_keyword_char(' ', spec));
89        assert!(!is_keyword_char('.', spec));
90    }
91}