Merge 'Syntax highlighting and hinting' from Pedro Muniz

Start of syntax highlighting and hinting. Still need to figure out how
to sublime-syntax works to produce good highlights.
Edit:
Personally, I believe there are more interesting syntax highlighting
possibilities with `reedline` crate, but currently, we cannot use it as
the our DB `Connection` would have to be `Send`. This PR is an
introduction and quality of life changes for the users of the CLI and
for us developers, as we now won't have to look at black and white text
only. I want to have a config file to personalize the color pallets,
that will be made in a following PR.

Closes #1101
This commit is contained in:
Pekka Enberg
2025-03-21 18:17:47 +02:00
7 changed files with 2177 additions and 208 deletions

574
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
# Copyright 2023 the Limbo authors. All rights reserved. MIT license.
[package]
name = "limbo_cli"
version.workspace = true
authors.workspace = true
default-run = "limbo"
description = "The Limbo interactive SQL shell"
edition.workspace = true
license.workspace = true
name = "limbo_cli"
repository.workspace = true
description = "The Limbo interactive SQL shell"
version.workspace = true
[package.metadata.dist]
dist = true
@@ -36,7 +36,14 @@ miette = { version = "7.4.0", features = ["fancy"] }
cfg-if = "1.0.0"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing = "0.1.41"
syntect = "5.2.0"
nu-ansi-term = "0.50.1"
[features]
default = ["io_uring"]
io_uring = ["limbo_core/io_uring"]
[build-dependencies]
syntect = "5.2.0"

