use std::sync::Arc;
use crate::{BracketState, PartialParse, SubtreeProvider, ValueProvider};
pub fn fs_paths_provider() -> ValueProvider {
Arc::new(|partial: &str, _ctx: &[&str]| -> Vec<String> {
complete_fs_paths(partial, FsKind::Any)
})
}
pub fn fs_files_provider() -> ValueProvider {
Arc::new(|partial: &str, _ctx: &[&str]| -> Vec<String> {
complete_fs_paths(partial, FsKind::FilesAndDirsForDescent)
})
}
pub fn fs_dirs_provider() -> ValueProvider {
Arc::new(|partial: &str, _ctx: &[&str]| -> Vec<String> {
complete_fs_paths(partial, FsKind::DirsOnly)
})
}
pub fn no_op_provider() -> ValueProvider {
Arc::new(|_partial: &str, _ctx: &[&str]| -> Vec<String> { Vec::new() })
}
#[derive(Clone, Copy)]
enum FsKind {
Any,
FilesAndDirsForDescent,
DirsOnly,
}
fn complete_fs_paths(partial: &str, kind: FsKind) -> Vec<String> {
let (dir_to_list, prefix_to_strip, name_prefix) = split_partial(partial);
let entries = match std::fs::read_dir(&dir_to_list) {
Ok(it) => it,
Err(_) => return Vec::new(),
};
let mut out: Vec<String> = Vec::new();
for entry in entries.flatten() {
let name_os = entry.file_name();
let Some(name) = name_os.to_str() else { continue };
if name.starts_with('.') && !name_prefix.starts_with('.') {
continue;
}
if !name.starts_with(&name_prefix) { continue; }
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
match kind {
FsKind::DirsOnly if !is_dir => continue,
FsKind::FilesAndDirsForDescent | FsKind::Any | FsKind::DirsOnly => {}
}
let mut candidate = String::with_capacity(prefix_to_strip.len() + name.len() + 1);
candidate.push_str(&prefix_to_strip);
candidate.push_str(name);
if is_dir { candidate.push('/'); }
out.push(candidate);
}
out.sort();
out
}
fn split_partial(partial: &str) -> (std::path::PathBuf, String, String) {
let (dir_str, name_prefix) = match partial.rfind('/') {
Some(i) => (&partial[..=i], &partial[i + 1..]),
None => ("", partial),
};
let dir_to_read: std::path::PathBuf = if let Some(rest) = dir_str.strip_prefix("~/") {
let home = std::env::var_os("HOME")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::PathBuf::from("."));
if rest.is_empty() { home } else { home.join(rest) }
} else if dir_str.is_empty() {
std::path::PathBuf::from(".")
} else {
std::path::PathBuf::from(dir_str)
};
(dir_to_read, dir_str.to_string(), name_prefix.to_string())
}
fn shell_wrapper_quote(raw_line: &str, cursor: usize) -> Option<(usize, char)> {
let before = &raw_line[..cursor.min(raw_line.len())];
let bytes = before.as_bytes();
let mut wrapper_open: Option<u8> = None;
let mut wrapper_open_at: Option<usize> = None;
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
match wrapper_open {
Some(q) => {
if c == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if c == q {
wrapper_open = None;
wrapper_open_at = None;
}
}
None => {
if c == b'\'' || c == b'"' {
let prev_is_ws_or_start =
i == 0 || (bytes[i - 1] as char).is_whitespace();
if prev_is_ws_or_start {
wrapper_open = Some(c);
wrapper_open_at = Some(i + 1);
}
}
}
}
i += 1;
}
wrapper_open_at.map(|s| (s, wrapper_open.expect("opener tracked alongside start") as char))
}
fn bracket_state_of(s: &str) -> BracketState {
let mut state = BracketState::default();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if let Some(q) = state.inside_quote {
if c == '\\' { chars.next(); continue; }
if c == q { state.inside_quote = None; }
continue;
}
match c {
'(' => state.paren += 1,
')' => state.paren -= 1,
'{' => state.brace += 1,
'}' => state.brace -= 1,
'[' => state.bracket += 1,
']' => state.bracket -= 1,
'"' | '\'' => state.inside_quote = Some(c),
_ => {}
}
}
state
}
fn ident_before_cursor_of(s: &str) -> &str {
let bytes = s.as_bytes();
let mut i = bytes.len();
while i > 0 {
let c = bytes[i - 1] as char;
if is_grammar_ident_char(c) { i -= 1; } else { break; }
}
&s[i..]
}
fn trigger_char_of(s: &str) -> Option<char> {
let mut chars = s.chars().rev();
while let Some(c) = chars.clone().next() {
if is_grammar_ident_char(c) { chars.next(); } else { break; }
}
chars.next()
}
#[inline]
fn is_grammar_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_' || c == ':'
}
const LABEL_MATCH_OPERATORS: &[&str] = &["=", "!=", "=~", "!~"];
fn expand_metric_continuations(
pp: &PartialParse,
target_start: usize,
metric: &str,
paren_depth: i32,
wrapper_quote: Option<char>,
ident: &str,
is_unique_match: bool,
) -> Vec<String> {
let mut variants: Vec<String> = vec![metric.to_string()];
let show_continuations = ident == metric || is_unique_match || pp.tap_count >= 2;
if show_continuations {
variants.push(format!("{metric}{{"));
variants.push(format!("{metric}["));
if paren_depth > 0 {
variants.push(format!("{metric})"));
}
if let Some(q) = wrapper_quote {
if paren_depth > 0 {
variants.push(format!("{metric}){q}"));
} else {
variants.push(format!("{metric}{q}"));
}
}
}
variants.into_iter()
.map(|v| pp.splice_candidate(target_start, &v))
.collect()
}
fn append_close_continuations(
pp: &PartialParse,
out: &mut Vec<String>,
ident: &str,
bs: &BracketState,
wrapper_quote: Option<char>,
) {
if !ident.is_empty() {
return;
}
let target_start = pp.cursor_offset;
if bs.paren > 0 {
out.push(pp.splice_candidate(target_start, ")"));
out.push(pp.splice_candidate(target_start, ","));
}
if let Some(q) = wrapper_quote {
if bs.paren == 0 && bs.brace == 0 && bs.bracket == 0 {
out.push(pp.splice_candidate(target_start, &q.to_string()));
} else if bs.paren == 1 && bs.brace == 0 && bs.bracket == 0 {
out.push(pp.splice_candidate(target_start, &format!("){q}")));
}
}
}
fn emit_function_candidates(
pp: &PartialParse,
target_start: usize,
ident: &str,
) -> Vec<String> {
let mut out = Vec::new();
for f in METRICSQL_FUNCTIONS.iter().filter(|f| f.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, &format!("{f}(")));
out.push(pp.splice_candidate(target_start, &format!("{f}()")));
}
out
}
fn enforce_multi_candidate(
pp: &PartialParse,
out: &mut Vec<String>,
wrapper_quote: Option<char>,
) {
if wrapper_quote.is_none() || out.len() >= 2 {
return;
}
let typed = pp.shell_current_word();
if typed.is_empty() {
return;
}
if let Some(real) = out.first()
&& real.starts_with(typed) && real.len() > typed.len() {
let mut split = real.len() - 1;
while split > typed.len() && !real.is_char_boundary(split) {
split -= 1;
}
if split > typed.len() {
let ghost = real[..split].to_string();
if !out.iter().any(|c| c == &ghost) {
out.push(ghost);
return;
}
}
}
if !out.iter().any(|c| c == typed) {
out.push(typed.to_string());
return;
}
let sentinel = format!("{typed}#options");
if !out.iter().any(|c| c == &sentinel) {
out.push(sentinel);
}
}
pub trait MetricsqlCatalog: Send + Sync + 'static {
fn metric_names(&self, prefix: &str) -> Vec<String>;
fn label_keys(&self, metric: &str, prefix: &str) -> Vec<String>;
fn label_values(&self, metric: &str, label: &str, prefix: &str) -> Vec<String>;
}
pub const METRICSQL_FUNCTIONS: &[&str] = &[
"rate", "irate", "increase", "increase_pure", "delta", "idelta",
"deriv", "deriv_fast", "predict_linear", "holt_winters",
"changes", "changes_prometheus", "resets",
"avg_over_time", "min_over_time", "max_over_time", "sum_over_time",
"count_over_time", "stddev_over_time", "stdvar_over_time",
"first_over_time", "last_over_time", "quantile_over_time",
"median_over_time", "mode_over_time", "absent_over_time",
"present_over_time", "distinct_over_time", "histogram_over_time",
"rollup", "rollup_rate", "rollup_increase", "rollup_delta",
"rollup_deriv", "rollup_scrape_interval", "rollup_candlestick",
"lag", "lifetime", "scrape_interval",
"sum", "min", "max", "avg", "stddev", "stdvar", "count",
"count_values", "bottomk", "topk", "quantile", "median", "group",
"limitk", "any", "geomean", "histogram", "outliersk", "mode",
"zscore", "share", "absent",
"abs", "absent", "ceil", "clamp_max", "clamp_min", "clamp",
"exp", "floor", "ln", "log2", "log10", "round", "scalar", "sgn",
"sort", "sort_desc", "sort_by_label", "sort_by_label_desc",
"sqrt", "time", "timestamp", "vector", "year", "month", "day_of_month",
"day_of_week", "days_in_month", "minute", "hour",
"label_replace", "label_join", "label_set", "label_del",
"label_keep", "label_lowercase", "label_uppercase", "label_value",
"label_match", "label_mismatch", "label_copy", "label_move",
"label_transform", "labels_equal",
"histogram_quantile", "histogram_share", "histogram_avg",
"histogram_stddev", "histogram_stdvar", "buckets_limit",
"prometheus_buckets",
"pi", "rand", "rand_normal", "rand_exponential", "now", "step",
"start", "end",
];
pub const METRICSQL_AGGR_MODIFIERS: &[&str] = &[
"by", "without",
];
pub const METRICSQL_BIN_MATCHING_MODIFIERS: &[&str] = &[
"on", "ignoring", "group_left", "group_right",
];
pub const METRICSQL_LOGICAL_OPS: &[&str] = &[
"and", "or", "unless",
];
pub const METRICSQL_COMPARISON_OPS: &[&str] = &[
"==", "!=", ">", "<", ">=", "<=",
];
pub const METRICSQL_ARITH_OPS: &[&str] = &[
"+", "-", "*", "/", "%", "^", "atan2",
];
pub const METRICSQL_BIN_OPS: &[&str] = &[
"and", "or", "unless", "ignoring", "on", "group_left", "group_right",
"atan2", "+", "-", "*", "/", "%", "^",
"==", "!=", ">", "<", ">=", "<=",
];
pub const METRICSQL_TIME_UNITS: &[&str] = &[
"ms", "s", "m", "h", "d", "w", "y", "i",
];
pub const METRICSQL_KEYWORDS: &[&str] = &[
"bool", "offset", "@", "start()", "end()", "default",
"WITH", "keep_metric_names", "limit",
];
const METRICSQL_SCALAR_FIRST_ARG: &[&str] = &[
"histogram_quantile", "quantile", "quantile_over_time",
"predict_linear", "holt_winters", "topk", "bottomk",
"limitk", "outliersk", "round", "clamp", "clamp_min", "clamp_max",
];
pub fn metricsql_provider(catalog: Arc<dyn MetricsqlCatalog>) -> SubtreeProvider {
Arc::new(move |pp: &PartialParse| {
complete_metricsql(&*catalog, pp)
})
}
pub const METRICSQL_DIAGNOSTIC_FLAGS: &[&str] = &[
"---metricsql-vocab", "---metricsql-context", ];
pub fn metricsql_diagnostic_args(app_name: &str) -> bool {
let argv: Vec<String> = std::env::args().collect();
let flag_idx = argv.iter().position(|a| a.starts_with("---metricsql"));
let Some(idx) = flag_idx else { return false; };
let flag = argv[idx].as_str();
let rest: Vec<&str> = argv.iter().skip(idx + 1).map(|s| s.as_str()).collect();
match flag {
"---metricsql-vocab" => {
println!("# functions");
for f in METRICSQL_FUNCTIONS { println!("{f}"); }
println!();
println!("# aggregation modifiers");
for m in METRICSQL_AGGR_MODIFIERS { println!("{m}"); }
println!();
println!("# time units");
for u in METRICSQL_TIME_UNITS { println!("{u}"); }
println!();
println!("# binary operators");
for op in METRICSQL_BIN_OPS { println!("{op}"); }
println!();
println!("# keywords");
for k in METRICSQL_KEYWORDS { println!("{k}"); }
}
"---metricsql-context" => {
let user_line = rest.first().copied().unwrap_or("");
let user_point: usize = rest.get(1)
.and_then(|s| s.parse().ok())
.unwrap_or(user_line.len());
let line_with_app = format!("{} {}", app_name, user_line);
let cursor = user_point + app_name.len() + 1;
let pp = PartialParse {
completed: Vec::new(),
partial: "",
tree_path: Vec::new(),
raw_line: &line_with_app,
cursor_offset: cursor,
tap_count: 1,
};
crate::print_partial_parse_for_diagnostics(&pp);
println!();
println!("# metricsql-derived");
let bs = pp.bracket_state();
let category = if bs.inside_quote.is_some() {
"label-value (inside open quote)"
} else if bs.bracket > 0 {
"time-unit (inside `[`)"
} else if bs.brace > 0 {
match pp.trigger_char() {
Some('=') | Some('~') => "label-value (after `=`)",
_ => "label-key (inside `{`)",
}
} else {
"top-of-expression (metric / function name)"
};
println!("category: {category}");
}
_ => return false,
}
true
}
fn complete_metricsql(
catalog: &dyn MetricsqlCatalog,
pp: &PartialParse,
) -> Vec<String> {
let wrapper = shell_wrapper_quote(pp.raw_line, pp.cursor_offset);
let wrapper_quote: Option<char> = wrapper.map(|(_, q)| q);
let mut out = complete_metricsql_inner(catalog, pp, wrapper);
enforce_multi_candidate(pp, &mut out, wrapper_quote);
out
}
fn complete_metricsql_inner(
catalog: &dyn MetricsqlCatalog,
pp: &PartialParse,
wrapper: Option<(usize, char)>,
) -> Vec<String> {
if wrapper.is_none() && cursor_past_closed_wrapper(pp.raw_line, pp.cursor_offset) {
return Vec::new();
}
let inner_start = wrapper.map(|(s, _)| s).unwrap_or(0);
let wrapper_quote: Option<char> = wrapper.map(|(_, q)| q);
let before = &pp.raw_line[inner_start..pp.cursor_offset.min(pp.raw_line.len())];
let bs = bracket_state_of(before);
let ident = ident_before_cursor_of(before);
let inner_cursor = pp.cursor_offset - inner_start;
let to_raw = |inner: usize| inner_start + inner;
if bs.inside_quote.is_some() {
if let Some(LabelValueContext { metric, label }) = label_value_context(before) {
let prefix = current_quoted_prefix(before);
let target_start = pp.cursor_offset.saturating_sub(prefix.len());
let mut out = Vec::new();
for v in catalog.label_values(metric, label, prefix) {
out.push(pp.splice_candidate(target_start, &v));
out.push(pp.splice_candidate(target_start, &format!("{v}\"")));
out.push(pp.splice_candidate(target_start, &format!("{v}\"}}")));
if let Some(q) = wrapper_quote {
out.push(pp.splice_candidate(target_start, &format!("{v}\"}}{q}")));
}
}
return out;
}
return Vec::new();
}
if bs.bracket > 0 {
let after_last_open_bracket = bracket_inner_segment(before);
let is_subquery_step = after_last_open_bracket.contains(':');
let after_colon = match ident.rsplit_once(':') {
Some((_, suffix)) => suffix,
None => ident,
};
let unit_prefix: &str = after_colon.trim_start_matches(|c: char| c.is_ascii_digit());
let target_start = pp.cursor_offset;
let mut out: Vec<String> = METRICSQL_TIME_UNITS.iter()
.filter(|u| u.starts_with(unit_prefix))
.map(|u| pp.splice_candidate(target_start, u))
.collect();
if !is_subquery_step && after_last_open_bracket.chars().any(|c| c.is_alphabetic()) {
out.push(pp.splice_candidate(pp.cursor_offset, ":"));
}
return out;
}
if bs.brace > 0 {
if brace_between_matchers(before) {
let target_start = pp.cursor_offset;
let mut out = vec![
pp.splice_candidate(target_start, ","),
pp.splice_candidate(target_start, "}"),
];
if bs.paren > 0 {
out.push(pp.splice_candidate(target_start, "})"));
}
if let Some(q) = wrapper_quote {
if bs.paren == 0 && bs.bracket == 0 {
out.push(pp.splice_candidate(target_start, &format!("}}{q}")));
} else if bs.paren == 1 && bs.bracket == 0 {
out.push(pp.splice_candidate(target_start, &format!("}}){q}")));
}
}
return out;
}
let trig = trigger_char_of(before);
match trig {
Some('=') | Some('~') => {
if let Some(LabelValueContext { metric, label }) = label_value_context(before) {
let target_start = pp.cursor_offset;
return catalog.label_values(metric, label, "")
.into_iter()
.map(|v| pp.splice_candidate(target_start, &format!("\"{}\"", v)))
.collect();
}
return Vec::new();
}
_ => {
let metric = metric_name_before_brace(before).unwrap_or("");
let target_start_inner = label_key_start_in_brace(before, inner_cursor);
let target_start = to_raw(target_start_inner);
let mut out = Vec::new();
for k in catalog.label_keys(metric, ident) {
for op in LABEL_MATCH_OPERATORS {
out.push(pp.splice_candidate(
target_start,
&format!("{}{}", k, op),
));
}
}
return out;
}
}
}
if let Some(prev_kw) = preceding_keyword(before) {
if (prev_kw == "by" || prev_kw == "without"
|| prev_kw == "on" || prev_kw == "ignoring"
|| prev_kw == "group_left" || prev_kw == "group_right")
&& bs.paren > 0
{
let target_start_inner = label_grouping_target_start(before, inner_cursor);
let target_start = to_raw(target_start_inner);
return catalog.label_keys("", ident)
.into_iter()
.map(|k| pp.splice_candidate(target_start, &k))
.collect();
}
if prev_kw == "offset" {
let target_start = pp.cursor_offset.saturating_sub(ident.len());
return METRICSQL_TIME_UNITS.iter()
.filter(|u| u.starts_with(ident))
.map(|u| pp.splice_candidate(target_start, u))
.collect();
}
if prev_kw == "@" {
let target_start = pp.cursor_offset.saturating_sub(ident.len());
return ["start()", "end()"].iter()
.filter(|s| s.starts_with(ident))
.map(|s| pp.splice_candidate(target_start, s))
.collect();
}
if METRICSQL_COMPARISON_OPS.contains(&prev_kw) {
let target_start = pp.cursor_offset.saturating_sub(ident.len());
let mut out: Vec<String> = vec![];
if "bool".starts_with(ident) {
out.push(pp.splice_candidate(target_start, "bool"));
}
out.extend(emit_function_candidates(pp, target_start, ident));
let metric_matches = catalog.metric_names(ident);
let is_unique = metric_matches.len() == 1;
for m in metric_matches {
out.extend(expand_metric_continuations(
pp, target_start, &m, bs.paren, wrapper_quote, ident, is_unique,
));
}
out.sort();
out.dedup();
return out;
}
}
if let Some(prev_ident) = preceding_ident(before) {
if is_aggregation_function(prev_ident) {
let inside_own_call = bs.paren > 0 && enclosing_function_call(before)
.map(|(fn_name, _)| fn_name == prev_ident)
.unwrap_or(false);
if !inside_own_call {
let target_start = pp.cursor_offset.saturating_sub(ident.len());
return METRICSQL_AGGR_MODIFIERS.iter()
.filter(|m| m.starts_with(ident))
.map(|m| pp.splice_candidate(target_start, m))
.collect();
}
}
if prev_ident == "keep_metric_names" {
}
}
if matches!(last_significant_char(before), Some(')')) {
let target_start = pp.cursor_offset.saturating_sub(ident.len());
let mut out: Vec<String> = vec![];
for m in METRICSQL_AGGR_MODIFIERS.iter().filter(|m| m.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, m));
}
for m in METRICSQL_BIN_MATCHING_MODIFIERS.iter().filter(|m| m.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, m));
}
for op in METRICSQL_LOGICAL_OPS.iter().filter(|o| o.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, op));
}
for op in METRICSQL_COMPARISON_OPS.iter().filter(|o| o.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, op));
}
for op in METRICSQL_ARITH_OPS.iter().filter(|o| o.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, op));
}
for kw in ["keep_metric_names", "offset", "@"].iter()
.filter(|k| k.starts_with(ident))
{
out.push(pp.splice_candidate(target_start, kw));
}
append_close_continuations(pp, &mut out, ident, &bs, wrapper_quote);
out.sort();
out.dedup();
return out;
}
if matches!(last_significant_char(before), Some('}')) {
let target_start = pp.cursor_offset.saturating_sub(ident.len());
let mut out: Vec<String> = vec![];
for op in METRICSQL_LOGICAL_OPS.iter().filter(|o| o.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, op));
}
for op in METRICSQL_COMPARISON_OPS.iter().filter(|o| o.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, op));
}
for op in METRICSQL_ARITH_OPS.iter().filter(|o| o.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, op));
}
for kw in ["[", "offset", "@"].iter().filter(|k| k.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, kw));
}
append_close_continuations(pp, &mut out, ident, &bs, wrapper_quote);
out.sort();
out.dedup();
return out;
}
if matches!(last_significant_char(before), Some(']')) {
let target_start = pp.cursor_offset.saturating_sub(ident.len());
let mut out: Vec<String> = vec![];
for op in METRICSQL_LOGICAL_OPS.iter().filter(|o| o.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, op));
}
for op in METRICSQL_COMPARISON_OPS.iter().filter(|o| o.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, op));
}
for op in METRICSQL_ARITH_OPS.iter().filter(|o| o.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, op));
}
for kw in ["offset", "@"].iter().filter(|k| k.starts_with(ident)) {
out.push(pp.splice_candidate(target_start, kw));
}
append_close_continuations(pp, &mut out, ident, &bs, wrapper_quote);
out.sort();
out.dedup();
return out;
}
if bs.paren > 0
&& let Some((fn_name, arg_idx)) = enclosing_function_call(before)
&& METRICSQL_SCALAR_FIRST_ARG.contains(&fn_name) && arg_idx == 0 {
return Vec::new();
}
let target_start = pp.cursor_offset.saturating_sub(ident.len());
let mut metrics: Vec<String> = Vec::new();
let metric_matches = catalog.metric_names(ident);
let is_unique = metric_matches.len() == 1;
for m in metric_matches {
metrics.extend(expand_metric_continuations(
pp, target_start, &m, bs.paren, wrapper_quote, ident, is_unique,
));
}
let functions = emit_function_candidates(pp, target_start, ident);
let show_functions = !ident.is_empty() || pp.tap_count >= 2;
let mut out = metrics;
if show_functions {
out.extend(functions);
}
out.sort();
out.dedup();
out
}
struct LabelValueContext<'a> {
metric: &'a str,
label: &'a str,
}
fn label_value_context(before: &str) -> Option<LabelValueContext<'_>> {
let bytes = before.as_bytes();
let mut in_quote: Option<u8> = None;
let mut last_op_end: Option<usize> = None;
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
match in_quote {
Some(q) => {
if c == b'\\' { i += 2; continue; }
if c == q { in_quote = None; }
}
None => match c {
b'"' | b'\'' => in_quote = Some(c),
b'=' | b'~' => last_op_end = Some(i + 1),
_ => {}
}
}
i += 1;
}
let op_end = last_op_end?;
let mut key_end = op_end;
while key_end > 0 {
let c = bytes[key_end - 1];
if c == b'=' || c == b'~' || c == b'!' { key_end -= 1; } else { break; }
}
let mut key_start = key_end;
while key_start > 0 && is_label_char(bytes[key_start - 1] as char) {
key_start -= 1;
}
let label = &before[key_start..key_end];
if label.is_empty() { return None; }
let mut j = key_start;
while j > 0 {
let c = bytes[j - 1];
if c == b'{' { j -= 1; break; }
j -= 1;
}
if j == 0 || bytes[j] != b'{' { return None; }
let metric_end = j;
let mut metric_start = metric_end;
while metric_start > 0 && is_label_char(bytes[metric_start - 1] as char) {
metric_start -= 1;
}
let metric = &before[metric_start..metric_end];
Some(LabelValueContext { metric, label })
}
fn brace_between_matchers(before: &str) -> bool {
let bytes = before.as_bytes();
let mut i = bytes.len();
let mut in_str = false;
while i > 0 {
let c = bytes[i - 1];
if in_str {
if c == b'"' { in_str = false; }
i -= 1;
continue;
}
match c {
b'"' => { in_str = true; i -= 1; }
b',' | b'{' => break,
_ => i -= 1,
}
}
let segment = before[i..].trim();
let segment = segment.trim_start_matches(['{', ',']).trim();
if segment.is_empty() {
return false;
}
let last_op = segment.rfind(['=', '~']);
match last_op {
Some(pos) => !segment[pos + 1..].trim().is_empty(),
None => false,
}
}
fn cursor_past_closed_wrapper(raw_line: &str, cursor: usize) -> bool {
let before = &raw_line[..cursor.min(raw_line.len())];
let bytes = before.as_bytes();
let mut wrapper_open: Option<u8> = None;
let mut had_close = false;
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
match wrapper_open {
Some(q) => {
if c == b'\\' && i + 1 < bytes.len() { i += 2; continue; }
if c == q { wrapper_open = None; had_close = true; }
}
None => {
if c == b'\'' || c == b'"' {
let prev_is_ws_or_start =
i == 0 || (bytes[i - 1] as char).is_whitespace();
if prev_is_ws_or_start {
wrapper_open = Some(c);
}
}
}
}
i += 1;
}
had_close && wrapper_open.is_none()
}
fn label_key_start_in_brace(before: &str, cursor: usize) -> usize {
let bytes = before.as_bytes();
let mut in_quote: Option<u8> = None;
let mut last_split: Option<usize> = None;
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
match in_quote {
Some(q) => {
if c == b'\\' { i += 2; continue; }
if c == q { in_quote = None; }
}
None => match c {
b'"' | b'\'' => in_quote = Some(c),
b'{' | b',' => last_split = Some(i),
_ => {}
}
}
i += 1;
}
let mut p = last_split.map(|x| x + 1).unwrap_or(0);
while p < bytes.len() && (bytes[p] as char).is_whitespace() {
p += 1;
}
p.min(cursor)
}
fn label_grouping_target_start(before: &str, cursor: usize) -> usize {
let bytes = before.as_bytes();
let mut last_split: Option<usize> = None;
for (i, &c) in bytes.iter().enumerate() {
if c == b'(' || c == b',' { last_split = Some(i); }
}
let mut p = last_split.map(|x| x + 1).unwrap_or(0);
while p < bytes.len() && (bytes[p] as char).is_whitespace() {
p += 1;
}
p.min(cursor)
}
fn bracket_inner_segment(before: &str) -> &str {
let bytes = before.as_bytes();
let mut depth: i32 = 0;
let mut last_open: Option<usize> = None;
for (i, &c) in bytes.iter().enumerate() {
match c {
b'[' => { depth += 1; if depth == 1 { last_open = Some(i); } }
b']' => { depth -= 1; }
_ => {}
}
}
match last_open {
Some(p) if depth > 0 => &before[p + 1..],
_ => "",
}
}
fn last_significant_char(before: &str) -> Option<char> {
let bytes = before.as_bytes();
let mut i = bytes.len();
while i > 0 {
let c = bytes[i - 1] as char;
if c.is_whitespace() || is_grammar_ident_char(c) {
i -= 1;
} else {
return Some(c);
}
}
None
}
fn enclosing_function_call(before: &str) -> Option<(&str, usize)> {
let bytes = before.as_bytes();
let mut depth: i32 = 0;
let mut commas: usize = 0;
let mut paren_pos: Option<usize> = None;
let mut in_quote: Option<u8> = None;
for (i, &c) in bytes.iter().enumerate() {
match in_quote {
Some(q) => {
if c == b'\\' { }
if c == q { in_quote = None; }
}
None => match c {
b'"' | b'\'' => in_quote = Some(c),
b'(' => {
depth += 1;
if depth == 1 {
paren_pos = Some(i);
commas = 0;
}
}
b')' => {
depth -= 1;
if depth == 0 { paren_pos = None; commas = 0; }
}
b',' if depth == 1 => commas += 1,
_ => {}
}
}
}
let p = paren_pos?;
if depth == 0 { return None; }
let mut start = p;
while start > 0 && is_grammar_ident_char(bytes[start - 1] as char) {
start -= 1;
}
if start == p { return None; }
Some((&before[start..p], commas))
}
fn metric_name_before_brace(before: &str) -> Option<&str> {
let bytes = before.as_bytes();
let brace = bytes.iter().rposition(|&b| b == b'{')?;
let metric_end = brace;
let mut metric_start = metric_end;
while metric_start > 0 && is_label_char(bytes[metric_start - 1] as char) {
metric_start -= 1;
}
if metric_end == metric_start { return None; }
Some(&before[metric_start..metric_end])
}
fn current_quoted_prefix(before: &str) -> &str {
let bytes = before.as_bytes();
let mut i = bytes.len();
while i > 0 {
let c = bytes[i - 1];
if c == b'"' || c == b'\'' {
if i >= 2 && bytes[i - 2] == b'\\' { i -= 1; continue; }
return &before[i..];
}
i -= 1;
}
before
}
fn preceding_keyword(before: &str) -> Option<&str> {
let trimmed = before.trim_end_matches(|c: char| is_label_char(c));
let trimmed = trimmed.trim_end_matches(|c: char| {
c.is_whitespace() || c == '(' || c == ')' || c == ','
});
let last_word = trimmed
.rsplit(|c: char| {
c.is_whitespace() || c == '(' || c == ')' || c == ','
})
.next()?;
if last_word.is_empty() { return None; }
Some(last_word)
}
fn preceding_ident(before: &str) -> Option<&str> {
preceding_keyword(before)
}
fn is_aggregation_function(name: &str) -> bool {
matches!(
name,
"sum" | "min" | "max" | "avg" | "stddev" | "stdvar" | "count"
| "count_values" | "bottomk" | "topk" | "quantile" | "median"
| "group" | "limitk" | "any" | "geomean" | "histogram"
| "outliersk" | "mode" | "zscore" | "share"
)
}
#[inline]
fn is_label_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
#[cfg(test)]
mod tests {
use super::*;
struct StubCatalog;
impl MetricsqlCatalog for StubCatalog {
fn metric_names(&self, prefix: &str) -> Vec<String> {
["up", "node_cpu_seconds_total", "http_requests_total",
"process_cpu_seconds", "node_memory_MemAvailable_bytes"]
.iter()
.filter(|n| n.starts_with(prefix))
.map(|s| s.to_string())
.collect()
}
fn label_keys(&self, _metric: &str, prefix: &str) -> Vec<String> {
["job", "instance", "mode", "code", "method", "le"]
.iter()
.filter(|n| n.starts_with(prefix))
.map(|s| s.to_string())
.collect()
}
fn label_values(&self, _metric: &str, label: &str, prefix: &str) -> Vec<String> {
let pool: &[&str] = match label {
"job" => &["prometheus", "node_exporter", "api_gateway"],
"instance" => &["node-1:9100", "node-2:9100", "node-3:9100"],
"mode" => &["idle", "user", "system", "iowait", "irq", "softirq"],
"code" => &["200", "201", "301", "404", "500", "503"],
"method" => &["GET", "POST", "PUT", "DELETE", "PATCH"],
"le" => &["0.005", "0.01", "0.025", "0.05", "0.1", "0.25", "+Inf"],
_ => &[],
};
pool.iter()
.filter(|v| v.starts_with(prefix))
.map(|s| s.to_string())
.collect()
}
}
fn pp_at_end<'a>(line: &'a str) -> PartialParse<'a> {
pp_at_end_with_tap(line, 1)
}
fn pp_at_end_with_tap<'a>(line: &'a str, tap_count: u32) -> PartialParse<'a> {
PartialParse {
completed: vec![],
partial: "",
tree_path: vec![],
raw_line: line,
cursor_offset: line.len(),
tap_count,
}
}
fn run(line: &str) -> Vec<String> {
let cat = StubCatalog;
complete_metricsql(&cat, &pp_at_end(line))
}
#[test]
fn top_level_with_empty_prefix_tap1_offers_metrics_only() {
let out = run("");
assert!(out.iter().any(|s| s == "up"));
assert!(!out.iter().any(|s| s == "rate("),
"tap 1 with empty prefix should hide functions: {:?}",
out.iter().take(5).collect::<Vec<_>>());
}
#[test]
fn top_level_with_empty_prefix_tap2_adds_functions() {
let line = "";
let pp = pp_at_end_with_tap(line, 2);
let cat = StubCatalog;
let out = complete_metricsql(&cat, &pp);
assert!(out.iter().any(|s| s == "up"),
"metrics still present at tap 2");
assert!(out.iter().any(|s| s == "rate("),
"functions land with `(` at tap 2: {:?}",
out.iter().filter(|s| s.starts_with("rate")).collect::<Vec<_>>());
assert!(out.iter().any(|s| s == "histogram_quantile("));
}
#[test]
fn top_level_with_prefix_shows_both_immediately() {
let out = run("hist");
assert!(out.iter().any(|s| s == "histogram_quantile("));
assert!(out.iter().any(|s| s == "histogram_over_time("));
assert!(!out.iter().any(|s| s == "rate("),
"non-matching candidate should be filtered: {:?}", out);
}
#[test]
fn metric_name_prefix_returns_only_matches() {
let out = run("node_");
assert!(out.iter().any(|s| s == "node_cpu_seconds_total"));
assert!(out.iter().any(|s| s == "node_memory_MemAvailable_bytes"));
assert!(!out.iter().any(|s| s == "up"));
}
#[test]
fn inside_brace_offers_label_keys_with_each_operator() {
let out = run("up{");
for op in ["=", "!=", "=~", "!~"] {
for k in ["job", "instance"] {
let want = format!("up{{{k}{op}");
assert!(out.iter().any(|s| s == &want),
"missing `{want}` in candidates: {:?}", out);
}
}
}
#[test]
fn inside_brace_with_partial_filters_keys_to_one() {
let out = run("up{ins");
let want_eq = "up{instance=".to_string();
let want_ne = "up{instance!=".to_string();
let want_re = "up{instance=~".to_string();
let want_nre = "up{instance!~".to_string();
assert!(out.contains(&want_eq), "missing `=` variant: {:?}", out);
assert!(out.contains(&want_ne), "missing `!=` variant: {:?}", out);
assert!(out.contains(&want_re), "missing `=~` variant: {:?}", out);
assert!(out.contains(&want_nre), "missing `!~` variant: {:?}", out);
assert!(!out.iter().any(|s| s.contains("job")), "{:?}", out);
}
#[test]
fn inside_brace_after_first_matcher_still_offers_keys() {
let out = run("up{job=\"prometheus\", ");
assert!(out.iter().any(|s| s == "instance="));
assert!(out.iter().any(|s| s == "mode="));
assert!(out.iter().any(|s| s == "instance=~"));
}
#[test]
fn after_eq_offers_quoted_label_values() {
let out = run("up{job=");
assert!(out.iter().any(|s| s == "up{job=\"prometheus\""));
assert!(out.iter().any(|s| s == "up{job=\"node_exporter\""));
}
#[test]
fn inside_open_quote_offers_bare_label_values() {
let out = run("up{job=\"prom");
assert!(out.iter().any(|s| s == "up{job=\"prometheus"));
assert!(!out.iter().any(|s| s.ends_with("node_exporter")));
}
#[test]
fn label_value_for_specific_label_only() {
let out = run("up{mode=");
assert!(out.iter().any(|s| s == "up{mode=\"idle\""));
assert!(!out.iter().any(|s| s.ends_with("\"prometheus\"")));
}
#[test]
fn http_request_with_code_label() {
let out = run("http_requests_total{code=");
assert!(out.iter().any(|s| s == "http_requests_total{code=\"200\""));
assert!(out.iter().any(|s| s == "http_requests_total{code=\"500\""));
}
#[test]
fn inside_quote_for_method() {
let out = run("http_requests_total{method=\"P");
assert!(out.iter().any(|s| s == "http_requests_total{method=\"POST"));
assert!(out.iter().any(|s| s == "http_requests_total{method=\"PUT"));
assert!(out.iter().any(|s| s == "http_requests_total{method=\"PATCH"));
assert!(!out.iter().any(|s| s.ends_with("GET")));
}
#[test]
fn inside_bracket_offers_time_units_with_bash_prefix() {
let out = run("rate(http_requests_total[");
assert!(out.iter().any(|s| s == "rate(http_requests_total[s"),
"candidate must include bash-preserved prefix: {:?}", out);
assert!(out.iter().any(|s| s == "rate(http_requests_total[m"));
assert!(out.iter().any(|s| s == "rate(http_requests_total[h"));
}
#[test]
fn time_unit_prefix_filter_keeps_digits() {
let out = run("rate(http_requests_total[5");
assert!(out.iter().any(|s| s == "rate(http_requests_total[5m"));
assert!(out.iter().any(|s| s == "rate(http_requests_total[5h"));
}
#[test]
fn after_aggregation_offers_modifiers() {
let out = run("sum ");
assert!(out.iter().any(|s| s == "by"));
assert!(out.iter().any(|s| s == "without"));
}
#[test]
fn after_by_offers_label_keys() {
let out = run("sum by (");
assert!(out.iter().any(|s| s == "(job"));
assert!(out.iter().any(|s| s == "(instance"));
}
#[test]
fn after_offset_offers_time_units() {
let out = run("rate(http_requests_total[5m]) offset ");
assert!(out.iter().any(|s| s == "m"));
assert!(out.iter().any(|s| s == "h"));
}
#[test]
fn realistic_complex_query_inside_label_value() {
let line = "sum by (instance) (rate(node_cpu_seconds_total{mode=\"i";
let out = run(line);
let prefix = "(rate(node_cpu_seconds_total{mode=\"";
assert!(out.iter().any(|s| s == &format!("{prefix}idle")));
assert!(out.iter().any(|s| s == &format!("{prefix}iowait")));
assert!(out.iter().any(|s| s == &format!("{prefix}irq")));
assert!(!out.iter().any(|s| s.ends_with("user")),
"non-matching value should be filtered: {:?}", out);
}
#[test]
fn realistic_complex_query_label_keys_after_comma() {
let out = run("up{job=\"prometheus\", instance=\"node-1:9100\", ");
assert!(out.iter().any(|s| s == "mode="));
}
#[test]
fn histogram_quantile_outer_function_lands_at_paren() {
let out = run("histogram_q");
assert_eq!(out, vec![
"histogram_quantile(".to_string(),
"histogram_quantile()".to_string(),
]);
}
#[test]
fn label_value_inside_regex_match() {
let out = run("up{job=~\"prom");
assert!(out.iter().any(|s| s == "up{job=~\"prometheus"));
}
#[test]
fn nested_function_with_metric_name() {
let out = run("irate(node_");
assert!(out.iter().any(|s| s == "irate(node_cpu_seconds_total"));
}
fn engine_run(line: &str) -> Vec<String> {
use std::sync::Arc;
use crate::{CommandTree, Node, complete_at_tap_with_raw};
let tree = CommandTree::new("metricsql")
.command("query", Node::leaf(&[]))
.with_metricsql_at(&["query"], Arc::new(StubCatalog));
let cursor = line.len();
let words: Vec<String> = line.split_whitespace().map(|s| s.to_string()).collect();
let words_ref: Vec<&str> = words.iter().map(|s| s.as_str()).collect();
complete_at_tap_with_raw(&tree, &words_ref, 1, line, cursor)
}
fn apply_substitution(line: &str, candidate: &str) -> String {
let cursor = line.len();
let pp = pp_at_end(line);
let bws = pp.shell_word_start();
let mut out = String::with_capacity(line.len() + candidate.len());
out.push_str(&line[..bws]);
out.push_str(candidate);
out.push_str(&line[cursor..]);
out
}
#[test]
fn substitution_inside_brace_keeps_metric_prefix_and_adds_operator() {
let line = "up{";
let cands = run(line);
let job_eq = cands.iter().find(|s| s.as_str() == "up{job=")
.expect("expected `up{job=` candidate");
assert_eq!(apply_substitution(line, job_eq), "up{job=",
"selecting `{job_eq}` from `{line}` should yield `up{{job=`");
}
#[test]
fn substitution_inside_bracket_keeps_metric_prefix() {
let line = "rate(http_requests_total[";
let cands = run(line);
let m_cand = cands.iter().find(|s| s.ends_with("[m")).expect("expected a `[m` candidate");
assert_eq!(
apply_substitution(line, m_cand),
"rate(http_requests_total[m",
"substitution should preserve the function call + metric + `[`",
);
}
#[test]
fn substitution_with_partial_unit_digit_keeps_5() {
let line = "rate(http_requests_total[5";
let cands = run(line);
let m_cand = cands.iter().find(|s| s.ends_with("[5m")).expect("expected a `[5m` candidate");
assert_eq!(
apply_substitution(line, m_cand),
"rate(http_requests_total[5m",
"substitution should keep the `5` digit prefix",
);
}
#[test]
fn substitution_after_eq_inserts_quoted_value() {
let line = "up{job=";
let cands = run(line);
let prom = cands.iter().find(|s| *s == "up{job=\"prometheus\"")
.expect("expected splice-ready quoted value");
assert_eq!(apply_substitution(line, prom), "up{job=\"prometheus\"");
}
#[test]
fn substitution_inside_open_quote_preserves_partial_value() {
let line = "up{job=\"prom";
let cands = run(line);
let prom = cands.iter().find(|s| *s == "up{job=\"prometheus")
.expect("expected splice-ready bare value");
assert_eq!(apply_substitution(line, prom), "up{job=\"prometheus");
}
#[test]
fn multi_matcher_label_value_then_label_key() {
let line1 = "up{job=";
let cands1 = run(line1);
let pick1 = cands1.iter().find(|s| *s == "up{job=\"prometheus\"").unwrap();
let after1 = apply_substitution(line1, pick1);
assert_eq!(after1, "up{job=\"prometheus\"");
let line2 = format!("{after1}, ");
let cands2 = run(&line2);
assert!(cands2.iter().any(|s| s == "instance="),
"after `, ` should offer operator-appended label keys: {:?}", cands2);
let after2 = apply_substitution(&line2, "instance=");
assert_eq!(after2, "up{job=\"prometheus\", instance=");
let cands3 = run(&after2);
assert!(cands3.iter().any(|s| *s == "instance=\"node-1:9100\""),
"after `instance=` should offer splice-ready instance values: {:?}", cands3);
}
#[test]
fn nested_function_completions_compose() {
let line1 = "histogram_q";
let cands1 = run(line1);
assert_eq!(cands1, vec![
"histogram_quantile(".to_string(),
"histogram_quantile()".to_string(),
]);
assert_eq!(
apply_substitution(line1, "histogram_quantile("),
"histogram_quantile(",
);
let line2 = "histogram_quantile(0.95, rate(http_";
let cands2 = run(line2);
let want = "rate(http_requests_total";
assert!(cands2.iter().any(|s| s == want),
"expected metric: {:?}", cands2);
let pick = cands2.iter().find(|s| *s == want).unwrap();
assert_eq!(
apply_substitution(line2, pick),
"histogram_quantile(0.95, rate(http_requests_total",
);
let line3 = "histogram_quantile(0.95, rate(http_requests_total[";
let cands3 = run(line3);
let pick = cands3.iter()
.find(|s| s.ends_with("[m"))
.expect("expected a `[m` candidate");
assert!(apply_substitution(line3, pick).ends_with("[m"));
}
#[test]
fn aggregation_with_by_and_inner_rate() {
let cands = run("sum ");
assert!(cands.iter().any(|s| s == "by"));
let line = "sum by (";
let cands = run(line);
assert!(cands.iter().any(|s| s == "(instance"));
assert_eq!(apply_substitution(line, "(instance"), "sum by (instance");
let line = "sum by (instance) (rate(node_cpu_seconds_total{";
let cands = run(line);
let pick = cands.iter().find(|s| s.ends_with("{mode=")).expect("expected mode= variant");
assert_eq!(
apply_substitution(line, pick),
"sum by (instance) (rate(node_cpu_seconds_total{mode=",
);
}
#[test]
fn regex_match_inside_open_quote_offers_values() {
let line = "up{job=~\"prom";
let cands = run(line);
let want = "up{job=~\"prometheus";
assert!(cands.iter().any(|s| s == want));
let pick = cands.iter().find(|s| *s == want).unwrap();
assert_eq!(apply_substitution(line, pick), "up{job=~\"prometheus");
}
#[test]
fn escaped_quote_inside_string_doesnt_close_value() {
let line = "up{job=\"abc\\\"d";
let pp = pp_at_end(line);
let bs = pp.bracket_state();
assert_eq!(bs.inside_quote, Some('"'),
"escaped \\\" should not close the string: {:?}", bs);
}
#[test]
fn engine_path_returns_shell_correct_candidates_for_label_keys() {
let cands = engine_run("metricsql query up{");
assert!(cands.iter().any(|s| s == "up{job="),
"engine path must produce shell-correct candidates: {:?}", cands);
}
#[test]
fn engine_path_substitution_round_trip_inside_brace() {
let line = "metricsql query up{ins";
let cands = engine_run(line);
let pick = cands.iter().find(|s| s.as_str() == "up{instance=")
.expect("expected up{instance=");
let pp = pp_at_end(line);
let sws = pp.shell_word_start();
let final_line = format!("{}{}", &line[..sws], pick);
assert_eq!(final_line, "metricsql query up{instance=");
}
#[test]
fn engine_path_round_trip_inside_bracket() {
let line = "metricsql query rate(http_requests_total[5";
let cands = engine_run(line);
let pick = cands.iter().find(|s| s.ends_with("[5m")).expect("expected ...[5m");
let pp = pp_at_end(line);
let bws = pp.shell_word_start();
let final_line = format!("{}{}", &line[..bws], pick);
assert_eq!(
final_line,
"metricsql query rate(http_requests_total[5m",
);
}
#[test]
fn bracket_state_balances_through_escaped_special_chars() {
let line = "up{job=\"value with } brace inside\"";
let pp = pp_at_end(line);
let bs = pp.bracket_state();
assert_eq!(bs.brace, 1, "open brace count should be 1: {:?}", bs);
assert_eq!(bs.inside_quote, None, "string is closed");
}
#[test]
fn shell_wrapped_single_quote_brace_offers_label_keys() {
let cands = engine_run("metricsql query '{");
assert!(cands.iter().any(|s| s.ends_with("{job=")),
"shell-wrapped `'{{` should offer label keys: {:?}", cands);
}
#[test]
fn shell_wrapped_double_quote_brace_offers_label_keys() {
let cands = engine_run("metricsql query \"{");
assert!(cands.iter().any(|s| s.ends_with("{job=")),
"shell-wrapped `\"{{` should offer label keys: {:?}", cands);
}
#[test]
fn shell_wrapped_with_metric_offers_label_keys_for_that_metric() {
let cands = engine_run("metricsql query 'up{");
assert!(cands.iter().any(|s| s.ends_with("{job=")));
assert!(cands.iter().any(|s| s.ends_with("{instance=")));
}
#[test]
fn shell_wrapped_top_of_expression_offers_metrics() {
let cands = engine_run("metricsql query '");
assert!(cands.iter().any(|s| s == "'up"),
"should offer splice-ready metric names: {:?}",
cands.iter().take(10).collect::<Vec<_>>());
assert!(!cands.iter().any(|s| s.ends_with("rate(")),
"tap 1 with empty prefix shouldn't show functions: {:?}", cands);
}
#[test]
fn label_keys_offer_all_four_match_operators() {
let cands = run("up{");
for k in ["job", "instance", "mode"] {
for op in ["=", "!=", "=~", "!~"] {
let want = format!("up{{{k}{op}");
assert!(cands.contains(&want), "missing `{want}`: {:?}", cands);
}
}
}
#[test]
fn after_neq_offers_label_values() {
let cands = run("up{job!=");
assert!(cands.iter().any(|s| s == "up{job!=\"prometheus\""),
"expected splice-ready prefixed candidate: {:?}", cands);
}
#[test]
fn after_regex_neq_offers_label_values() {
let cands = run("up{job!~");
assert!(cands.iter().any(|s| s == "up{job!~\"prometheus\""),
"expected prefixed candidate: {:?}", cands);
}
#[test]
fn subquery_inside_bracket_after_colon_offers_time_units() {
let cands = run("rate(up[5m:");
assert!(cands.iter().any(|s| s == "rate(up[5m:m"),
"subquery step position should offer splice-ready time units: {:?}", cands);
assert!(cands.iter().any(|s| s == "rate(up[5m:h"));
}
#[test]
fn after_range_unit_offers_subquery_colon() {
let cands = run("rate(up[5m");
assert!(cands.iter().any(|s| s.ends_with(":")),
"should offer `:` to start subquery step: {:?}", cands);
}
#[test]
fn after_at_modifier_offers_start_end() {
let cands = run("rate(up[5m]) @ ");
assert!(cands.iter().any(|s| s == "start()"));
assert!(cands.iter().any(|s| s == "end()"));
}
#[test]
fn after_at_with_partial_filters() {
let cands = run("rate(up[5m]) @ s");
assert!(cands.iter().any(|s| s == "start()"));
assert!(!cands.iter().any(|s| s == "end()"));
}
#[test]
fn after_comparison_operator_offers_bool_modifier() {
let cands = run("up > ");
assert!(cands.iter().any(|s| s == "bool"),
"after `>` should offer `bool` modifier: {:?}",
cands.iter().take(10).collect::<Vec<_>>());
}
#[test]
fn after_on_offers_label_keys() {
let cands = run("a + on(");
assert!(cands.iter().any(|s| s == "on(job"),
"after `on(` should offer label keys: {:?}", cands);
}
#[test]
fn after_ignoring_offers_label_keys() {
let cands = run("a + ignoring(");
assert!(cands.iter().any(|s| s == "ignoring(job"));
}
#[test]
fn after_group_left_offers_label_keys() {
let cands = run("a + on(job) group_left(");
assert!(cands.iter().any(|s| s == "group_left(instance"));
}
#[test]
fn after_close_paren_offers_aggregation_modifiers_and_binops() {
let cands = run("sum(rate(up[5m])) ");
assert!(cands.iter().any(|s| s == "by"),
"after `)` should offer `by`: {:?}",
cands.iter().take(15).collect::<Vec<_>>());
assert!(cands.iter().any(|s| s == "without"));
assert!(cands.iter().any(|s| s == "+"));
assert!(cands.iter().any(|s| s == "and"));
assert!(cands.iter().any(|s| s == "keep_metric_names"));
}
#[test]
fn after_close_brace_offers_binops_and_modifiers() {
let cands = run("up{job=\"prometheus\"} ");
assert!(cands.iter().any(|s| s == "+"));
assert!(cands.iter().any(|s| s == "and"));
assert!(cands.iter().any(|s| s == "offset"));
assert!(cands.iter().any(|s| s == "["));
}
#[test]
fn after_close_range_bracket_offers_binops_and_modifiers() {
let _cands = run("rate(up[5m]) ");
let cands = run("up[5m] ");
assert!(cands.iter().any(|s| s == "offset"));
assert!(cands.iter().any(|s| s == "@"));
let _ = cands;
}
#[test]
fn histogram_quantile_first_arg_returns_no_metric_names() {
let cands = run("histogram_quantile(");
assert!(!cands.iter().any(|s| s.contains("up")),
"scalar-first-arg position must not offer metric names: {:?}",
cands.iter().take(10).collect::<Vec<_>>());
}
#[test]
fn histogram_quantile_second_arg_offers_full_expression_set() {
let line = "histogram_quantile(0.95, ";
let cat = StubCatalog;
let pp1 = pp_at_end_with_tap(line, 1);
let pp2 = pp_at_end_with_tap(line, 2);
let cands1 = complete_metricsql(&cat, &pp1);
let cands2 = complete_metricsql(&cat, &pp2);
assert!(cands1.iter().any(|s| s == "http_requests_total"),
"tap 1 vector arg should offer metric names: {:?}", cands1);
assert!(!cands1.iter().any(|s| s == "rate("),
"tap 1 should not yet offer functions");
assert!(cands2.iter().any(|s| s == "rate("),
"tap 2 vector arg should add functions (with `(`): {:?}", cands2);
}
#[test]
fn topk_first_arg_is_scalar() {
let cands = run("topk(");
assert!(!cands.iter().any(|s| s.contains("up")),
"topk first arg should be silent: {:?}", cands);
}
#[test]
fn shell_wrapped_inside_label_value_offers_values() {
let cands = engine_run("metricsql query 'up{job=\"prom");
assert!(cands.iter().any(|s| s == "'up{job=\"prometheus"),
"shell-wrapped expression should still complete label values: {:?}", cands);
}
#[test]
fn user_walkthrough_function_partial_lands_at_open_paren() {
let cands = engine_run("metricsql query 'avg_");
let pick = cands.iter().find(|s| s.ends_with("avg_over_time("))
.unwrap_or_else(|| panic!("expected avg_over_time(: {:?}", cands));
let line = "metricsql query 'avg_";
let pp = pp_at_end(line);
let sws = pp.shell_word_start();
let final_line = format!("{}{}", &line[..sws], pick);
assert_eq!(final_line, "metricsql query 'avg_over_time(",
"function suggestion must land at `(`, not at the bare name");
}
#[test]
fn user_walkthrough_inside_func_call_tap1_metrics_only() {
let cands = engine_run("metricsql query 'avg_over_time(");
assert!(cands.iter().any(|s| s.ends_with("up")),
"tap 1 should offer metric names: {:?}",
cands.iter().take(10).collect::<Vec<_>>());
assert!(!cands.iter().any(|s| s.ends_with("rate(")),
"tap 1 should not yet offer functions: {:?}",
cands.iter().filter(|s| s.contains("rate")).collect::<Vec<_>>());
}
#[test]
fn user_walkthrough_inside_func_call_tap2_adds_inner_functions() {
use std::sync::Arc;
use crate::{CommandTree, Node, complete_at_tap_with_raw};
let tree = CommandTree::new("metricsql")
.command("query", Node::leaf(&[]))
.with_metricsql_at(&["query"], Arc::new(StubCatalog));
let line = "metricsql query 'avg_over_time(";
let cursor = line.len();
let words: Vec<String> = line.split_whitespace().map(|s| s.to_string()).collect();
let words_ref: Vec<&str> = words.iter().map(|s| s.as_str()).collect();
let cands_t1 = complete_at_tap_with_raw(&tree, &words_ref, 1, line, cursor);
let cands_t2 = complete_at_tap_with_raw(&tree, &words_ref, 2, line, cursor);
assert!(!cands_t1.iter().any(|s| s.ends_with("rate(")),
"tap 1 inside func call should be metrics only");
assert!(cands_t2.iter().any(|s| s.ends_with("rate(")),
"tap 2 should add inner functions for stacking: {:?}",
cands_t2.iter().filter(|s| s.contains("rate")).take(5).collect::<Vec<_>>());
assert!(cands_t2.iter().any(|s| s.ends_with("irate(")),
"tap 2 should include `irate(`");
assert!(cands_t2.iter().any(|s| s.ends_with("increase(")),
"tap 2 should include `increase(`");
}
#[test]
fn user_walkthrough_bare_brace_inside_func_call_offers_label_keys() {
let cands = engine_run("metricsql query 'avg_over_time({");
assert!(cands.iter().any(|s| s.ends_with("{job=")),
"bare-brace should offer label keys with operators: {:?}",
cands.iter().filter(|s| s.contains("job")).collect::<Vec<_>>());
assert!(cands.iter().any(|s| s.ends_with("{instance=")));
assert!(cands.iter().any(|s| s.ends_with("{job!=")),
"should also offer the `!=` variant");
}
#[test]
fn user_walkthrough_function_substitution_round_trip() {
let line = "metricsql query 'avg_";
let cands = engine_run(line);
let pick = cands.iter().find(|s| s.ends_with("avg_over_time(")).unwrap();
let pp = pp_at_end(line);
let sws = pp.shell_word_start();
assert_eq!(
format!("{}{}", &line[..sws], pick),
"metricsql query 'avg_over_time(",
);
}
#[test]
fn full_metric_match_emits_continuations_on_tap1() {
let cands = engine_run("metricsql query 'abs(http_requests_total");
assert!(cands.len() >= 2,
"full match must emit continuations as multi-candidate: {cands:?}");
let want_bare = "'abs(http_requests_total";
let want_open_labels = "'abs(http_requests_total{";
let want_open_range = "'abs(http_requests_total[";
let want_close_func = "'abs(http_requests_total)";
let want_terminal = "'abs(http_requests_total)'";
for want in [want_bare, want_open_labels, want_open_range,
want_close_func, want_terminal] {
assert!(cands.iter().any(|s| s == want),
"missing continuation `{want}`: {cands:?}");
}
}
#[test]
fn partial_metric_match_with_multiple_matches_is_just_names() {
let cands = engine_run("metricsql query 'node_");
assert!(cands.iter().any(|s| s == "'node_cpu_seconds_total"));
assert!(cands.iter().any(|s| s == "'node_memory_MemAvailable_bytes"));
assert!(!cands.iter().any(|s| s == "'node_cpu_seconds_total{"),
"multi-match tap 1 must NOT yet show `{{` continuation: {cands:?}");
}
#[test]
fn partial_metric_unique_match_emits_continuations_on_tap1() {
let cands = engine_run("metricsql query 'http_requests_to");
assert!(cands.iter().any(|s| s == "'http_requests_total"));
assert!(cands.iter().any(|s| s == "'http_requests_total{"),
"unique match must emit `{{` so common-prefix advances: {cands:?}");
assert!(cands.iter().any(|s| s == "'http_requests_total["));
assert!(cands.iter().any(|s| s == "'http_requests_total'"));
}
#[test]
fn partial_metric_multi_match_tap2_adds_continuations() {
use std::sync::Arc;
use crate::{CommandTree, Node, complete_at_tap_with_raw};
let tree = CommandTree::new("metricsql")
.command("query", Node::leaf(&[]))
.with_metricsql_at(&["query"], Arc::new(StubCatalog));
let line = "metricsql query 'node_";
let cursor = line.len();
let words: Vec<String> = line.split_whitespace().map(|s| s.to_string()).collect();
let words_ref: Vec<&str> = words.iter().map(|s| s.as_str()).collect();
let cands_t2 = complete_at_tap_with_raw(&tree, &words_ref, 2, line, cursor);
assert!(cands_t2.iter().any(|s| s == "'node_cpu_seconds_total{"),
"tap 2 multi-match must add `{{` continuation: {cands_t2:?}");
assert!(cands_t2.iter().any(|s| s == "'node_memory_MemAvailable_bytes{"));
}
#[test]
fn unique_function_match_emits_lcp_advancing_pair() {
let line = "metricsql query 'delt";
let cands = engine_run(line);
assert!(cands.iter().any(|s| s == "'delta("));
assert!(cands.iter().any(|s| s == "'delta()"));
let lcp = longest_common_prefix(&cands);
assert!(lcp.ends_with('('),
"LCP must reach the function-call entry point: {lcp:?}");
assert!(lcp.contains("delta("),
"LCP must include `delta(`: {lcp:?}");
}
#[test]
fn unique_function_match_in_func_arg_also_advances() {
let line = "metricsql query 'avg(rat";
let cands = engine_run(line);
assert!(cands.iter().any(|s| s == "'avg(rate("));
assert!(cands.iter().any(|s| s == "'avg(rate()"));
let lcp = longest_common_prefix(&cands);
assert!(lcp.ends_with("rate("),
"LCP must reach `rate(` entry point: {lcp:?}");
}
fn longest_common_prefix(strs: &[String]) -> String {
if strs.is_empty() { return String::new(); }
let first = &strs[0];
let mut end = first.len();
for s in &strs[1..] {
let cap = end.min(s.len());
let mut i = 0;
while i < cap && first.as_bytes()[i] == s.as_bytes()[i] { i += 1; }
end = i;
}
while end > 0 && !first.is_char_boundary(end) { end -= 1; }
first[..end].to_string()
}
#[test]
fn enforce_multi_candidate_skips_when_no_wrapper_open() {
let cands = run("delt");
assert_eq!(cands, vec!["delta(".to_string(), "delta()".to_string()],
"function emission pair fires regardless of wrapper: {cands:?}");
}
#[test]
fn open_string_label_value_offers_close_quote_continuations() {
let cands = engine_run("metricsql query 'up{job=\"prom");
assert!(cands.len() >= 2,
"open-string single match must suppress auto-close: {cands:?}");
for want in [
"'up{job=\"prometheus",
"'up{job=\"prometheus\"",
"'up{job=\"prometheus\"}",
"'up{job=\"prometheus\"}'",
] {
assert!(cands.iter().any(|s| s == want),
"missing close-string continuation `{want}`: {cands:?}");
}
}
#[test]
fn post_close_paren_offers_wrapper_terminal() {
let cands = engine_run("metricsql query 'sum(rate(up[5m])) ");
assert!(cands.iter().any(|s| s == "'"),
"post-`)` complete-expression should offer wrapper-close TERMINAL: {cands:?}");
}
#[test]
fn aggregation_function_inside_own_call_offers_top_of_expression() {
let cands = engine_run("metricsql query 'avg(");
assert!(cands.iter().any(|s| s == "'avg(up"),
"inside agg call must offer metric names, not by/without: {cands:?}");
assert!(cands.iter().any(|s| s == "'avg(http_requests_total"));
assert!(!cands.iter().any(|s| s == "'avg(by"),
"by/without is alternate-placement; not valid inside the call: {cands:?}");
}
#[test]
fn aggregation_function_alternate_placement_still_offers_modifiers() {
let cands = engine_run("metricsql query 'sum ");
assert!(cands.iter().any(|s| s == "by"));
assert!(cands.iter().any(|s| s == "without"));
}
#[test]
fn modifier_keyword_outside_paren_does_not_emit_label_keys() {
let cands = engine_run("metricsql query 'sum by ");
assert!(!cands.iter().any(|s| s == "job"),
"label keys must not be offered outside the modifier paren: {cands:?}");
assert!(!cands.iter().any(|s| s == "instance"),
"label keys must not be offered outside the modifier paren: {cands:?}");
}
#[test]
fn closed_value_pair_inside_brace_does_not_destroy_line() {
let cands = engine_run("metricsql query 'avg(http_requests{job=\"prometheus\"");
for cand in &cands {
assert!(!cand.ends_with("{job="),
"destructive splice candidate (truncates value): {cand:?}");
assert!(!cand.contains("{job=\"") || cand.contains("\"prometheus\""),
"candidate must preserve the existing closed value: {cand:?}");
}
let want_comma = "'avg(http_requests{job=\"prometheus\",";
let want_close = "'avg(http_requests{job=\"prometheus\"}";
let want_close_all = "'avg(http_requests{job=\"prometheus\"})";
let want_terminal = "'avg(http_requests{job=\"prometheus\"})'";
for want in [want_comma, want_close, want_close_all, want_terminal] {
assert!(cands.iter().any(|s| s == want),
"missing safe continuation `{want}`: {cands:?}");
}
}
#[test]
fn syntax_error_inside_brace_does_not_destroy_line() {
let cands = engine_run("metricsql query 'avg(http_requests_total{job=\"prometheus\")");
let line_word_after_sws = "'avg(http_requests_total{job=\"prometheus\")";
for cand in &cands {
assert!(cand.starts_with(line_word_after_sws),
"candidate must extend, not truncate: {cand:?}");
}
assert!(cands.iter().any(|s| s.ends_with(",")),
"must offer `,` continuation: {cands:?}");
assert!(cands.iter().any(|s| s.ends_with("}")),
"must offer `}}` continuation: {cands:?}");
}
#[test]
fn cursor_past_closed_wrapper_emits_nothing() {
let cands = engine_run("metricsql query 'avg(http_requests_total{job=\"prometheus\")'");
assert!(cands.is_empty(),
"past-closed-wrapper must yield no candidates: {cands:?}");
}
#[test]
fn complete_inner_inside_func_call_offers_close_paren_and_comma() {
let cands = engine_run("metricsql query 'avg(http_requests_total{job=\"prometheus\"}");
assert!(cands.iter().any(|s| s.ends_with(")")),
"must offer `)` close-func continuation: {cands:?}");
assert!(cands.iter().any(|s| s.ends_with(",")),
"must offer `,` next-varg continuation: {cands:?}");
assert!(cands.iter().any(|s| s.ends_with(")'")),
"must offer `)<wrapper>` TERMINAL: {cands:?}");
}
#[test]
fn after_comma_inside_brace_still_offers_label_keys() {
let cands = engine_run("metricsql query 'up{job=\"prometheus\", ");
assert!(cands.iter().any(|s| s == "instance="),
"after `, ` should offer fresh label keys: {cands:?}");
assert!(cands.iter().any(|s| s == "mode="));
}
#[test]
fn modifier_keyword_inside_paren_offers_label_keys() {
let cands = engine_run("metricsql query 'sum by(");
assert!(cands.iter().any(|s| s == "by(job"),
"expected splice-ready label key: {cands:?}");
assert!(cands.iter().any(|s| s == "by(instance"));
}
#[test]
fn metric_top_of_expression_offers_terminal_when_wrapped() {
let cands = engine_run("metricsql query 'up");
assert!(cands.iter().any(|s| s == "'up'"),
"expected TERMINAL `'up'` continuation: {cands:?}");
}
#[test]
fn fs_paths_lists_relative_entries() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join("alpha")).unwrap();
std::fs::create_dir(tmp.path().join("alpine")).unwrap();
std::fs::write(tmp.path().join("alfa.txt"), b"x").unwrap();
std::fs::write(tmp.path().join("zeta.txt"), b"x").unwrap();
let _guard = ChangeDirGuard::to(tmp.path());
let provider = fs_paths_provider();
let mut out = provider("al", &[]);
out.sort();
assert_eq!(out, vec!["alfa.txt", "alpha/", "alpine/"]);
}
#[test]
fn fs_dirs_only_directories() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir(tmp.path().join("data")).unwrap();
std::fs::write(tmp.path().join("readme"), b"x").unwrap();
let _guard = ChangeDirGuard::to(tmp.path());
let provider = fs_dirs_provider();
let out = provider("", &[]);
assert_eq!(out, vec!["data/"]);
}
#[test]
fn fs_paths_preserves_directory_prefix() {
let tmp = tempfile::tempdir().unwrap();
let base = tmp.path().join("profiles/base");
std::fs::create_dir_all(&base).unwrap();
std::fs::write(base.join("metadata_content.slab"), b"x").unwrap();
std::fs::write(base.join("base_vectors.fvec"), b"x").unwrap();
let _guard = ChangeDirGuard::to(tmp.path());
let provider = fs_paths_provider();
let mut out = provider("profiles/base/m", &[]);
out.sort();
assert_eq!(out, vec!["profiles/base/metadata_content.slab"]);
}
#[test]
fn fs_paths_hides_dotfiles_by_default() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join(".hidden"), b"x").unwrap();
std::fs::write(tmp.path().join("visible"), b"x").unwrap();
let _guard = ChangeDirGuard::to(tmp.path());
let provider = fs_paths_provider();
let out_plain = provider("", &[]);
assert_eq!(out_plain, vec!["visible"]);
let out_dot = provider(".", &[]);
assert!(out_dot.iter().any(|s| s == ".hidden"));
}
#[test]
fn no_op_provider_is_empty() {
let p = no_op_provider();
assert!(p("", &[]).is_empty());
assert!(p("anything", &["context"]).is_empty());
}
struct ChangeDirGuard {
prior: std::path::PathBuf,
_lock: std::sync::MutexGuard<'static, ()>,
}
impl ChangeDirGuard {
fn to(p: &std::path::Path) -> Self {
static CWD_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let lock = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prior = std::env::current_dir().unwrap();
std::env::set_current_dir(p).unwrap();
Self { prior, _lock: lock }
}
}
impl Drop for ChangeDirGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.prior);
}
}
#[test]
fn bracket_state_paren_inside_string_doesnt_count() {
let line = "rate(http_requests_total{label=\"value with (paren\"})";
let pp = pp_at_end(line);
let bs = pp.bracket_state();
assert_eq!(bs.paren, 0, "paren depth should be 0: {:?}", bs);
assert_eq!(bs.brace, 0, "brace depth should be 0: {:?}", bs);
}
}