datafusion_cli/
highlighter.rs1use std::{
21 borrow::Cow::{self, Borrowed},
22 fmt::Display,
23};
24
25use datafusion::sql::sqlparser::{
26 dialect::{Dialect, GenericDialect, dialect_from_str},
27 keywords::Keyword,
28 tokenizer::{Token, Tokenizer},
29};
30use datafusion_common::config;
31use rustyline::highlight::{CmdKind, Highlighter};
32
33#[derive(Debug)]
35pub struct SyntaxHighlighter {
36 dialect: Box<dyn Dialect>,
37}
38
39impl SyntaxHighlighter {
40 pub fn new(dialect: &config::Dialect) -> Self {
41 let dialect =
42 dialect_from_str(dialect).unwrap_or_else(|| Box::new(GenericDialect {}));
43 Self { dialect }
44 }
45}
46
47pub struct NoSyntaxHighlighter {}
48
49impl Highlighter for NoSyntaxHighlighter {}
50
51impl Highlighter for SyntaxHighlighter {
52 fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> {
53 let mut out_line = String::new();
54
55 let mut tokenizer =
57 Tokenizer::new(self.dialect.as_ref(), line).with_unescape(false);
58 let tokens = tokenizer.tokenize();
59 match tokens {
60 Ok(tokens) => {
61 for token in tokens.iter() {
62 match token {
63 Token::Word(w) if w.keyword != Keyword::NoKeyword => {
64 out_line.push_str(&Color::red(token));
65 }
66 Token::SingleQuotedString(_) => {
67 out_line.push_str(&Color::green(token));
68 }
69 other => out_line.push_str(&format!("{other}")),
70 }
71 }
72 out_line.into()
73 }
74 Err(_) => Borrowed(line),
75 }
76 }
77
78 fn highlight_char(&self, line: &str, _pos: usize, _cmd: CmdKind) -> bool {
79 !line.is_empty()
80 }
81}
82
83pub(crate) struct Color {}
85
86impl Color {
87 pub(crate) fn green(s: impl Display) -> String {
88 format!("\x1b[92m{s}\x1b[0m")
89 }
90
91 pub(crate) fn red(s: impl Display) -> String {
92 format!("\x1b[91m{s}\x1b[0m")
93 }
94
95 pub(crate) fn gray(s: impl Display) -> String {
96 format!("\x1b[90m{s}\x1b[0m")
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::SyntaxHighlighter;
103 use super::config::Dialect;
104 use rustyline::highlight::Highlighter;
105
106 #[test]
107 fn highlighter_valid() {
108 let s = "SElect col_a from tab_1;";
109 let highlighter = SyntaxHighlighter::new(&Dialect::Generic);
110 let out = highlighter.highlight(s, s.len());
111 assert_eq!(
112 "\u{1b}[91mSElect\u{1b}[0m col_a \u{1b}[91mfrom\u{1b}[0m tab_1;",
113 out
114 );
115 }
116
117 #[test]
118 fn highlighter_valid_with_new_line() {
119 let s = "SElect col_a from tab_1\n WHERE col_b = 'なにか';";
120 let highlighter = SyntaxHighlighter::new(&Dialect::Generic);
121 let out = highlighter.highlight(s, s.len());
122 assert_eq!(
123 "\u{1b}[91mSElect\u{1b}[0m col_a \u{1b}[91mfrom\u{1b}[0m tab_1\n \u{1b}[91mWHERE\u{1b}[0m col_b = \u{1b}[92m'なにか'\u{1b}[0m;",
124 out
125 );
126 }
127
128 #[test]
129 fn highlighter_invalid() {
130 let s = "SElect col_a from tab_1 WHERE col_b = ';";
131 let highlighter = SyntaxHighlighter::new(&Dialect::Generic);
132 let out = highlighter.highlight(s, s.len());
133 assert_eq!("SElect col_a from tab_1 WHERE col_b = ';", out);
134 }
135}