Skip to content

Commit

Permalink
feat: Highlight in prqlc (#4755)
Browse files Browse the repository at this point in the history
Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
  • Loading branch information
3 people authored Jul 21, 2024
1 parent 6207ec3 commit 577ae6f
Show file tree
Hide file tree
Showing 13 changed files with 348 additions and 51 deletions.
2 changes: 1 addition & 1 deletion prqlc/prqlc-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Macros for PRQL compilation at build time.
//!
//! ```
//! use prql_compiler_macros::prql_to_sql;
//! use prqlc_macros::prql_to_sql;
//!
//! let sql: &str = prql_to_sql!("from albums | select {title, artist_id}");
//! assert_eq!(sql, "SELECT title, artist_id FROM albums");
Expand Down
2 changes: 0 additions & 2 deletions prqlc/prqlc-parser/src/parser/pr/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ pub enum VarDefKind {
Main,
}

// The following code is tested by the tests_misc crate to match stmt.rs in prql_compiler.

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Stmt {
#[serde(flatten)]
Expand Down
163 changes: 163 additions & 0 deletions prqlc/prqlc/src/cli/highlight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use color_eyre::owo_colors::OwoColorize;
use prqlc::{
lr::{TokenKind, Tokens},
pr::Literal,
};

/// Highlight PRQL code printed to the terminal.
pub(crate) fn highlight(tokens: &Tokens) -> String {
let mut output = String::new();
let mut last = 0;

for token in &tokens.0 {
let diff = token.span.start - last;
last = token.span.end;
output.push_str(&" ".repeat(diff));
output.push_str(&highlight_token_kind(&token.kind));
}

output
}

fn highlight_token_kind(token: &TokenKind) -> String {
// LineWrap is recursive with TokenKind, so we needed to split this function
// out from the one above (otherwise would have it as a single func)
let mut output = String::new();
match &token {
TokenKind::NewLine => output.push('\n'),
TokenKind::Ident(ident) => {
if is_transform(ident) {
output.push_str(&ident.green().to_string())
} else {
output.push_str(&ident.to_string())
}
}
TokenKind::Keyword(keyword) => output.push_str(&keyword.blue().to_string()),
TokenKind::Literal(literal) => output.push_str(&match literal {
Literal::Null => literal.green().bold().to_string(),
Literal::Integer(_) => literal.green().to_string(),
Literal::Float(_) => literal.green().to_string(),
Literal::Boolean(_) => literal.green().bold().to_string(),
Literal::String(_) => literal.yellow().to_string(),
_ => literal.to_string(),
}),
TokenKind::Param(param) => output.push_str(&param.purple().to_string()),
TokenKind::Range {
bind_left: _,
bind_right: _,
} => output.push_str(".."),
TokenKind::Interpolation(_, _) => output.push_str(&format!("{}", token.yellow())),
TokenKind::Control(char) => output.push(*char),
TokenKind::ArrowThin
| TokenKind::ArrowFat
| TokenKind::Eq
| TokenKind::Ne
| TokenKind::Gte
| TokenKind::Lte
| TokenKind::RegexSearch => output.push_str(&format!("{}", token)),
TokenKind::And | TokenKind::Or => {
output.push_str(&format!("{}", token).purple().to_string())
}
TokenKind::Coalesce | TokenKind::DivInt | TokenKind::Pow | TokenKind::Annotate => {
output.push_str(&format!("{}", token))
}
TokenKind::Comment(comment) => output.push_str(
&format!("#{comment}")
.truecolor(95, 135, 135)
.italic()
.to_string(),
),
TokenKind::DocComment(comment) => output.push_str(
&format!("#!{comment}")
.truecolor(95, 135, 135)
.italic()
.to_string(),
),
TokenKind::LineWrap(inner_tokens) => {
output.push_str("\n\\");
for t in inner_tokens {
output.push_str(&highlight_token_kind(t));
}
}
TokenKind::Start => {}
}
output
}

fn is_transform(ident: &str) -> bool {
// TODO: Could we instead source these from the standard library?
// We could also use the semantic understanding from later compiler stages?
matches!(
ident,
"from"
| "derive"
| "select"
| "filter"
| "sort"
| "join"
| "take"
| "group"
| "aggregate"
| "window"
| "loop"
)
}

#[cfg(test)]
mod tests {
use std::process::Command;

use insta_cmd::assert_cmd_snapshot;
use insta_cmd::get_cargo_bin;

#[test]
fn highlight() {
// (Colors don't show because they're disabled; we could have a test
// that forces them to show?)
assert_cmd_snapshot!(prqlc_command().args(["experimental", "highlight"]).pass_stdin(r#"
from tracks
filter artist == "Bob Marley" # Each line transforms the previous result
aggregate { # `aggregate` reduces each column to a value
plays = sum plays,
longest = max length,
shortest = min length, # Trailing commas are allowed
}
"#), @r###"
success: true
exit_code: 0
----- stdout -----
from tracks
filter artist == "Bob Marley" # Each line transforms the previous result
aggregate { # `aggregate` reduces each column to a value
plays = sum plays,
longest = max length,
shortest = min length, # Trailing commas are allowed
}
----- stderr -----
"###);
}

// TODO: import from existing location, need to adjust visibility
fn prqlc_command() -> Command {
let mut cmd = Command::new(get_cargo_bin("prqlc"));
normalize_prqlc(&mut cmd);
cmd
}

fn normalize_prqlc(cmd: &mut Command) -> &mut Command {
cmd
// We set `CLICOLOR_FORCE` in CI to force color output, but we don't want `prqlc` to
// output color for our snapshot tests. And it seems to override the
// `--color=never` flag.
.env_remove("CLICOLOR_FORCE")
.env("NO_COLOR", "1")
.args(["--color=never"])
// We don't want the tests to be affected by the user's `RUST_BACKTRACE` setting.
.env_remove("RUST_BACKTRACE")
.env_remove("RUST_LOG")
}
}
27 changes: 23 additions & 4 deletions prqlc/prqlc/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
#![cfg(not(target_family = "wasm"))]

mod docs_generator;
mod jinja;
mod watch;

use std::collections::HashMap;
use std::env;
use std::fs::File;
Expand Down Expand Up @@ -32,9 +28,17 @@ use prqlc::ir::{pl, rq};
use prqlc::pr;
use prqlc::semantic;
use prqlc::semantic::reporting::FrameCollector;
use prqlc::utils::maybe_strip_colors;
use prqlc::{pl_to_prql, pl_to_rq_tree, prql_to_pl, prql_to_pl_tree, prql_to_tokens, rq_to_sql};
use prqlc::{Options, SourceTree, Target};

mod docs_generator;
mod highlight;
mod jinja;
#[cfg(test)]
mod test;
mod watch;

/// Entrypoint called by [`crate::main`]
pub fn main() -> color_eyre::eyre::Result<()> {
let mut cli = Cli::parse();
Expand Down Expand Up @@ -222,6 +226,10 @@ pub enum ExperimentalCommand {
/// Generate Markdown documentation
#[command(name = "doc")]
GenerateDocs(IoArgs),

/// Syntax highlight
#[command(name = "highlight")]
Highlight(IoArgs),
}

#[derive(clap::Args, Default, Debug, Clone)]
Expand Down Expand Up @@ -435,6 +443,15 @@ impl Command {

docs_generator::generate_markdown_docs(module_ref.stmts).into_bytes()
}
Command::Experimental(ExperimentalCommand::Highlight(_)) => {
let s = sources.sources.values().exactly_one().or_else(|_| {
// TODO: allow multiple sources
bail!("Currently `highlight` only works with a single source, but found multiple sources")
})?;
let tokens = prql_to_tokens(s)?;

maybe_strip_colors(&highlight::highlight(&tokens)).into_bytes()
}
Command::Compile {
signature_comment,
format,
Expand Down Expand Up @@ -483,6 +500,7 @@ impl Command {
io_args
}
Experimental(ExperimentalCommand::GenerateDocs(io_args)) => io_args,
Experimental(ExperimentalCommand::Highlight(io_args)) => io_args,
_ => unreachable!(),
};
let input = &mut io_args.input;
Expand Down Expand Up @@ -518,6 +536,7 @@ impl Command {
io_args.output.clone()
}
Experimental(ExperimentalCommand::GenerateDocs(io_args)) => io_args.output.clone(),
Experimental(ExperimentalCommand::Highlight(io_args)) => io_args.output.clone(),
_ => unreachable!(),
};
output.write_all(data)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
---
source: prqlc/prqlc/tests/integration/cli.rs
info:
program: prqlc
args:
Expand Down Expand Up @@ -81,18 +80,24 @@ complete -c prqlc -n "__fish_seen_subcommand_from debug; and __fish_seen_subcomm
complete -c prqlc -n "__fish_seen_subcommand_from debug; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from annotate; and not __fish_seen_subcommand_from lineage; and not __fish_seen_subcommand_from ast; and not __fish_seen_subcommand_from json-schema; and not __fish_seen_subcommand_from help" -f -a "ast" -d 'Print info about the AST data structure'
complete -c prqlc -n "__fish_seen_subcommand_from debug; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from annotate; and not __fish_seen_subcommand_from lineage; and not __fish_seen_subcommand_from ast; and not __fish_seen_subcommand_from json-schema; and not __fish_seen_subcommand_from help" -f -a "json-schema" -d 'Print JSON Schema'
complete -c prqlc -n "__fish_seen_subcommand_from debug; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from annotate; and not __fish_seen_subcommand_from lineage; and not __fish_seen_subcommand_from ast; and not __fish_seen_subcommand_from json-schema; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from help" -l color -d 'Controls when to use color' -r -f -a "{auto '',always '',never ''}"
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from help" -s v -l verbose -d 'Increase logging verbosity'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from help" -s q -l quiet -d 'Silences logging output'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from help" -s h -l help -d 'Print help (see more with \'--help\')'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from help" -f -a "doc" -d 'Generate Markdown documentation'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight; and not __fish_seen_subcommand_from help" -l color -d 'Controls when to use color' -r -f -a "{auto '',always '',never ''}"
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight; and not __fish_seen_subcommand_from help" -s v -l verbose -d 'Increase logging verbosity'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight; and not __fish_seen_subcommand_from help" -s q -l quiet -d 'Silences logging output'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight; and not __fish_seen_subcommand_from help" -s h -l help -d 'Print help (see more with \'--help\')'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight; and not __fish_seen_subcommand_from help" -f -a "doc" -d 'Generate Markdown documentation'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight; and not __fish_seen_subcommand_from help" -f -a "highlight" -d 'Syntax highlight'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from doc" -l color -d 'Controls when to use color' -r -f -a "{auto '',always '',never ''}"
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from doc" -s v -l verbose -d 'Increase logging verbosity'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from doc" -s q -l quiet -d 'Silences logging output'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from doc" -s h -l help -d 'Print help (see more with \'--help\')'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from help" -f -a "doc" -d 'Generate Markdown documentation'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from highlight" -l color -d 'Controls when to use color' -r -f -a "{auto '',always '',never ''}"
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from highlight" -s v -l verbose -d 'Increase logging verbosity'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from highlight" -s q -l quiet -d 'Silences logging output'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from highlight" -s h -l help -d 'Print help (see more with \'--help\')'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight; and not __fish_seen_subcommand_from help" -f -a "doc" -d 'Generate Markdown documentation'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight; and not __fish_seen_subcommand_from help" -f -a "highlight" -d 'Syntax highlight'
complete -c prqlc -n "__fish_seen_subcommand_from experimental; and __fish_seen_subcommand_from help; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c prqlc -n "__fish_seen_subcommand_from compile" -s t -l target -d 'Target to compile to' -r
complete -c prqlc -n "__fish_seen_subcommand_from compile" -l debug-log -d 'File path into which to write the debug log to' -r -F
complete -c prqlc -n "__fish_seen_subcommand_from compile" -l color -d 'Controls when to use color' -r -f -a "{auto '',always '',never ''}"
Expand Down Expand Up @@ -130,6 +135,7 @@ complete -c prqlc -n "__fish_seen_subcommand_from help; and __fish_seen_subcomma
complete -c prqlc -n "__fish_seen_subcommand_from help; and __fish_seen_subcommand_from debug; and not __fish_seen_subcommand_from annotate; and not __fish_seen_subcommand_from lineage; and not __fish_seen_subcommand_from ast; and not __fish_seen_subcommand_from json-schema" -f -a "lineage" -d 'Output column-level lineage graph'
complete -c prqlc -n "__fish_seen_subcommand_from help; and __fish_seen_subcommand_from debug; and not __fish_seen_subcommand_from annotate; and not __fish_seen_subcommand_from lineage; and not __fish_seen_subcommand_from ast; and not __fish_seen_subcommand_from json-schema" -f -a "ast" -d 'Print info about the AST data structure'
complete -c prqlc -n "__fish_seen_subcommand_from help; and __fish_seen_subcommand_from debug; and not __fish_seen_subcommand_from annotate; and not __fish_seen_subcommand_from lineage; and not __fish_seen_subcommand_from ast; and not __fish_seen_subcommand_from json-schema" -f -a "json-schema" -d 'Print JSON Schema'
complete -c prqlc -n "__fish_seen_subcommand_from help; and __fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc" -f -a "doc" -d 'Generate Markdown documentation'
complete -c prqlc -n "__fish_seen_subcommand_from help; and __fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight" -f -a "doc" -d 'Generate Markdown documentation'
complete -c prqlc -n "__fish_seen_subcommand_from help; and __fish_seen_subcommand_from experimental; and not __fish_seen_subcommand_from doc; and not __fish_seen_subcommand_from highlight" -f -a "highlight" -d 'Syntax highlight'

----- stderr -----
Loading

0 comments on commit 577ae6f

Please sign in to comment.