feat: add vim query navigation
This commit is contained in:
+66
@@ -34,6 +34,7 @@ pub struct App {
|
||||
show_schema: bool,
|
||||
query: Input,
|
||||
query_insert: bool,
|
||||
query_g: bool,
|
||||
query_highlights: sql::Highlights,
|
||||
query_result: Option<QueryResult>,
|
||||
error: Option<String>,
|
||||
@@ -59,6 +60,7 @@ impl App {
|
||||
show_schema: false,
|
||||
query: Input::default(),
|
||||
query_insert: false,
|
||||
query_g: false,
|
||||
query_highlights: Vec::new(),
|
||||
query_result: None,
|
||||
error: None,
|
||||
@@ -80,6 +82,7 @@ impl App {
|
||||
}
|
||||
|
||||
pub(crate) fn leave_query_insert(&mut self) -> bool {
|
||||
self.query_g = false;
|
||||
std::mem::replace(&mut self.query_insert, false)
|
||||
}
|
||||
|
||||
@@ -116,6 +119,7 @@ impl App {
|
||||
|
||||
pub(crate) fn handle_query_key(&mut self, key: KeyEvent) {
|
||||
let previous_len = self.query.value.len();
|
||||
let pending_g = std::mem::take(&mut self.query_g);
|
||||
match key.code {
|
||||
KeyCode::F(5) => self.execute_query(),
|
||||
KeyCode::Enter if key.modifiers.contains(KeyModifiers::CONTROL) => self.execute_query(),
|
||||
@@ -140,6 +144,20 @@ impl App {
|
||||
'j' => Some(KeyCode::Down),
|
||||
'k' => Some(KeyCode::Up),
|
||||
'l' => Some(KeyCode::Right),
|
||||
'0' => Some(KeyCode::Home),
|
||||
'$' => Some(KeyCode::End),
|
||||
'^' | 'w' | 'b' | 'e' | 'G' => {
|
||||
self.query.vim_motion(character);
|
||||
None
|
||||
}
|
||||
'g' => {
|
||||
if pending_g {
|
||||
self.query.vim_motion('g');
|
||||
} else {
|
||||
self.query_g = true;
|
||||
}
|
||||
None
|
||||
}
|
||||
'a' => {
|
||||
let (line, column) = self.query_position();
|
||||
self.query
|
||||
@@ -527,6 +545,54 @@ mod tests {
|
||||
assert_eq!(moved_index(Some(1), 3, isize::MAX), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_query_motions_preserve_sql_and_handle_unicode() -> crate::Result<()> {
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
let file = tempfile::NamedTempFile::new()?;
|
||||
let mut app = super::App::new(crate::db::Database::open_sqlite(file.path())?)?;
|
||||
let key = |character| KeyEvent::new(KeyCode::Char(character), KeyModifiers::NONE);
|
||||
app.handle_query_key(key('i'));
|
||||
app.paste_query(" SELECT café,界\n\n FROM data");
|
||||
app.leave_query_insert();
|
||||
for (motion, expected) in [
|
||||
('g', (3, 12)),
|
||||
('g', (1, 1)),
|
||||
('^', (1, 3)),
|
||||
('e', (1, 8)),
|
||||
('w', (1, 10)),
|
||||
('e', (1, 13)),
|
||||
('w', (1, 14)),
|
||||
('w', (1, 15)),
|
||||
('w', (3, 3)),
|
||||
('b', (1, 15)),
|
||||
('b', (1, 14)),
|
||||
('b', (1, 10)),
|
||||
('0', (1, 1)),
|
||||
('$', (1, 16)),
|
||||
('G', (3, 3)),
|
||||
('e', (3, 6)),
|
||||
('w', (3, 8)),
|
||||
('w', (3, 12)),
|
||||
('w', (3, 12)),
|
||||
] {
|
||||
app.handle_query_key(key(motion));
|
||||
assert_eq!(app.query_position(), expected, "motion {motion}");
|
||||
assert!(!app.query_insert_mode());
|
||||
}
|
||||
assert_eq!(app.query.value, " SELECT café,界\n\n FROM data");
|
||||
app.handle_query_key(key('i'));
|
||||
for character in ['w', 'b', 'e', '0', '^', '$', 'g', 'G'] {
|
||||
app.handle_query_key(key(character));
|
||||
}
|
||||
assert_eq!(app.query.value, " SELECT café,界\n\n FROM datawbe0^$gG");
|
||||
app.leave_query_insert();
|
||||
app.handle_query_key(key('g'));
|
||||
app.handle_query_key(key('0'));
|
||||
app.handle_query_key(key('g'));
|
||||
assert_eq!(app.query_position(), (3, 1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_modes_preserve_literal_text_and_navigation() -> crate::Result<()> {
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
Reference in New Issue
Block a user