Skip to content

Commit f39841b

Browse files
authored
Add syntax highlight to datafusion-cli (#8918)
* use tokenizer * clippy * license + lighter color * unit tests
1 parent 180cbfb commit f39841b

File tree

3 files changed

+143
-1
lines changed

3 files changed

+143
-1
lines changed

datafusion-cli/src/helper.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
//! Helper that helps with interactive editing, including multi-line parsing and validation,
1919
//! and auto-completion for file name during creating external table.
2020
21+
use std::borrow::Cow;
22+
2123
use datafusion::common::sql_err;
2224
use datafusion::error::DataFusionError;
2325
use datafusion::sql::parser::{DFParser, Statement};
@@ -36,16 +38,20 @@ use rustyline::Context;
3638
use rustyline::Helper;
3739
use rustyline::Result;
3840

41+
use crate::highlighter::SyntaxHighlighter;
42+
3943
pub struct CliHelper {
4044
completer: FilenameCompleter,
4145
dialect: String,
46+
highlighter: SyntaxHighlighter,
4247
}
4348

4449
impl CliHelper {
4550
pub fn new(dialect: &str) -> Self {
4651
Self {
4752
completer: FilenameCompleter::new(),
4853
dialect: dialect.into(),
54+
highlighter: SyntaxHighlighter::new(dialect),
4955
}
5056
}
5157

@@ -100,7 +106,15 @@ impl Default for CliHelper {
100106
}
101107
}
102108

103-
impl Highlighter for CliHelper {}
109+
impl Highlighter for CliHelper {
110+
fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
111+
self.highlighter.highlight(line, pos)
112+
}
113+
114+
fn highlight_char(&self, line: &str, pos: usize) -> bool {
115+
self.highlighter.highlight_char(line, pos)
116+
}
117+
}
104118

105119
impl Hinter for CliHelper {
106120
type Hint = String;

datafusion-cli/src/highlighter.rs

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//! The syntax highlighter.
19+
20+
use std::{
21+
borrow::Cow::{self, Borrowed},
22+
fmt::Display,
23+
};
24+
25+
use datafusion::sql::sqlparser::{
26+
dialect::{dialect_from_str, Dialect, GenericDialect},
27+
keywords::Keyword,
28+
tokenizer::{Token, Tokenizer},
29+
};
30+
use rustyline::highlight::Highlighter;
31+
32+
/// The syntax highlighter.
33+
pub struct SyntaxHighlighter {
34+
dialect: Box<dyn Dialect>,
35+
}
36+
37+
impl SyntaxHighlighter {
38+
pub fn new(dialect: &str) -> Self {
39+
let dialect = match dialect_from_str(dialect) {
40+
Some(dialect) => dialect,
41+
None => Box::new(GenericDialect {}),
42+
};
43+
Self { dialect }
44+
}
45+
}
46+
47+
impl Highlighter for SyntaxHighlighter {
48+
fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> {
49+
let mut out_line = String::new();
50+
51+
// `with_unescape(false)` since we want to rebuild the original string.
52+
let mut tokenizer =
53+
Tokenizer::new(self.dialect.as_ref(), line).with_unescape(false);
54+
let tokens = tokenizer.tokenize();
55+
match tokens {
56+
Ok(tokens) => {
57+
for token in tokens.iter() {
58+
match token {
59+
Token::Word(w) if w.keyword != Keyword::NoKeyword => {
60+
out_line.push_str(&Color::red(token));
61+
}
62+
Token::SingleQuotedString(_) => {
63+
out_line.push_str(&Color::green(token));
64+
}
65+
other => out_line.push_str(&format!("{other}")),
66+
}
67+
}
68+
out_line.into()
69+
}
70+
Err(_) => Borrowed(line),
71+
}
72+
}
73+
74+
fn highlight_char(&self, line: &str, _: usize) -> bool {
75+
!line.is_empty()
76+
}
77+
}
78+
79+
/// Convenient utility to return strings with [ANSI color](https://gist.github.com/JBlond/2fea43a3049b38287e5e9cefc87b2124).
80+
struct Color {}
81+
82+
impl Color {
83+
fn green(s: impl Display) -> String {
84+
format!("\x1b[92m{s}\x1b[0m")
85+
}
86+
87+
fn red(s: impl Display) -> String {
88+
format!("\x1b[91m{s}\x1b[0m")
89+
}
90+
}
91+
92+
#[cfg(test)]
93+
mod tests {
94+
use super::SyntaxHighlighter;
95+
use rustyline::highlight::Highlighter;
96+
97+
#[test]
98+
fn highlighter_valid() {
99+
let s = "SElect col_a from tab_1;";
100+
let highlighter = SyntaxHighlighter::new("generic");
101+
let out = highlighter.highlight(s, s.len());
102+
assert_eq!(
103+
"\u{1b}[91mSElect\u{1b}[0m col_a \u{1b}[91mfrom\u{1b}[0m tab_1;",
104+
out
105+
);
106+
}
107+
108+
#[test]
109+
fn highlighter_valid_with_new_line() {
110+
let s = "SElect col_a from tab_1\n WHERE col_b = 'なにか';";
111+
let highlighter = SyntaxHighlighter::new("generic");
112+
let out = highlighter.highlight(s, s.len());
113+
assert_eq!(
114+
"\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;",
115+
out
116+
);
117+
}
118+
119+
#[test]
120+
fn highlighter_invalid() {
121+
let s = "SElect col_a from tab_1 WHERE col_b = ';";
122+
let highlighter = SyntaxHighlighter::new("generic");
123+
let out = highlighter.highlight(s, s.len());
124+
assert_eq!("SElect col_a from tab_1 WHERE col_b = ';", out);
125+
}
126+
}

datafusion-cli/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ pub mod helper;
2626
pub mod object_storage;
2727
pub mod print_format;
2828
pub mod print_options;
29+
30+
mod highlighter;

0 commit comments

Comments
 (0)