feat: add SQL syntax highlighting in query editor

This commit is contained in:
Valdemar Lorenzen
2026-10-19 14:16:04 +02:00
parent 49a3dbddcf
commit dbd4c05cf4
7 changed files with 285 additions and 11 deletions
+45 -1
View File
@@ -11,7 +11,7 @@ use crate::{
Result,
connection::Input,
db::{Column, Database, PAGE_SIZE, Page, QueryResult, Relation},
error_text,
error_text, sql,
};
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -33,6 +33,7 @@ pub struct App {
column_offset: usize,
show_schema: bool,
query: Input,
query_highlights: sql::Highlights,
query_result: Option<QueryResult>,
error: Option<String>,
}
@@ -56,6 +57,7 @@ impl App {
column_offset: 0,
show_schema: false,
query: Input::default(),
query_highlights: Vec::new(),
query_result: None,
error: None,
};
@@ -104,16 +106,27 @@ impl App {
}
pub(crate) fn handle_query_key(&mut self, key: KeyEvent) {
let previous_len = self.query.value.len();
match key.code {
KeyCode::F(5) => self.execute_query(),
KeyCode::Enter if key.modifiers.contains(KeyModifiers::CONTROL) => self.execute_query(),
KeyCode::Enter => self.query.paste_multiline("\n"),
_ => self.query.edit(key),
}
self.highlight_query(previous_len);
}
pub(crate) fn paste_query(&mut self, text: &str) {
let previous_len = self.query.value.len();
self.query.paste_multiline(text);
self.highlight_query(previous_len);
}
fn highlight_query(&mut self, previous_len: usize) {
// Editing inserts or removes bytes; cursor movement does not invalidate syntax.
if self.query.value.len() != previous_len {
self.query_highlights = sql::highlight(&self.query.value, self.db.name() == "SQLite");
}
}
fn execute_query(&mut self) {
@@ -232,6 +245,7 @@ impl App {
" Query · F5 execute ",
false,
focus == Focus::Query,
Some(sql::text(&self.query.value, &self.query_highlights)),
);
let title = if self.query_result.is_some() {
" Results "
@@ -432,6 +446,36 @@ mod tests {
assert_eq!(moved_index(Some(1), 3, isize::MAX), Some(2));
}
#[test]
fn query_colors_survive_editing_and_scrolling() -> crate::Result<()> {
use super::{App, Focus};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{Terminal, backend::TestBackend, style::Color};
let file = tempfile::NamedTempFile::new()?;
let mut app = App::new(crate::db::Database::open_sqlite(file.path())?)?;
let mut terminal = Terminal::new(TestBackend::new(60, 14))?;
app.paste_query("SELECT '界', 42;\n-- comment");
terminal.draw(|frame| app.draw(frame, frame.area(), Focus::Query))?;
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(1, 2)].fg, Color::Cyan);
assert_eq!(buffer[(8, 2)].fg, Color::Green);
assert_eq!(buffer[(1, 3)].fg, Color::DarkGray);
app.handle_query_key(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
app.handle_query_key(KeyEvent::new(KeyCode::Char('\''), KeyModifiers::NONE));
app.paste_query("unfinished\nstring");
terminal.draw(|frame| app.draw(frame, frame.area(), Focus::Query))?;
assert_eq!(terminal.backend().buffer()[(1, 3)].fg, Color::Green);
app.handle_query_key(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
app.paste_query(&format!("{}--{}", "-- line\n".repeat(12), "x".repeat(90)));
terminal.draw(|frame| app.draw(frame, frame.area(), Focus::Query))?;
let cursor = terminal.get_cursor_position()?;
let buffer = terminal.backend().buffer();
assert_eq!(cursor.x, 58);
assert_eq!(buffer[(cursor.x - 1, cursor.y)].symbol(), "x");
assert_eq!(buffer[(1, cursor.y)].fg, Color::DarkGray);
Ok(())
}
#[test]
fn queries_replace_rows_and_preserve_results_on_error() -> crate::Result<()> {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};