1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::io;
use std::time::Duration;
use super::app_state::{App, Focus};
use crate::clipboard;
use crate::editor;
use crate::editor::EditorMode;
use crate::history;
use crate::results;
mod global;
/// Timeout for event polling - allows periodic UI refresh for notifications
const EVENT_POLL_TIMEOUT: Duration = Duration::from_millis(100);
impl App {
/// Handle events and update application state
pub fn handle_events(&mut self) -> io::Result<()> {
// Check for pending debounced execution before processing new events
// This ensures queries are executed after the debounce period (50ms) has elapsed
if self.debouncer.should_execute() {
editor::editor_events::execute_query(self);
self.debouncer.mark_executed();
}
// Poll with timeout to allow periodic refresh for notification expiration
if event::poll(EVENT_POLL_TIMEOUT)? {
match event::read()? {
// Check that it's a key press event to avoid duplicates
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_event(key_event);
}
// Handle paste events (bracketed paste mode)
Event::Paste(text) => {
self.handle_paste_event(text);
}
_ => {}
}
}
Ok(())
}
/// Handle paste events from bracketed paste mode
/// Inserts all pasted text at once and executes query immediately (no debounce)
fn handle_paste_event(&mut self, text: String) {
// Insert all text at once into the textarea
self.input.textarea.insert_str(&text);
// Rebuild brace tracker for autocomplete context detection
self.input
.brace_tracker
.rebuild(self.input.textarea.lines()[0].as_ref());
// Execute query immediately (no debounce for paste operations)
editor::editor_events::execute_query(self);
// Update autocomplete suggestions after paste
self.update_autocomplete();
// Update tooltip based on new cursor position
self.update_tooltip();
}
/// Handle key press events
pub fn handle_key_event(&mut self, key: KeyEvent) {
// Handle search keys FIRST when search is visible
// This ensures Enter confirms search instead of executing query
if crate::search::search_events::handle_search_key(self, key) {
return; // Key was handled by search
}
// Try global keys next
if global::handle_global_keys(self, key) {
return; // Key was handled globally
}
// Handle clipboard Ctrl+Y before mode-specific handling
if clipboard::clipboard_events::handle_clipboard_key(self, key, self.clipboard_backend) {
return; // Key was handled by clipboard
}
// Not a global key, delegate to focused pane
match self.focus {
Focus::InputField => self.handle_input_field_key(key),
Focus::ResultsPane => results::results_events::handle_results_pane_key(self, key),
}
}
/// Handle keys when Input field is focused
fn handle_input_field_key(&mut self, key: KeyEvent) {
// Handle history popup when visible
if self.history.is_visible() {
history::history_events::handle_history_popup_key(self, key);
return;
}
// Handle ESC - close autocomplete and switch to Normal mode
if key.code == KeyCode::Esc {
if self.autocomplete.is_visible() {
self.autocomplete.hide();
}
self.input.editor_mode = EditorMode::Normal;
return;
}
// Handle autocomplete navigation (in Insert mode only)
if self.input.editor_mode == EditorMode::Insert && self.autocomplete.is_visible() {
match key.code {
KeyCode::Down => {
self.autocomplete.select_next();
return;
}
KeyCode::Up => {
self.autocomplete.select_previous();
return;
}
_ => {}
}
}
// Handle history trigger (in Insert mode only)
if self.input.editor_mode == EditorMode::Insert {
// Ctrl+P: Cycle to previous (older) history entry
if key.code == KeyCode::Char('p') && key.modifiers.contains(KeyModifiers::CONTROL) {
if let Some(entry) = self.history.cycle_previous() {
self.replace_query_with(&entry);
}
return;
}
// Ctrl+N: Cycle to next (newer) history entry
if key.code == KeyCode::Char('n') && key.modifiers.contains(KeyModifiers::CONTROL) {
if let Some(entry) = self.history.cycle_next() {
self.replace_query_with(&entry);
} else {
// At most recent, clear the input
self.input.textarea.delete_line_by_head();
self.input.textarea.delete_line_by_end();
editor::editor_events::execute_query(self);
}
return;
}
// Ctrl+R: Open history
if key.code == KeyCode::Char('r') && key.modifiers.contains(KeyModifiers::CONTROL) {
self.open_history_popup();
return;
}
// Up arrow: Open history popup (always)
if key.code == KeyCode::Up {
self.open_history_popup();
return;
}
}
// Handle input based on current mode
match self.input.editor_mode {
EditorMode::Insert => editor::editor_events::handle_insert_mode_key(self, key),
EditorMode::Normal => editor::editor_events::handle_normal_mode_key(self, key),
EditorMode::Operator(_) => editor::editor_events::handle_operator_mode_key(self, key),
}
}
/// Replace the current query with the given text
fn replace_query_with(&mut self, text: &str) {
self.input.textarea.delete_line_by_head();
self.input.textarea.delete_line_by_end();
self.input.textarea.insert_str(text);
editor::editor_events::execute_query(self);
}
/// Open the history popup with current query as initial search
fn open_history_popup(&mut self) {
// Don't open if history is empty
if self.history.total_count() == 0 {
return;
}
let query = self.query().to_string();
let initial_query = if query.is_empty() {
None
} else {
Some(query.as_str())
};
self.history.open(initial_query);
self.autocomplete.hide();
}
}
#[cfg(test)]
mod tests {
use crate::test_utils::test_helpers::test_app;
use proptest::prelude::*;
// =========================================================================
// Unit Tests for Paste Event Handling
// =========================================================================
#[test]
fn test_paste_event_inserts_text() {
let mut app = test_app(r#"{"name": "test"}"#);
// Simulate paste event
app.handle_paste_event(".name".to_string());
assert_eq!(app.query(), ".name");
}
#[test]
fn test_paste_event_executes_query() {
let mut app = test_app(r#"{"name": "Alice"}"#);
// Simulate paste event
app.handle_paste_event(".name".to_string());
// Query should have been executed
assert!(app.query.result.is_ok());
let result = app.query.result.as_ref().unwrap();
assert!(result.contains("Alice"));
}
#[test]
fn test_paste_event_appends_to_existing_text() {
let mut app = test_app(r#"{"user": {"name": "Bob"}}"#);
// First, type some text
app.input.textarea.insert_str(".user");
// Then paste more text
app.handle_paste_event(".name".to_string());
assert_eq!(app.query(), ".user.name");
}
#[test]
fn test_paste_event_with_empty_string() {
let mut app = test_app(r#"{"name": "test"}"#);
// Paste empty string
app.handle_paste_event(String::new());
// Query should remain empty
assert_eq!(app.query(), "");
}
#[test]
fn test_paste_event_with_multiline_text() {
let mut app = test_app(r#"{"name": "test"}"#);
// Paste multiline text (jq queries are single-line, but paste should handle it)
app.handle_paste_event(".name\n| length".to_string());
// The textarea handles this - verify text was inserted
assert!(app.query().contains(".name"));
}
// =========================================================================
// Property-Based Tests
// =========================================================================
// Feature: performance, Property 1: Paste text insertion integrity
// *For any* string pasted into the application, the input field content after
// the paste operation should contain exactly that string at the cursor position.
// **Validates: Requirements 1.2**
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_paste_text_insertion_integrity(
// Generate printable ASCII strings (avoiding control characters that might
// cause issues with the textarea)
text in "[a-zA-Z0-9._\\[\\]|? ]{0,50}"
) {
let mut app = test_app(r#"{"test": true}"#);
// Paste the text
app.handle_paste_event(text.clone());
// The query should contain exactly the pasted text
prop_assert_eq!(
app.query(), &text,
"Pasted text should appear exactly in the input field"
);
}
#[test]
fn prop_paste_appends_at_cursor_position(
// Generate two parts of text
prefix in "[a-zA-Z0-9.]{0,20}",
pasted in "[a-zA-Z0-9.]{0,20}",
) {
let mut app = test_app(r#"{"test": true}"#);
// First insert the prefix
app.input.textarea.insert_str(&prefix);
// Then paste additional text
app.handle_paste_event(pasted.clone());
// The query should be prefix + pasted
let expected = format!("{}{}", prefix, pasted);
prop_assert_eq!(
app.query(), &expected,
"Pasted text should be appended at cursor position"
);
}
#[test]
fn prop_paste_executes_query_once(
// Generate valid jq-like queries
query in "\\.[a-z]{1,10}"
) {
let json = r#"{"name": "test", "value": 42}"#;
let mut app = test_app(json);
// Paste a query
app.handle_paste_event(query.clone());
// Query should have been executed (result should be set)
// We can't easily verify "exactly once" but we can verify it was executed
prop_assert!(
app.query.result.is_ok() || app.query.result.is_err(),
"Query should have been executed after paste"
);
// The query text should match what was pasted
prop_assert_eq!(
app.query(), &query,
"Query text should match pasted text"
);
}
}
}