1669
cli/SQL.sublime-syntax Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ use crate::{
input::{get_io, get_writer, DbLocation, OutputMode, Settings, HELP_MSG},
opcodes_dictionary::OPCODE_DESCRIPTIONS,
};
use comfy_table::{Attribute, Cell, CellAlignment, ContentArrangement, Row, Table};
use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Row, Table};
use limbo_core::{Database, LimboError, OwnedValue, Statement, StepResult};
use clap::{Parser, ValueEnum};
@@ -200,6 +200,8 @@ macro_rules! query_internal {
}};
}
static COLORS: &[Color] = &[Color::DarkRed, Color::DarkGreen, Color::DarkBlue];
impl<'a> Limbo<'a> {
pub fn new(rl: &'a mut rustyline::Editor<LimboHelper, DefaultHistory>) -> anyhow::Result<Self> {
let opts = Opts::parse();
@@ -249,6 +251,7 @@ impl<'a> Limbo<'a> {
opts: Settings::from(&opts),
rl,
};
if opts.sql.is_some() {
app.handle_first_input(opts.sql.as_ref().unwrap());
}
@@ -752,7 +755,9 @@ impl<'a> Limbo<'a> {
let header = (0..rows.num_columns())
.map(|i| {
let name = rows.get_column_name(i);
Cell::new(name).add_attribute(Attribute::Bold)
Cell::new(name)
.add_attribute(Attribute::Bold)
.fg(Color::AnsiValue(49)) // Green color for headers
})
.collect::<Vec<_>>();
table.set_header(header);
@@ -763,7 +768,7 @@ impl<'a> Limbo<'a> {
let record = rows.row().unwrap();
let mut row = Row::new();
row.max_height(1);
for value in record.get_values() {
for (idx, value) in record.get_values().iter().enumerate() {
let (content, alignment) = match value {
OwnedValue::Null => {
(self.opts.null_value.clone(), CellAlignment::Left)
@@ -781,7 +786,11 @@ impl<'a> Limbo<'a> {
),
_ => unreachable!(),
};
row.add_cell(Cell::new(content).set_alignment(alignment));
row.add_cell(
Cell::new(content)
.set_alignment(alignment)
.fg(COLORS[idx % COLORS.len()]),
);
}
table.add_row(row);
}

26
cli/build.rs Normal file
View File

@@ -0,0 +1,26 @@
//! Build.rs script to generate a binary syntax set for syntect
//! based on the SQL.sublime-syntax file.
use std::env;
use std::path::Path;
use syntect::dumps::dump_to_uncompressed_file;
use syntect::parsing::SyntaxDefinition;
use syntect::parsing::SyntaxSet;
fn main() {
println!("cargo::rerun-if-changed=SQL.sublime-syntax");
println!("cargo::rerun-if-changed=build.rs");
let out_dir = env::var_os("OUT_DIR").unwrap();
let syntax =
SyntaxDefinition::load_from_str(include_str!("./SQL.sublime-syntax"), false, None).unwrap();
let mut ps = SyntaxSet::new().into_builder();
ps.add(syntax);
let ps = ps.build();
dump_to_uncompressed_file(
&ps,
Path::new(&out_dir).join("SQL_syntax_set_dump.packdump"),
)
.unwrap();
}

View File

@@ -2,9 +2,16 @@ use std::rc::Rc;
use std::sync::Arc;
use limbo_core::{Connection, StepResult};
use nu_ansi_term::{Color, Style};
use rustyline::completion::{extract_word, Completer, Pair};
use rustyline::highlight::Highlighter;
use rustyline::hint::HistoryHinter;
use rustyline::{Completer, Helper, Hinter, Validator};
use syntect::dumps::from_uncompressed_data;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::parsing::{Scope, SyntaxSet};
use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings};
macro_rules! try_result {
($expr:expr, $err:expr) => {
@@ -19,17 +26,92 @@ macro_rules! try_result {
pub struct LimboHelper {
#[rustyline(Completer)]
completer: SqlCompleter,
syntax_set: SyntaxSet,
theme_set: ThemeSet,
#[rustyline(Hinter)]
hinter: HistoryHinter,
}
impl LimboHelper {
pub fn new(conn: Rc<Connection>, io: Arc<dyn limbo_core::IO>) -> Self {
// Load only predefined syntax
let ps = from_uncompressed_data(include_bytes!(concat!(
env!("OUT_DIR"),
"/SQL_syntax_set_dump.packdump"
)))
.unwrap();
let ts = ThemeSet::load_defaults();
LimboHelper {
completer: SqlCompleter::new(conn, io),
syntax_set: ps,
theme_set: ts,
hinter: HistoryHinter::new(),
}
}
}
impl Highlighter for LimboHelper {}
impl Highlighter for LimboHelper {
fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> {
let _ = pos;
// TODO use lifetimes to store highlight lines
let syntax = self
.syntax_set
.find_syntax_by_scope(Scope::new("source.sql").unwrap())
.unwrap();
let mut h = HighlightLines::new(syntax, &self.theme_set.themes["base16-ocean.dark"]);
let ranges = {
let mut ret_ranges = Vec::new();
for new_line in LinesWithEndings::from(line) {
let ranges: Vec<(syntect::highlighting::Style, &str)> =
h.highlight_line(new_line, &self.syntax_set).unwrap();
ret_ranges.extend(ranges);
}
ret_ranges
};
let mut ret_line = as_24_bit_terminal_escaped(&ranges[..], false);
// Push this escape sequence to reset color modes at the end of the string
ret_line.push_str("\x1b[0m");
std::borrow::Cow::Owned(ret_line)
}
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
default: bool,
) -> std::borrow::Cow<'b, str> {
let _ = default;
// Make prompt bold
let style = Style::new().bold().fg(Color::Green);
let styled_str = style.paint(prompt);
std::borrow::Cow::Owned(styled_str.to_string())
}
fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
let style = Style::new()
.bold()
.dimmed()
.underline()
.fg(Color::Fixed(246));
let styled_str = style.paint(hint);
std::borrow::Cow::Owned(styled_str.to_string()) // Bold dim grey underline
}
fn highlight_candidate<'c>(
&self,
candidate: &'c str,
completion: rustyline::CompletionType,
) -> std::borrow::Cow<'c, str> {
let _ = completion;
let style = Style::new().fg(Color::Fixed(69));
let styled_str = style.paint(candidate);
std::borrow::Cow::Owned(styled_str.to_string())
}
fn highlight_char(&self, line: &str, pos: usize, kind: rustyline::highlight::CmdKind) -> bool {
let _ = (line, pos);
!matches!(kind, rustyline::highlight::CmdKind::MoveCursor)
}
}
pub struct SqlCompleter {
conn: Rc<Connection>,

View File

@@ -23,7 +23,7 @@ tempfile = "3.0.7"
env_logger = "0.10.1"
regex = "1.11.1"
regex-syntax = { version = "0.8.5", default-features = false, features = ["unicode"] }
anarchist-readable-name-generator-lib = "0.1.2"
anarchist-readable-name-generator-lib = "=0.1.2"
clap = { version = "4.5", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }