use calcit::cli_args::*;
use colored::Colorize;
macro_rules! echo_items {
($tokens:expr $(,)?) => {};
($tokens:expr, pos $name:literal => $value:expr $(, $($rest:tt)*)?) => {{
push_positional($tokens, $name, $value);
echo_items!($tokens $(, $($rest)*)?);
}};
($tokens:expr, switch $name:literal => $value:expr $(, $($rest:tt)*)?) => {{
push_switch($tokens, $name, $value);
echo_items!($tokens $(, $($rest)*)?);
}};
($tokens:expr, value $name:literal => $value:expr ; default $default:expr $(, $($rest:tt)*)?) => {{
let value = ($value).to_string();
push_value($tokens, $name, &value, Some($default));
echo_items!($tokens $(, $($rest)*)?);
}};
($tokens:expr, value $name:literal => $value:expr $(, $($rest:tt)*)?) => {{
let value = ($value).to_string();
push_value($tokens, $name, &value, None);
echo_items!($tokens $(, $($rest)*)?);
}};
($tokens:expr, opt $name:literal => $value:expr ; default $default:expr $(, $($rest:tt)*)?) => {{
push_optional($tokens, $name, $value, $default);
echo_items!($tokens $(, $($rest)*)?);
}};
($tokens:expr, opt_owned $name:literal => $value:expr ; default $default:expr $(, $($rest:tt)*)?) => {{
push_optional_owned($tokens, $name, $value, $default);
echo_items!($tokens $(, $($rest)*)?);
}};
($tokens:expr, list $name:literal => $value:expr $(, $($rest:tt)*)?) => {{
push_list($tokens, $name, $value);
echo_items!($tokens $(, $($rest)*)?);
}};
($tokens:expr, code_input $opts:expr $(, $($rest:tt)*)?) => {{
let opts = &$opts;
push_code_input(
$tokens,
&CodeInputParts::new(
opts.file.as_deref(),
opts.code.as_deref(),
opts.json.as_deref(),
opts.json_input,
),
);
echo_items!($tokens $(, $($rest)*)?);
}};
}
pub fn should_echo_command(cli_args: &ToplevelCalcit) -> bool {
matches!(
cli_args.subcommand,
Some(CalcitCommand::Query(_))
| Some(CalcitCommand::Docs(_))
| Some(CalcitCommand::Libs(_))
| Some(CalcitCommand::Edit(_))
| Some(CalcitCommand::Tree(_))
| Some(CalcitCommand::Config(_))
| Some(CalcitCommand::Analyze(_))
| Some(CalcitCommand::Cirru(_))
)
}
pub fn print_command_echo(cli_args: &ToplevelCalcit) {
let Some(command) = render_command_echo(cli_args) else {
return;
};
eprintln!("{}", format!("Command: {command}").blue().bold());
if let Some(explanation) = render_command_explanation(cli_args) {
eprintln!("{}", format!("# Explanation: {explanation}").blue().bold());
}
}
fn render_command_echo(cli_args: &ToplevelCalcit) -> Option<String> {
let subcommand = cli_args.subcommand.as_ref()?;
let mut tokens = vec![match subcommand {
CalcitCommand::Query(cmd) => format!("cr query {}", query_name(&cmd.subcommand)),
CalcitCommand::Docs(cmd) => match &cmd.subcommand {
DocsSubcommand::RemoteLibs(opts) => match opts.subcommand.as_ref() {
Some(subcommand) => format!("cr docs remote-libs {}", libs_name(subcommand)),
None => "cr docs remote-libs".to_string(),
},
other => format!("cr docs {}", docs_name(other)),
},
CalcitCommand::Libs(cmd) => format!("cr libs {}", libs_name(cmd.subcommand.as_ref()?)),
CalcitCommand::Edit(cmd) => format!("cr edit {}", edit_name(&cmd.subcommand)),
CalcitCommand::Tree(cmd) => format!("cr tree {}", tree_name(&cmd.subcommand)),
CalcitCommand::Config(cmd) => format!("cr config {}", config_name(&cmd.subcommand)),
CalcitCommand::Analyze(cmd) => format!("cr analyze {}", analyze_name(&cmd.subcommand)),
CalcitCommand::Cirru(cmd) => format!("cr cirru {}", cirru_name(&cmd.subcommand)),
_ => return None,
}];
match subcommand {
CalcitCommand::Query(cmd) => push_query(&mut tokens, cmd),
CalcitCommand::Docs(cmd) => push_docs(&mut tokens, cmd),
CalcitCommand::Libs(cmd) => push_libs(&mut tokens, cmd.subcommand.as_ref()?),
CalcitCommand::Edit(cmd) => push_edit(&mut tokens, cmd),
CalcitCommand::Tree(cmd) => push_tree(&mut tokens, cmd),
CalcitCommand::Config(cmd) => push_config(&mut tokens, cmd),
CalcitCommand::Analyze(cmd) => push_analyze(&mut tokens, cmd),
CalcitCommand::Cirru(cmd) => push_cirru(&mut tokens, cmd),
_ => return None,
}
Some(tokens.join(" "))
}
fn render_command_explanation(cli_args: &ToplevelCalcit) -> Option<String> {
let subcommand = cli_args.subcommand.as_ref()?;
match subcommand {
CalcitCommand::Query(cmd) => render_query_explanation(cmd),
CalcitCommand::Edit(cmd) => render_edit_explanation(cmd),
CalcitCommand::Tree(cmd) => render_tree_explanation(cmd),
CalcitCommand::Docs(cmd) => render_docs_explanation(cmd),
CalcitCommand::Analyze(cmd) => render_analyze_explanation(cmd),
CalcitCommand::Cirru(cmd) => render_cirru_explanation(cmd),
_ => None,
}
}
fn describe_code_input(parts: &CodeInputParts<'_>) -> String {
let mut desc = String::new();
if parts.code.is_some() {
if parts.json_input {
desc.push_str("--code value is a **JSON-encoded AST** (with --json-input). ");
} else {
desc.push_str("--code is a **Cirru expression snippet (wrapped in `(quote ...)`)**. ");
}
}
if let Some(_file) = parts.file {
desc.push_str("--file provides Cirru source from a file. ");
}
if let Some(json) = parts.json {
desc.push_str(&format!("--json provides **JSON-encoded AST**: `{json}`. "));
}
desc
}
fn render_query_explanation(cmd: &QueryCommand) -> Option<String> {
Some(match &cmd.subcommand {
QuerySubcommand::Search(opts) => {
let mut desc = format!("searches for `{}` in AST leaf nodes", opts.pattern);
if let Some(filter) = &opts.filter {
desc.push_str(&format!(", filtered by definitions matching `{filter}`"));
}
if opts.exact {
desc.push_str(" (exact match)");
} else if !opts.regex {
desc.push_str(" (case-insensitive contains, default)");
}
if opts.regex {
desc.push_str(" (regex match)");
}
if opts.max_depth > 0 {
desc.push_str(&format!(", max tree depth={}", opts.max_depth));
}
desc
}
QuerySubcommand::SearchExpr(opts) => {
let mut desc = format!("searches for Cirru expression `{}` in definitions", opts.pattern);
if let Some(filter) = &opts.filter {
desc.push_str(&format!(", filtered by `{filter}`"));
}
if opts.json {
desc.push_str(" (JSON output)");
}
desc
}
QuerySubcommand::Find(opts) => {
let mut desc = format!("finds all references to symbol `{}`", opts.symbol);
if opts.deps {
desc.push_str(" (including dependencies)");
}
if opts.exact {
desc.push_str(" (exact match)");
}
desc
}
QuerySubcommand::Def(opts) => {
let mut desc = format!("displays definition `{}` (namespace/definition format)", opts.target);
if opts.json {
desc.push_str(" (JSON output)");
}
if opts.raw {
desc.push_str(" (raw, no chunking)");
}
desc
}
QuerySubcommand::Peek(opts) => format!("previews definition `{}`", opts.target),
QuerySubcommand::Examples(opts) => format!("shows usage examples for `{}`", opts.target),
QuerySubcommand::Schema(opts) => format!("shows type schema for `{}`", opts.target),
QuerySubcommand::Usages(opts) => {
let mut desc = format!("finds all usages of `{}` in codebase", opts.target);
if opts.deps {
desc.push_str(" (including dependencies)");
}
desc
}
QuerySubcommand::Ns(opts) => {
let mut desc = "lists all namespaces".to_string();
if let Some(ns) = &opts.namespace {
desc.push_str(&format!(" matching `{ns}`"));
}
if opts.deps {
desc.push_str(" (including dependency namespaces)");
}
desc
}
QuerySubcommand::Defs(opts) => {
let mut desc = format!("lists all definitions in namespace `{}`", opts.namespace);
if let Some(tag) = &opts.tag {
desc.push_str(&format!(" filtered by tag `{tag}`"));
}
desc
}
QuerySubcommand::Pkg(_) => "shows package metadata".to_string(),
QuerySubcommand::Config(_) => "shows project configuration".to_string(),
QuerySubcommand::Error(_) => "shows recent build errors".to_string(),
QuerySubcommand::Modules(_) => "lists loaded modules".to_string(),
QuerySubcommand::HostProcs(opts) => {
let mut desc = "lists host procedures (FFI)".to_string();
if let Some(tag) = &opts.tag {
desc.push_str(&format!(" filtered by tag `{tag}`"));
}
desc
}
})
}
fn render_edit_explanation(cmd: &EditCommand) -> Option<String> {
Some(match &cmd.subcommand {
EditSubcommand::Format(_) => "rewrites snapshot file in canonical format".to_string(),
EditSubcommand::Def(opts) => {
let desc = format!("adds/updates definition `{}`", opts.target);
format!(
"{}; {}",
desc,
describe_code_input(&CodeInputParts::new(
opts.file.as_deref(),
opts.code.as_deref(),
opts.json.as_deref(),
opts.json_input,
))
)
}
EditSubcommand::MvDef(opts) => format!("moves/renames definition from `{}` to `{}`", opts.source, opts.target),
EditSubcommand::RmDef(opts) => format!("deletes definition `{}`", opts.target),
EditSubcommand::Doc(opts) => format!("updates documentation for `{}`", opts.target),
EditSubcommand::Schema(opts) => {
let mut desc = format!("updates type schema for `{}`", opts.target);
if opts.clear {
desc.push_str(" (clears existing schema)");
}
desc
}
EditSubcommand::Examples(opts) => {
let mut desc = format!("sets usage examples for `{}`", opts.target);
if opts.clear {
desc.push_str(" (clears existing examples)");
}
desc
}
EditSubcommand::AddExample(opts) => format!(
"adds example to `{}`{}",
opts.target,
opts.at.as_ref().map_or(String::new(), |at| format!(" at position `{at}`"))
),
EditSubcommand::RmExample(opts) => format!("removes example at index {} from `{}`", opts.index, opts.target),
EditSubcommand::Tags(opts) => {
let mut desc = format!("views/updates tags for `{}`", opts.target);
if let Some(tags) = &opts.tags {
desc.push_str(&format!(" to `{tags}`"));
}
desc
}
EditSubcommand::AddNs(opts) => {
let desc = format!("adds namespace `{}`", opts.namespace);
format!(
"{}; {}",
desc,
describe_code_input(&CodeInputParts::new(
opts.file.as_deref(),
opts.code.as_deref(),
opts.json.as_deref(),
opts.json_input,
))
)
}
EditSubcommand::RmNs(opts) => format!("deletes namespace `{}`", opts.namespace),
EditSubcommand::Imports(opts) => {
let desc = format!("replaces all imports in namespace `{}`", opts.namespace);
format!(
"{}; {}",
desc,
describe_code_input(&CodeInputParts::new(
opts.file.as_deref(),
opts.code.as_deref(),
opts.json.as_deref(),
opts.json_input,
))
)
}
EditSubcommand::AddImport(opts) => {
let desc = format!("adds import rule to namespace `{}`", opts.namespace);
format!(
"{}; {} {}",
desc,
describe_code_input(&CodeInputParts::new(
opts.file.as_deref(),
opts.code.as_deref(),
opts.json.as_deref(),
opts.json_input,
)),
if opts.overwrite { "(overwrites existing)" } else { "" }
)
}
EditSubcommand::RmImport(opts) => format!("removes import `{}` from namespace `{}`", opts.source_ns, opts.namespace),
EditSubcommand::NsDoc(opts) => format!("updates documentation for namespace `{}`", opts.namespace),
EditSubcommand::Inc(_) => "describes incremental code changes (added/removed/changed defs)".to_string(),
EditSubcommand::Cp(opts) => format!(
"copies AST node from path `{}` to `{}` in `{}` (position: {})",
opts.from, opts.path, opts.target, opts.at
),
EditSubcommand::Mv(opts) => format!(
"moves AST node from path `{}` to `{}` in `{}` (position: {})",
opts.from, opts.path, opts.target, opts.at
),
EditSubcommand::Rename(opts) => format!("renames `{}` to `{}` (same namespace)", opts.source, opts.new_name),
EditSubcommand::SplitDef(opts) => format!(
"extracts sub-expression at path `{}` from `{}` into new definition `{}`",
opts.path, opts.target, opts.new_name
),
})
}
fn render_tree_explanation(cmd: &TreeCommand) -> Option<String> {
Some(match &cmd.subcommand {
TreeSubcommand::Show(opts) => {
let mut desc = format!("displays AST subtree of `{}`", opts.target);
if let Some(path) = &opts.path {
desc.push_str(&format!(" at path `{path}`"));
}
if opts.json {
desc.push_str(" (JSON output)");
}
if opts.raw {
desc.push_str(" (raw, no chunking)");
}
desc
}
TreeSubcommand::Replace(opts) => {
let code_parts = CodeInputParts::new(opts.file.as_deref(), opts.code.as_deref(), opts.json.as_deref(), opts.json_input);
let mut desc = format!("replaces AST node at path `{}` in `{}`", opts.path, opts.target);
let code_desc = describe_code_input(&code_parts);
if !code_desc.is_empty() {
desc.push_str(&format!("; {code_desc}"));
}
desc
}
TreeSubcommand::ReplaceLeaf(opts) => {
let code_parts = CodeInputParts::new(opts.file.as_deref(), opts.code.as_deref(), opts.json.as_deref(), opts.json_input);
let mode = if opts.regex { "regex match" } else { "exact match" };
let mut desc = format!("replaces all leaf nodes matching `{}` in `{}` ({mode})", opts.pattern, opts.target);
let code_desc = describe_code_input(&code_parts);
if !code_desc.is_empty() {
desc.push_str(&format!("; {code_desc}"));
}
desc
}
TreeSubcommand::SearchReplace(opts) => {
let code_parts = CodeInputParts::new(opts.file.as_deref(), opts.code.as_deref(), opts.json.as_deref(), opts.json_input);
let mut desc = format!("finds unique leaf `{}` in `{}` and replaces it", opts.pattern, opts.target);
let code_desc = describe_code_input(&code_parts);
if !code_desc.is_empty() {
desc.push_str(&format!("; {code_desc}"));
}
desc
}
TreeSubcommand::Delete(opts) => format!("removes AST node at path `{}` from `{}`", opts.path, opts.target),
TreeSubcommand::InsertBefore(opts) => format!("inserts code before path `{}` in `{}`", opts.path, opts.target),
TreeSubcommand::InsertAfter(opts) => format!("inserts code after path `{}` in `{}`", opts.path, opts.target),
TreeSubcommand::InsertChild(opts) => format!("inserts child node at path `{}` in `{}`", opts.path, opts.target),
TreeSubcommand::AppendChild(opts) => format!("appends child node at path `{}` in `{}`", opts.path, opts.target),
TreeSubcommand::SwapNext(opts) => format!("swaps node at path `{}` with its next sibling in `{}`", opts.path, opts.target),
TreeSubcommand::SwapPrev(opts) => format!("swaps node at path `{}` with its previous sibling in `{}`", opts.path, opts.target),
TreeSubcommand::Unwrap(opts) => format!(
"removes list wrapping at path `{}` in `{}`, promotes children up",
opts.path, opts.target
),
TreeSubcommand::Raise(opts) => format!("raises node at path `{}` one level up in `{}`", opts.path, opts.target),
TreeSubcommand::Wrap(opts) => {
let code_parts = CodeInputParts::new(opts.file.as_deref(), opts.code.as_deref(), opts.json.as_deref(), opts.json_input);
let mut desc = format!("wraps node at path `{}` in a new list in `{}`", opts.path, opts.target);
let code_desc = describe_code_input(&code_parts);
if !code_desc.is_empty() {
desc.push_str(&format!("; {code_desc}"));
}
desc
}
TreeSubcommand::Rewrite(opts) => {
let mut desc = format!("structural rewrite at path `{}` in `{}`", opts.path, opts.target);
if !opts.with.is_empty() {
desc.push_str(&format!("; binds: {}", opts.with.join(", ")));
}
desc
}
TreeSubcommand::BatchDelete(opts) => format!(
"deletes {} path(s) from `{}` (sorted highest-to-lowest)",
opts.paths.len(),
opts.target
),
})
}
fn render_docs_explanation(cmd: &DocsCommand) -> Option<String> {
Some(match &cmd.subcommand {
DocsSubcommand::Scopes(_) => "lists available documentation scopes".to_string(),
DocsSubcommand::RemoteLibs(_) => "searches remote library documentation".to_string(),
DocsSubcommand::Search(opts) => format!("searches documentation for `{}`", opts.keyword),
DocsSubcommand::List(opts) => {
let mut desc = "lists available documentation files".to_string();
if let Some(module) = &opts.module {
desc.push_str(&format!(" in module `{module}`"));
}
desc
}
DocsSubcommand::Sections(opts) => format!("shows sections in `{}`", opts.filename),
DocsSubcommand::Read(opts) => format!("reads documentation from `{}`", opts.filename),
DocsSubcommand::Agents(_) => "reads agent/developer guide".to_string(),
DocsSubcommand::ReadLines(opts) => format!("reads specific lines from `{}`", opts.filename),
DocsSubcommand::CheckMd(opts) => format!("validates code snippets in `{}`", opts.file),
})
}
fn render_analyze_explanation(cmd: &AnalyzeCommand) -> Option<String> {
Some(match &cmd.subcommand {
AnalyzeSubcommand::CallGraph(opts) => {
let mut desc = "analyzes function call graph".to_string();
if let Some(root) = &opts.root {
desc.push_str(&format!(" from root `{root}`"));
}
if let Some(prefix) = &opts.ns_prefix {
desc.push_str(&format!(", namespace prefix `{prefix}`"));
}
desc
}
AnalyzeSubcommand::CallGraphDiff(opts) => format!("compares call graph changes since `{}`", opts.git_ref),
AnalyzeSubcommand::CountCalls(opts) => {
let mut desc = "counts function call frequencies".to_string();
if let Some(root) = &opts.root {
desc.push_str(&format!(" from root `{root}`"));
}
desc
}
AnalyzeSubcommand::ProgramDiff(opts) => format!("compares AST structure since `{}`", opts.git_ref),
AnalyzeSubcommand::CheckExamples(_) => "validates all code examples in definitions".to_string(),
AnalyzeSubcommand::CheckTypes(opts) => {
let mut desc = "performs type checking".to_string();
if let Some(ns) = &opts.ns {
desc.push_str(&format!(" in namespace `{ns}`"));
}
desc
}
AnalyzeSubcommand::WeakTypes(opts) => {
let mut desc = "analyzes weak/untyped definitions".to_string();
if let Some(ns) = &opts.ns {
desc.push_str(&format!(" in namespace `{ns}`"));
}
desc
}
AnalyzeSubcommand::EffectsGraph(opts) => {
let mut desc = "builds effects dependency graph".to_string();
if let Some(root) = &opts.root {
desc.push_str(&format!(" from root `{root}`"));
}
desc
}
AnalyzeSubcommand::JsEscape(opts) => format!("escapes Calcit symbol `{}` to JS-safe name", opts.symbol),
AnalyzeSubcommand::JsUnescape(opts) => format!("unescapes JS name back to Calcit symbol `{}`", opts.symbol),
})
}
fn render_cirru_explanation(cmd: &CirruCommand) -> Option<String> {
Some(match &cmd.subcommand {
CirruSubcommand::Parse(opts) => format!("parses Cirru code `{}` into AST", opts.code),
CirruSubcommand::Format(opts) => format!("formats JSON-encoded Cirru AST: `{}`", opts.json),
CirruSubcommand::ParseEdn(opts) => format!("parses EDN data: `{}`", opts.edn),
CirruSubcommand::ShowGuide(_) => "displays Cirru language syntax guide".to_string(),
})
}
fn push_query(tokens: &mut Vec<String>, cmd: &QueryCommand) {
match &cmd.subcommand {
QuerySubcommand::Schema(opts) => echo_items!(tokens, pos "target" => &opts.target, switch "json" => opts.json),
QuerySubcommand::Ns(opts) => {
echo_items!(tokens, opt "namespace" => opts.namespace.as_deref(); default "all", switch "deps" => opts.deps)
}
QuerySubcommand::Defs(opts) => {
echo_items!(tokens, pos "namespace" => &opts.namespace, opt "tag" => opts.tag.as_deref(); default "none")
}
QuerySubcommand::Pkg(_) | QuerySubcommand::Config(_) | QuerySubcommand::Error(_) | QuerySubcommand::Modules(_) => {}
QuerySubcommand::Def(opts) => echo_items!(
tokens,
pos "target" => &opts.target,
switch "json" => opts.json,
value "chunk-target-nodes" => opts.chunk_target_nodes; default "56",
value "chunk-max-nodes" => opts.chunk_max_nodes; default "68",
value "chunk-trigger-nodes" => opts.chunk_trigger_nodes; default "88",
switch "raw" => opts.raw
),
QuerySubcommand::Peek(opts) => echo_items!(tokens, pos "target" => &opts.target),
QuerySubcommand::Examples(opts) => echo_items!(tokens, pos "target" => &opts.target),
QuerySubcommand::Find(opts) => echo_items!(
tokens,
pos "symbol" => &opts.symbol,
switch "deps" => opts.deps,
switch "exact" => opts.exact,
value "limit" => opts.limit; default "20",
value "detail-offset" => opts.detail_offset; default "0"
),
QuerySubcommand::Usages(opts) => echo_items!(
tokens,
pos "target" => &opts.target,
switch "deps" => opts.deps,
value "detail-offset" => opts.detail_offset; default "0"
),
QuerySubcommand::Search(opts) => echo_items!(
tokens,
pos "pattern" => &opts.pattern,
opt "filter" => opts.filter.as_deref(); default "none",
switch "exact" => opts.exact,
switch "regex" => opts.regex,
value "max-depth" => opts.max_depth; default "0",
opt "start-path" => opts.start_path.as_deref(); default "none",
opt "entry" => opts.entry.as_deref(); default "none",
value "detail-offset" => opts.detail_offset; default "0"
),
QuerySubcommand::SearchExpr(opts) => echo_items!(
tokens,
pos "pattern" => &opts.pattern,
opt "filter" => opts.filter.as_deref(); default "none",
switch "exact" => opts.exact,
value "max-depth" => opts.max_depth; default "0",
switch "json" => opts.json,
opt "entry" => opts.entry.as_deref(); default "none",
value "detail-offset" => opts.detail_offset; default "0"
),
QuerySubcommand::HostProcs(opts) => {
echo_items!(tokens, opt "tag" => opts.tag.as_deref(); default "none")
}
}
}
fn push_docs(tokens: &mut Vec<String>, cmd: &DocsCommand) {
match &cmd.subcommand {
DocsSubcommand::Scopes(_) => {}
DocsSubcommand::RemoteLibs(opts) => {
if let Some(subcommand) = opts.subcommand.as_ref() {
push_libs(tokens, subcommand);
}
}
DocsSubcommand::Search(opts) => echo_items!(
tokens,
pos "keyword" => &opts.keyword,
value "context" => opts.context; default "5",
opt "filename" => opts.filename.as_deref(); default "none",
opt "module" => opts.module.as_deref(); default "none"
),
DocsSubcommand::List(opts) => echo_items!(tokens, opt "module" => opts.module.as_deref(); default "none"),
DocsSubcommand::Sections(opts) => echo_items!(
tokens,
pos "filename" => &opts.filename,
opt "module" => opts.module.as_deref(); default "none",
switch "with-lines" => opts.with_lines
),
DocsSubcommand::Read(opts) => echo_items!(
tokens,
pos "filename" => &opts.filename,
list "heading" => &opts.headings,
switch "no-subheadings" => opts.no_subheadings,
switch "full" => opts.full,
switch "with-lines" => opts.with_lines,
opt "module" => opts.module.as_deref(); default "none"
),
DocsSubcommand::Agents(opts) => echo_items!(
tokens,
list "heading" => &opts.headings,
switch "no-subheadings" => opts.no_subheadings,
switch "full" => opts.full,
switch "with-lines" => opts.with_lines,
switch "refresh" => opts.refresh
),
DocsSubcommand::ReadLines(opts) => echo_items!(
tokens,
pos "filename" => &opts.filename,
value "start" => opts.start; default "0",
value "lines" => opts.lines; default "80",
opt "module" => opts.module.as_deref(); default "none"
),
DocsSubcommand::CheckMd(opts) => {
echo_items!(tokens, pos "file" => &opts.file, value "entry" => &opts.entry; default "calcit.cirru", list "dep" => &opts.dep)
}
}
}
fn push_libs(tokens: &mut Vec<String>, subcommand: &LibsSubcommand) {
match subcommand {
LibsSubcommand::Readme(opts) => echo_items!(
tokens,
pos "package" => &opts.package,
list "heading" => &opts.headings,
opt "file" => opts.file.as_deref(); default "none",
switch "no-subheadings" => opts.no_subheadings,
switch "full" => opts.full,
switch "with-lines" => opts.with_lines
),
LibsSubcommand::Search(opts) => echo_items!(tokens, pos "keyword" => &opts.keyword),
LibsSubcommand::ScanMd(opts) => echo_items!(tokens, pos "module" => &opts.module),
}
}
fn push_cirru(tokens: &mut Vec<String>, cmd: &CirruCommand) {
match &cmd.subcommand {
CirruSubcommand::Parse(opts) => {
echo_items!(tokens, pos "code" => &opts.code, switch "expr-one" => opts.expr_one_liner, switch "validate" => opts.validate)
}
CirruSubcommand::Format(opts) => echo_items!(tokens, pos "json" => &opts.json),
CirruSubcommand::ParseEdn(opts) => echo_items!(tokens, pos "edn" => &opts.edn),
CirruSubcommand::ShowGuide(_) => {}
}
}
fn push_analyze(tokens: &mut Vec<String>, cmd: &AnalyzeCommand) {
match &cmd.subcommand {
AnalyzeSubcommand::CallGraph(opts) => echo_items!(
tokens,
opt "root" => opts.root.as_deref(); default "config.init-fn",
opt "ns-prefix" => opts.ns_prefix.as_deref(); default "none",
switch "include-core" => opts.include_core,
value "max-depth" => opts.max_depth; default "0",
switch "show-unused" => opts.show_unused,
value "format" => &opts.format; default "text"
),
AnalyzeSubcommand::CallGraphDiff(opts) => echo_items!(
tokens,
pos "git-ref" => &opts.git_ref,
opt "root" => opts.root.as_deref(); default "config.init-fn",
opt "ns-prefix" => opts.ns_prefix.as_deref(); default "none",
switch "include-core" => opts.include_core,
value "max-depth" => opts.max_depth; default "0"
),
AnalyzeSubcommand::CountCalls(opts) => echo_items!(
tokens,
opt "root" => opts.root.as_deref(); default "config.init-fn",
opt "ns-prefix" => opts.ns_prefix.as_deref(); default "none",
switch "include-core" => opts.include_core,
value "format" => &opts.format; default "text",
value "sort" => &opts.sort; default "count"
),
AnalyzeSubcommand::ProgramDiff(opts) => echo_items!(tokens, pos "git-ref" => &opts.git_ref,
opt "def" => opts.def.as_deref(); default "none"
),
AnalyzeSubcommand::CheckExamples(opts) => echo_items!(tokens, value "ns" => &opts.ns),
AnalyzeSubcommand::CheckTypes(opts) => echo_items!(
tokens,
opt "ns" => opts.ns.as_deref(); default "none",
opt "ns-prefix" => opts.ns_prefix.as_deref(); default "none",
opt "only" => opts.only.as_deref(); default "all",
switch "deps" => opts.deps
),
AnalyzeSubcommand::WeakTypes(opts) => echo_items!(
tokens,
opt "ns" => opts.ns.as_deref(); default "none",
opt "ns-prefix" => opts.ns_prefix.as_deref(); default "none",
opt "only" => opts.only.as_deref(); default "all",
switch "deps" => opts.deps
),
AnalyzeSubcommand::EffectsGraph(opts) => echo_items!(
tokens,
opt "root" => opts.root.as_deref(); default "config.init-fn",
opt "ns-prefix" => opts.ns_prefix.as_deref(); default "none",
switch "include-core" => opts.include_core,
value "max-depth" => opts.max_depth; default "2",
value "format" => &opts.format; default "tree",
value "detail" => &opts.detail; default "summary",
value "color" => opts.color; default "true"
),
AnalyzeSubcommand::JsEscape(opts) => echo_items!(tokens, pos "symbol" => &opts.symbol),
AnalyzeSubcommand::JsUnescape(opts) => echo_items!(tokens, pos "symbol" => &opts.symbol),
}
}
fn push_edit(tokens: &mut Vec<String>, cmd: &EditCommand) {
match &cmd.subcommand {
EditSubcommand::Format(_) => {}
EditSubcommand::Def(opts) => {
echo_items!(tokens, pos "target" => &opts.target, code_input opts, switch "overwrite" => opts.overwrite)
}
EditSubcommand::MvDef(opts) => echo_items!(tokens, pos "source" => &opts.source, pos "target" => &opts.target),
EditSubcommand::RmDef(opts) => echo_items!(tokens, pos "target" => &opts.target),
EditSubcommand::Doc(opts) => echo_items!(tokens, pos "target" => &opts.target, pos "doc" => &opts.doc),
EditSubcommand::Schema(opts) => echo_items!(tokens, pos "target" => &opts.target, code_input opts, switch "clear" => opts.clear),
EditSubcommand::Examples(opts) => echo_items!(tokens, pos "target" => &opts.target, code_input opts, switch "clear" => opts.clear),
EditSubcommand::AddExample(opts) => {
echo_items!(tokens, pos "target" => &opts.target, opt_owned "at" => opts.at.map(|v| v.to_string()); default "append", code_input opts)
}
EditSubcommand::RmExample(opts) => echo_items!(tokens, pos "target" => &opts.target, pos "index" => &opts.index.to_string()),
EditSubcommand::Tags(opts) => {
echo_items!(
tokens,
pos "target" => &opts.target,
opt "tags" => opts.tags.as_deref(); default "none"
)
}
EditSubcommand::AddNs(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace, code_input opts),
EditSubcommand::RmNs(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace),
EditSubcommand::Imports(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace, code_input opts),
EditSubcommand::AddImport(opts) => {
echo_items!(tokens, pos "namespace" => &opts.namespace, code_input opts, switch "overwrite" => opts.overwrite)
}
EditSubcommand::RmImport(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace, pos "source-ns" => &opts.source_ns),
EditSubcommand::NsDoc(opts) => echo_items!(tokens, pos "namespace" => &opts.namespace, pos "doc" => &opts.doc),
EditSubcommand::Inc(opts) => {
echo_items!(tokens, list "added-ns" => &opts.added_ns, list "removed-ns" => &opts.removed_ns, list "ns-updated" => &opts.ns_updated, list "added" => &opts.added, list "removed" => &opts.removed, list "changed" => &opts.changed)
}
EditSubcommand::Cp(opts) => {
echo_items!(tokens, pos "target" => &opts.target, value "from" => &opts.from, value "path" => &opts.path, value "at" => &opts.at; default "after")
}
EditSubcommand::Mv(opts) => {
echo_items!(tokens, pos "target" => &opts.target, value "from" => &opts.from, value "path" => &opts.path, value "at" => &opts.at; default "after")
}
EditSubcommand::Rename(opts) => echo_items!(tokens, pos "source" => &opts.source, pos "new-name" => &opts.new_name),
EditSubcommand::SplitDef(opts) => {
echo_items!(tokens, pos "target" => &opts.target, value "path" => &opts.path, value "name" => &opts.new_name)
}
}
}
fn push_tree(tokens: &mut Vec<String>, cmd: &TreeCommand) {
match &cmd.subcommand {
TreeSubcommand::Show(opts) => echo_items!(
tokens,
pos "target" => &opts.target,
opt "path" => opts.path.as_deref(); default "none",
value "depth" => opts.depth; default "2",
switch "json" => opts.json,
value "chunk-target-nodes" => opts.chunk_target_nodes; default "56",
value "chunk-max-nodes" => opts.chunk_max_nodes; default "68",
value "chunk-trigger-nodes" => opts.chunk_trigger_nodes; default "88",
value "chunk-expand-depth" => opts.chunk_expand_depth; default "1",
switch "raw" => opts.raw
),
TreeSubcommand::Replace(opts) => {
echo_items!(tokens, pos "target" => &opts.target, value "path" => &opts.path, code_input opts, value "depth" => opts.depth; default "2")
}
TreeSubcommand::ReplaceLeaf(opts) => {
echo_items!(tokens, pos "target" => &opts.target, value "pattern" => &opts.pattern, switch "regex" => opts.regex, code_input opts, value "depth" => opts.depth; default "2")
}
TreeSubcommand::Delete(opts) => {
echo_items!(tokens, pos "target" => &opts.target, value "path" => &opts.path, value "depth" => opts.depth; default "2")
}
TreeSubcommand::InsertBefore(opts) => push_tree_insert(
tokens,
&opts.target,
&opts.path,
CodeInputParts::new(opts.file.as_deref(), opts.code.as_deref(), opts.json.as_deref(), opts.json_input),
opts.depth,
),
TreeSubcommand::InsertAfter(opts) => push_tree_insert(
tokens,
&opts.target,
&opts.path,
CodeInputParts::new(opts.file.as_deref(), opts.code.as_deref(), opts.json.as_deref(), opts.json_input),
opts.depth,
),
TreeSubcommand::InsertChild(opts) => push_tree_insert(
tokens,
&opts.target,
&opts.path,
CodeInputParts::new(opts.file.as_deref(), opts.code.as_deref(), opts.json.as_deref(), opts.json_input),
opts.depth,
),
TreeSubcommand::AppendChild(opts) => push_tree_insert(
tokens,
&opts.target,
&opts.path,
CodeInputParts::new(opts.file.as_deref(), opts.code.as_deref(), opts.json.as_deref(), opts.json_input),
opts.depth,
),
TreeSubcommand::SwapNext(opts) => push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth),
TreeSubcommand::SwapPrev(opts) => push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth),
TreeSubcommand::Unwrap(opts) => push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth),
TreeSubcommand::Raise(opts) => push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth),
TreeSubcommand::Wrap(opts) => {
push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth);
echo_items!(tokens, code_input opts);
}
TreeSubcommand::SearchReplace(opts) => {
echo_items!(tokens, pos "target" => &opts.target, value "pattern" => &opts.pattern, switch "regex" => opts.regex, code_input opts, value "depth" => opts.depth; default "2")
}
TreeSubcommand::Rewrite(opts) => {
push_tree_path_depth(tokens, &opts.target, &opts.path, opts.depth);
echo_items!(tokens, code_input opts, list "with" => &opts.with);
}
TreeSubcommand::BatchDelete(opts) => {
echo_items!(tokens, pos "target" => &opts.target, list "paths" => &opts.paths, value "depth" => opts.depth; default "2");
}
}
}
struct CodeInputParts<'a> {
file: Option<&'a str>,
code: Option<&'a str>,
json: Option<&'a str>,
json_input: bool,
}
impl<'a> CodeInputParts<'a> {
fn new(file: Option<&'a str>, code: Option<&'a str>, json: Option<&'a str>, json_input: bool) -> Self {
Self {
file,
code,
json,
json_input,
}
}
}
fn push_tree_insert(tokens: &mut Vec<String>, target: &str, path: &str, code_input: CodeInputParts<'_>, depth: usize) {
push_tree_path_depth(tokens, target, path, depth);
push_code_input(tokens, &code_input);
}
fn push_tree_path_depth(tokens: &mut Vec<String>, target: &str, path: &str, depth: usize) {
push_positional(tokens, "target", target);
push_value(tokens, "path", path, None);
push_value(tokens, "depth", &depth.to_string(), Some("2"));
}
fn push_code_input(tokens: &mut Vec<String>, code_input: &CodeInputParts<'_>) {
push_optional(tokens, "file", code_input.file, "none");
push_optional(tokens, "code", code_input.code, "none");
push_optional(tokens, "json", code_input.json, "none");
push_switch(tokens, "json-input", code_input.json_input);
}
fn push_switch(tokens: &mut Vec<String>, name: &str, enabled: bool) {
tokens.push(format!("--{name}={}", if enabled { "ON" } else { "OFF" }));
}
fn push_value(tokens: &mut Vec<String>, name: &str, value: &str, default: Option<&str>) {
let display_value = format_atom(value);
match default {
Some(default_value) if default_value == value => {
tokens.push(format!("--{name}=({})", format_atom(default_value)));
}
_ => tokens.push(format!("--{name}={display_value}")),
}
}
fn push_optional(tokens: &mut Vec<String>, name: &str, value: Option<&str>, default_label: &str) {
match value {
Some(value) => tokens.push(format!("--{name}={}", format_atom(value))),
None => tokens.push(format!("--{name}=({})", format_atom(default_label))),
}
}
fn push_optional_owned(tokens: &mut Vec<String>, name: &str, value: Option<String>, default_label: &str) {
match value {
Some(value) => tokens.push(format!("--{name}={}", format_atom(&value))),
None => tokens.push(format!("--{name}=({})", format_atom(default_label))),
}
}
fn push_list(tokens: &mut Vec<String>, name: &str, values: &[String]) {
if values.is_empty() {
tokens.push(format!("--{name}=(none)"));
} else {
for value in values {
tokens.push(format!("--{name}={}", format_atom(value)));
}
}
}
fn push_positional(tokens: &mut Vec<String>, name: &str, value: &str) {
tokens.push(format!("{name}={}", format_atom(value)));
}
fn format_atom(value: &str) -> String {
if value.is_empty() {
return String::from("''");
}
if value.chars().any(char::is_whitespace) {
return format!("{value:?}");
}
value.to_owned()
}
fn query_name(subcommand: &QuerySubcommand) -> &'static str {
match subcommand {
QuerySubcommand::Ns(_) => "ns",
QuerySubcommand::Defs(_) => "defs",
QuerySubcommand::Pkg(_) => "pkg",
QuerySubcommand::Config(_) => "config",
QuerySubcommand::Error(_) => "error",
QuerySubcommand::Modules(_) => "modules",
QuerySubcommand::Def(_) => "def",
QuerySubcommand::Peek(_) => "peek",
QuerySubcommand::Examples(_) => "examples",
QuerySubcommand::Find(_) => "find",
QuerySubcommand::Usages(_) => "usages",
QuerySubcommand::Search(_) => "search",
QuerySubcommand::SearchExpr(_) => "search-expr",
QuerySubcommand::Schema(_) => "schema",
QuerySubcommand::HostProcs(_) => "host-procs",
}
}
fn docs_name(subcommand: &DocsSubcommand) -> &'static str {
match subcommand {
DocsSubcommand::Scopes(_) => "scopes",
DocsSubcommand::RemoteLibs(_) => "remote-libs",
DocsSubcommand::Search(_) => "search",
DocsSubcommand::List(_) => "list",
DocsSubcommand::Sections(_) => "sections",
DocsSubcommand::Read(_) => "read",
DocsSubcommand::Agents(_) => "agents",
DocsSubcommand::ReadLines(_) => "read-lines",
DocsSubcommand::CheckMd(_) => "check-md",
}
}
fn libs_name(subcommand: &LibsSubcommand) -> &'static str {
match subcommand {
LibsSubcommand::Readme(_) => "readme",
LibsSubcommand::Search(_) => "search",
LibsSubcommand::ScanMd(_) => "scan-md",
}
}
fn cirru_name(subcommand: &CirruSubcommand) -> &'static str {
match subcommand {
CirruSubcommand::Parse(_) => "parse",
CirruSubcommand::Format(_) => "format",
CirruSubcommand::ParseEdn(_) => "parse-edn",
CirruSubcommand::ShowGuide(_) => "show-guide",
}
}
fn analyze_name(subcommand: &AnalyzeSubcommand) -> &'static str {
match subcommand {
AnalyzeSubcommand::CallGraph(_) => "call-graph",
AnalyzeSubcommand::CallGraphDiff(_) => "call-graph-diff",
AnalyzeSubcommand::CountCalls(_) => "count-calls",
AnalyzeSubcommand::ProgramDiff(_) => "program-diff",
AnalyzeSubcommand::CheckExamples(_) => "check-examples",
AnalyzeSubcommand::CheckTypes(_) => "check-types",
AnalyzeSubcommand::WeakTypes(_) => "weak-types",
AnalyzeSubcommand::EffectsGraph(_) => "effects-graph",
AnalyzeSubcommand::JsEscape(_) => "js-escape",
AnalyzeSubcommand::JsUnescape(_) => "js-unescape",
}
}
fn edit_name(subcommand: &EditSubcommand) -> &'static str {
match subcommand {
EditSubcommand::Format(_) => "format",
EditSubcommand::Def(_) => "def",
EditSubcommand::MvDef(_) => "mv-def",
EditSubcommand::RmDef(_) => "rm-def",
EditSubcommand::Doc(_) => "doc",
EditSubcommand::Schema(_) => "schema",
EditSubcommand::Examples(_) => "examples",
EditSubcommand::AddExample(_) => "add-example",
EditSubcommand::RmExample(_) => "rm-example",
EditSubcommand::Tags(_) => "tags",
EditSubcommand::AddNs(_) => "add-ns",
EditSubcommand::RmNs(_) => "rm-ns",
EditSubcommand::Imports(_) => "imports",
EditSubcommand::AddImport(_) => "add-import",
EditSubcommand::RmImport(_) => "rm-import",
EditSubcommand::NsDoc(_) => "ns-doc",
EditSubcommand::Inc(_) => "inc",
EditSubcommand::Cp(_) => "cp",
EditSubcommand::Mv(_) => "mv",
EditSubcommand::Rename(_) => "rename",
EditSubcommand::SplitDef(_) => "split-def",
}
}
fn tree_name(subcommand: &TreeSubcommand) -> &'static str {
match subcommand {
TreeSubcommand::Show(_) => "show",
TreeSubcommand::Replace(_) => "replace",
TreeSubcommand::ReplaceLeaf(_) => "replace-leaf",
TreeSubcommand::Delete(_) => "delete",
TreeSubcommand::InsertBefore(_) => "insert-before",
TreeSubcommand::InsertAfter(_) => "insert-after",
TreeSubcommand::InsertChild(_) => "insert-child",
TreeSubcommand::AppendChild(_) => "append-child",
TreeSubcommand::SwapNext(_) => "swap-next",
TreeSubcommand::SwapPrev(_) => "swap-prev",
TreeSubcommand::Unwrap(_) => "unwrap",
TreeSubcommand::Raise(_) => "raise",
TreeSubcommand::Wrap(_) => "wrap",
TreeSubcommand::SearchReplace(_) => "search-replace",
TreeSubcommand::Rewrite(_) => "rewrite",
TreeSubcommand::BatchDelete(_) => "batch-delete",
}
}
fn config_name(subcommand: &ConfigSubcommand) -> &'static str {
match subcommand {
ConfigSubcommand::Show(_) => "show",
ConfigSubcommand::Modules(_) => "modules",
ConfigSubcommand::Version(_) => "version",
ConfigSubcommand::Set(_) => "set",
ConfigSubcommand::AddModule(_) => "add-module",
ConfigSubcommand::RmModule(_) => "rm-module",
}
}
fn push_config(tokens: &mut Vec<String>, cmd: &ConfigCommand) {
match &cmd.subcommand {
ConfigSubcommand::Show(opts) => echo_items!(tokens, opt "entry" => opts.entry.as_deref(); default "none"),
ConfigSubcommand::Modules(opts) => echo_items!(tokens, opt "entry" => opts.entry.as_deref(); default "none"),
ConfigSubcommand::Version(opts) => echo_items!(tokens, opt "value" => opts.value.as_deref(); default "none"),
ConfigSubcommand::Set(opts) => {
echo_items!(tokens, opt "entry" => opts.entry.as_deref(); default "none");
echo_items!(tokens, pos "key" => &opts.key, pos "value" => &opts.value);
}
ConfigSubcommand::AddModule(opts) => {
echo_items!(tokens, opt "entry" => opts.entry.as_deref(); default "none");
echo_items!(tokens, pos "module_path" => &opts.module_path);
}
ConfigSubcommand::RmModule(opts) => {
echo_items!(tokens, opt "entry" => opts.entry.as_deref(); default "none");
echo_items!(tokens, pos "module_path" => &opts.module_path);
}
}
}