Skip to main content

ai_agent/utils/
cursor.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/ink/cursor.ts
2//! Cursor utilities for text editing
3//!
4//! Translated from openclaudecode/src/utils/Cursor.ts
5
6use once_cell::sync::Lazy;
7use regex::Regex;
8
9/// Kill ring for storing killed (cut) text that can be yanked (pasted) with Ctrl+Y.
10/// This is global state that shares one kill ring across all input fields.
11const KILL_RING_MAX_SIZE: usize = 10;
12
13static KILL_RING: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
14static KILL_RING_INDEX: std::sync::Mutex<usize> = std::sync::Mutex::new(0);
15static LAST_ACTION_WAS_KILL: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
16
17// Track yank state for yank-pop (alt-y)
18static LAST_YANK_START: std::sync::Mutex<usize> = std::sync::Mutex::new(0);
19static LAST_YANK_LENGTH: std::sync::Mutex<usize> = std::sync::Mutex::new(0);
20static LAST_ACTION_WAS_YANK: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
21
22/// Push text to the kill ring
23pub fn push_to_kill_ring(text: &str, direction: &str) {
24    if text.is_empty() {
25        return;
26    }
27
28    let mut kill_ring = KILL_RING.lock().unwrap();
29    let mut last_action_kill = LAST_ACTION_WAS_KILL.lock().unwrap();
30
31    if *last_action_kill && !kill_ring.is_empty() {
32        // Accumulate with the most recent kill
33        if direction == "prepend" {
34            kill_ring[0] = format!("{}{}", text, kill_ring[0]);
35        } else {
36            kill_ring[0] = format!("{}{}", kill_ring[0], text);
37        }
38    } else {
39        // Add new entry to front of ring
40        kill_ring.insert(0, text.to_string());
41        if kill_ring.len() > KILL_RING_MAX_SIZE {
42            kill_ring.pop();
43        }
44    }
45    *last_action_kill = true;
46
47    // Reset yank state when killing new text
48    let mut last_action_yank = LAST_ACTION_WAS_YANK.lock().unwrap();
49    *last_action_yank = false;
50}
51
52/// Get the last kill from the ring
53pub fn get_last_kill() -> String {
54    let kill_ring = KILL_RING.lock().unwrap();
55    kill_ring.first().cloned().unwrap_or_default()
56}
57
58/// Get an item from the kill ring by index
59pub fn get_kill_ring_item(index: usize) -> String {
60    let kill_ring = KILL_RING.lock().unwrap();
61    if kill_ring.is_empty() {
62        return String::new();
63    }
64    let normalized_index = ((index % kill_ring.len()) + kill_ring.len()) % kill_ring.len();
65    kill_ring.get(normalized_index).cloned().unwrap_or_default()
66}
67
68/// Get the size of the kill ring
69pub fn get_kill_ring_size() -> usize {
70    let kill_ring = KILL_RING.lock().unwrap();
71    kill_ring.len()
72}
73
74/// Clear the kill ring
75pub fn clear_kill_ring() {
76    let mut kill_ring = KILL_RING.lock().unwrap();
77    let mut kill_ring_index = KILL_RING_INDEX.lock().unwrap();
78    let mut last_action_kill = LAST_ACTION_WAS_KILL.lock().unwrap();
79    let mut last_action_yank = LAST_ACTION_WAS_YANK.lock().unwrap();
80    let mut last_yank_start = LAST_YANK_START.lock().unwrap();
81    let mut last_yank_length = LAST_YANK_LENGTH.lock().unwrap();
82
83    kill_ring.clear();
84    *kill_ring_index = 0;
85    *last_action_kill = false;
86    *last_action_yank = false;
87    *last_yank_start = 0;
88    *last_yank_length = 0;
89}
90
91/// Reset kill accumulation state
92pub fn reset_kill_accumulation() {
93    let mut last_action_kill = LAST_ACTION_WAS_KILL.lock().unwrap();
94    *last_action_kill = false;
95}
96
97/// Record a yank for yank-pop functionality
98pub fn record_yank(start: usize, length: usize) {
99    let mut last_yank_start = LAST_YANK_START.lock().unwrap();
100    let mut last_yank_length = LAST_YANK_LENGTH.lock().unwrap();
101    let mut last_action_yank = LAST_ACTION_WAS_YANK.lock().unwrap();
102    let mut kill_ring_index = KILL_RING_INDEX.lock().unwrap();
103
104    *last_yank_start = start;
105    *last_yank_length = length;
106    *last_action_yank = true;
107    *kill_ring_index = 0;
108}
109
110/// Check if yank-pop is possible
111pub fn can_yank_pop() -> bool {
112    let last_action_yank = LAST_ACTION_WAS_YANK.lock().unwrap();
113    let kill_ring = KILL_RING.lock().unwrap();
114    *last_action_yank && kill_ring.len() > 1
115}
116
117/// Perform yank-pop operation
118pub fn yank_pop() -> Option<YankPopResult> {
119    let last_action_yank = LAST_ACTION_WAS_YANK.lock().unwrap();
120    let kill_ring = KILL_RING.lock().unwrap();
121
122    if !*last_action_yank || kill_ring.len() <= 1 {
123        return None;
124    }
125    drop(last_action_yank);
126    drop(kill_ring);
127
128    let mut kill_ring_index = KILL_RING_INDEX.lock().unwrap();
129    let last_yank_start = LAST_YANK_START.lock().unwrap();
130    let last_yank_length = LAST_YANK_LENGTH.lock().unwrap();
131
132    // Cycle to next item in kill ring
133    let kill_ring = KILL_RING.lock().unwrap();
134    *kill_ring_index = (*kill_ring_index + 1) % kill_ring.len();
135    let text = kill_ring.get(*kill_ring_index).cloned().unwrap_or_default();
136
137    Some(YankPopResult {
138        text,
139        start: *last_yank_start,
140        length: *last_yank_length,
141    })
142}
143
144/// Update the yank length
145pub fn update_yank_length(length: usize) {
146    let mut last_yank_length = LAST_YANK_LENGTH.lock().unwrap();
147    *last_yank_length = length;
148}
149
150/// Reset yank state
151pub fn reset_yank_state() {
152    let mut last_action_yank = LAST_ACTION_WAS_YANK.lock().unwrap();
153    *last_action_yank = false;
154}
155
156/// Result from yank-pop operation
157#[derive(Debug, Clone)]
158pub struct YankPopResult {
159    pub text: String,
160    pub start: usize,
161    pub length: usize,
162}
163
164// Pre-compiled regex patterns for Vim word detection
165pub static VIM_WORD_CHAR_REGEX: Lazy<Regex> =
166    Lazy::new(|| Regex::new(r"^[\p{L}\p{N}\p{M}_]$").unwrap());
167pub static WHITESPACE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s").unwrap());
168
169/// Check if character is a Vim word character (letter, digit, underscore)
170pub fn is_vim_word_char(ch: &str) -> bool {
171    VIM_WORD_CHAR_REGEX.is_match(ch)
172}
173
174/// Check if character is whitespace
175pub fn is_vim_whitespace(ch: &str) -> bool {
176    WHITESPACE_REGEX.is_match(ch)
177}
178
179/// Check if character is Vim punctuation
180pub fn is_vim_punctuation(ch: &str) -> bool {
181    !ch.is_empty() && !is_vim_whitespace(ch) && !is_vim_word_char(ch)
182}