Add src/dsl/sql_functions.rs (KNOWN_SQL_FUNCTIONS) as the shared source of truth at sql_expr_ident slots: - #15: offer the functions as Tab candidates under a new CandidateKind::Function + ninth Theme colour tok_function (blue, distinct from keyword/identifier/type). - #16: restore the column-typo flag the #6 fix had dropped wholesale — invalid_ident_at_cursor now bails only when the partial prefix-matches a known function, else falls through to the schema-column check. A column named like a function (e.g. `count`) is deduped (column wins). `cast` is excluded — CAST(x AS type) is not a plain-call shape. The no-validation-allowlist posture stands: the list drives completion + the typo hint only, never parse-time acceptance. Docs: ADR-0022 Amendment 6, ADR-0031 status note, README index, requirements I3/I4 + refreshed test baseline.
This commit is contained in:
+228
-9
@@ -228,6 +228,12 @@ pub enum CandidateKind {
|
||||
/// expects either `values` or `(`, and surfacing both makes
|
||||
/// the Form A path discoverable.
|
||||
Punct,
|
||||
/// A curated SQL function name (ADR-0022 Amendment 6, issue
|
||||
/// #15) offered at a `sql_expr_ident` slot. Coloured with
|
||||
/// `tok_function` so a learner can tell `sum` / `upper` apart
|
||||
/// from a column reference or a clause keyword. The set is
|
||||
/// `crate::dsl::sql_functions::KNOWN_SQL_FUNCTIONS`.
|
||||
Function,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -682,6 +688,27 @@ pub fn candidates_at_cursor_with_in_mode(
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Source 1.8: SQL function-name candidates (ADR-0022 Amendment 6,
|
||||
// issue #15). At a `sql_expr_ident` slot the grammar accepts a
|
||||
// column reference *or* the leading name of a function call
|
||||
// (ADR-0031 §1); the column candidates come from the schema
|
||||
// (Source 2), and these surface the curated known-function set so
|
||||
// a learner can discover `sum` / `upper` / … by Tab. Prefix-
|
||||
// filtered like every other source; empty prefix offers the whole
|
||||
// set. Tagged `CandidateKind::Function` for its own colour.
|
||||
let has_sql_expr_slot = expected.iter().any(|e| {
|
||||
matches!(e, Expectation::Ident { role: "sql_expr_ident", .. })
|
||||
});
|
||||
let mut functions: Vec<String> = if has_sql_expr_slot {
|
||||
crate::dsl::sql_functions::KNOWN_SQL_FUNCTIONS
|
||||
.iter()
|
||||
.filter(|f| matches_prefix(f))
|
||||
.map(|f| (*f).to_string())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Source 2: schema identifiers — accumulated across every
|
||||
// matching schema-listable `Ident { source }` expectation.
|
||||
// `NewName` / `Types` / `Free` sources don't query the
|
||||
@@ -725,6 +752,16 @@ pub fn candidates_at_cursor_with_in_mode(
|
||||
// resolving collisions in the user's favour would create
|
||||
// ambiguity in the live render.
|
||||
identifiers.retain(|name| !keywords.contains(name));
|
||||
// If a known function name collides with one of the user's own
|
||||
// columns (a column literally named `count` / `date` / …), the
|
||||
// column wins and the function candidate is dropped — otherwise the
|
||||
// same text would appear twice in the candidate line, distinguished
|
||||
// only by colour. Same spirit as the keyword-wins rule above: a
|
||||
// schema name is the user's content and the more relevant
|
||||
// completion; a bare `count` accepted as the column still becomes a
|
||||
// call the moment the user types `(`. Case-insensitive so `Count`
|
||||
// (column) also suppresses `count` (function). (DA review, #15.)
|
||||
functions.retain(|f| !identifiers.iter().any(|i| i.eq_ignore_ascii_case(f)));
|
||||
|
||||
// Schema identifiers first: a column / table name the user
|
||||
// would otherwise have to look up is the highest-value
|
||||
@@ -738,6 +775,7 @@ pub fn candidates_at_cursor_with_in_mode(
|
||||
identifiers.len()
|
||||
+ keywords.len()
|
||||
+ type_names.len()
|
||||
+ functions.len()
|
||||
+ composites.len()
|
||||
+ punct_candidates.len()
|
||||
+ flags.len(),
|
||||
@@ -762,6 +800,14 @@ pub fn candidates_at_cursor_with_in_mode(
|
||||
kind: CandidateKind::Keyword,
|
||||
mode: ModeClass::Both,
|
||||
}));
|
||||
// Function names sit after keywords/types: a learner reads the
|
||||
// clause keywords first, then discovers callables. Their own
|
||||
// `Function` kind drives the `tok_function` colour (issue #15).
|
||||
candidates.extend(functions.into_iter().map(|text| Candidate {
|
||||
text,
|
||||
kind: CandidateKind::Function,
|
||||
mode: ModeClass::Both,
|
||||
}));
|
||||
candidates.extend(composites.into_iter().map(|text| Candidate {
|
||||
text,
|
||||
kind: CandidateKind::Keyword,
|
||||
@@ -1134,19 +1180,23 @@ pub fn invalid_ident_at_cursor_in_mode(
|
||||
if expected.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Issue #6 follow-on: at a SQL expression position the partial
|
||||
// could resolve to either a column reference *or* a function-call
|
||||
// name (the grammar admits the call shape structurally — see
|
||||
// Issue #6 / #16: at a SQL expression position the partial could
|
||||
// resolve to either a column reference *or* a function-call name
|
||||
// (the grammar admits the call shape structurally — see
|
||||
// sql_expr.rs's `CALL_ARGS` comment, "it does not know which
|
||||
// names are aggregates"). Without lookahead for a trailing `(`,
|
||||
// we can't tell here, so we don't flag — submit-time validation
|
||||
// still catches a genuine column typo via the `unknown_column`
|
||||
// diagnostic, which only skips when the ident *is* followed by
|
||||
// `(` (i.e. it really is a function call).
|
||||
// names are aggregates"). Issue #6 dropped the flag here wholesale
|
||||
// to stop the false positive on names like `sum`; issue #16
|
||||
// restores it for genuine typos using the curated known-function
|
||||
// list. When the partial prefix-matches a known function name we
|
||||
// still bail (it may yet become a function call); otherwise we
|
||||
// fall through to the schema-column check below, which flags the
|
||||
// partial as "no such column" unless it prefix-matches a real
|
||||
// column. So `select Agx` warns at typing time again, while
|
||||
// `select sum` does not.
|
||||
let has_sql_expr_slot = expected.iter().any(|e| {
|
||||
matches!(e, Expectation::Ident { role: "sql_expr_ident", .. })
|
||||
});
|
||||
if has_sql_expr_slot {
|
||||
if has_sql_expr_slot && crate::dsl::sql_functions::is_known_function_prefix(partial) {
|
||||
return None;
|
||||
}
|
||||
// Find every schema-listable source in the expected list.
|
||||
@@ -2365,6 +2415,175 @@ mod tests {
|
||||
|
||||
// ---- ADR-0032 §10.5 qualified-prefix completion ----
|
||||
|
||||
// ---- SQL function-name completion (issue #15) ----
|
||||
|
||||
#[test]
|
||||
fn sql_expr_slot_offers_known_function_candidates() {
|
||||
// Issue #15: at a `sql_expr_ident` slot Tab offers the curated
|
||||
// function names so a learner can discover them.
|
||||
let cache = two_table_schema();
|
||||
let cs = cands_with("select * from a where ", 22, &cache);
|
||||
for f in ["sum", "avg", "count", "upper", "coalesce"] {
|
||||
assert!(
|
||||
cs.contains(&f.to_string()),
|
||||
"expected function `{f}` offered at WHERE expr slot; got {cs:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_slot_offers_known_function_candidates() {
|
||||
// Issue #15's headline example: at `select ` (the projection
|
||||
// slot, empty prefix) the functions must surface alongside
|
||||
// columns — the projection slot is also `sql_expr_ident`, and
|
||||
// it must not be swallowed by the value-literal suppression
|
||||
// (it carries a schema-ident expectation, so it isn't).
|
||||
let cache = two_table_schema();
|
||||
let cs = cands_with("select ", 7, &cache);
|
||||
for f in ["sum", "count", "upper"] {
|
||||
assert!(
|
||||
cs.contains(&f.to_string()),
|
||||
"expected function `{f}` offered at the projection slot; got {cs:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_function_candidates_filter_by_prefix() {
|
||||
// `su` narrows to the functions starting `su` — `sum`,
|
||||
// `substr` — and excludes non-matches.
|
||||
let cache = two_table_schema();
|
||||
let input = "select * from a where su";
|
||||
let cs = cands_with(input, input.len(), &cache);
|
||||
assert!(cs.contains(&"sum".to_string()), "got {cs:?}");
|
||||
assert!(cs.contains(&"substr".to_string()), "got {cs:?}");
|
||||
assert!(
|
||||
!cs.contains(&"avg".to_string()),
|
||||
"`avg` does not prefix-match `su`; got {cs:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_function_candidates_carry_function_kind() {
|
||||
// The hint panel colours functions via their own kind.
|
||||
let cache = two_table_schema();
|
||||
let input = "select * from a where su";
|
||||
let cands = candidates_at_cursor(input, input.len(), &cache)
|
||||
.expect("some completion")
|
||||
.candidates;
|
||||
let sum = cands
|
||||
.iter()
|
||||
.find(|c| c.text == "sum")
|
||||
.expect("`sum` present");
|
||||
assert_eq!(sum.kind, CandidateKind::Function);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_candidates_absent_at_non_expression_slots() {
|
||||
// A non-`sql_expr_ident` slot (the table slot of `show data `)
|
||||
// must not surface function names.
|
||||
let cache = SchemaCache {
|
||||
tables: vec!["Customers".to_string()],
|
||||
..SchemaCache::default()
|
||||
};
|
||||
let cs = cands_with("show data ", 10, &cache);
|
||||
for f in ["sum", "count", "upper"] {
|
||||
assert!(
|
||||
!cs.contains(&f.to_string()),
|
||||
"function `{f}` must not appear at the table slot; got {cs:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_candidate_deduped_against_a_like_named_column() {
|
||||
// DA review (#15): a column literally named like a function
|
||||
// (`count`) must appear exactly once in the candidate line —
|
||||
// the column wins, the redundant function candidate is dropped.
|
||||
let mut cache = SchemaCache {
|
||||
tables: vec!["a".to_string()],
|
||||
columns: vec!["count".to_string()],
|
||||
..SchemaCache::default()
|
||||
};
|
||||
cache
|
||||
.table_columns
|
||||
.insert("a".to_string(), vec![TableColumn::new("count", Type::Int)]);
|
||||
let input = "select * from a where co";
|
||||
let cands = candidates_at_cursor(input, input.len(), &cache)
|
||||
.expect("some completion")
|
||||
.candidates;
|
||||
let count_entries: Vec<_> =
|
||||
cands.iter().filter(|c| c.text.eq_ignore_ascii_case("count")).collect();
|
||||
assert_eq!(
|
||||
count_entries.len(),
|
||||
1,
|
||||
"`count` must appear once, not duplicated as column + function; got {count_entries:?}",
|
||||
);
|
||||
assert_eq!(
|
||||
count_entries[0].kind,
|
||||
CandidateKind::Identifier,
|
||||
"the surviving `count` candidate must be the user's column",
|
||||
);
|
||||
// A non-colliding function at the same slot is unaffected.
|
||||
assert!(
|
||||
cands.iter().any(|c| c.text == "coalesce" && c.kind == CandidateKind::Function),
|
||||
"non-colliding functions still surface; got {cands:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cast_is_not_offered_as_a_function_candidate() {
|
||||
// `cast` uses `CAST(x AS type)`, which the call-shape grammar
|
||||
// does not parse — it must never be offered (ADR-0031 call
|
||||
// shape; sql_functions.rs exclusion).
|
||||
let cache = two_table_schema();
|
||||
let input = "select * from a where ca";
|
||||
let cs = cands_with(input, input.len(), &cache);
|
||||
assert!(
|
||||
!cs.contains(&"cast".to_string()),
|
||||
"`cast` must not be offered as a function candidate; got {cs:?}",
|
||||
);
|
||||
}
|
||||
|
||||
// ---- typing-time column-typo hint restored (issue #16) ----
|
||||
|
||||
#[test]
|
||||
fn invalid_ident_fires_for_genuine_typo_at_sql_expr_slot() {
|
||||
// Issue #16: a genuine column typo at a `sql_expr_ident` slot
|
||||
// (before FROM is in scope) warns at typing time again — it
|
||||
// matches neither a schema column nor a known function name.
|
||||
let cache = two_table_schema();
|
||||
let inv = invalid_ident_at_cursor("select Agx", 10, &cache)
|
||||
.expect("genuine typo at an expr slot must flag");
|
||||
assert_eq!(inv.found, "Agx");
|
||||
assert_eq!(inv.source, IdentSource::Columns);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_ident_does_not_fire_for_function_prefix_at_sql_expr_slot() {
|
||||
// The reason issue #6 dropped the flag: a known function name
|
||||
// (or its prefix) must NOT be mis-flagged as an unknown column.
|
||||
let cache = two_table_schema();
|
||||
assert!(
|
||||
invalid_ident_at_cursor("select su", 9, &cache).is_none(),
|
||||
"`su` prefixes `sum`/`substr` — must not flag",
|
||||
);
|
||||
assert!(
|
||||
invalid_ident_at_cursor("select sum", 10, &cache).is_none(),
|
||||
"`sum` is a known function — must not flag",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_ident_does_not_fire_for_column_prefix_at_sql_expr_slot() {
|
||||
// A real column prefix is an in-progress lookup, not a typo.
|
||||
let cache = two_table_schema();
|
||||
assert!(
|
||||
invalid_ident_at_cursor("select na", 9, &cache).is_none(),
|
||||
"`na` prefixes the `name` column — must not flag",
|
||||
);
|
||||
}
|
||||
|
||||
fn two_table_schema() -> SchemaCache {
|
||||
use crate::dsl::types::Type;
|
||||
let mut s = SchemaCache::default();
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod command;
|
||||
pub mod grammar;
|
||||
pub mod parser;
|
||||
pub mod shortid;
|
||||
pub mod sql_functions;
|
||||
pub mod types;
|
||||
pub mod value;
|
||||
pub mod walker;
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//! Curated set of SQL function names the playground recognises
|
||||
//! (ADR-0022 Amendment 6, issues #15 / #16).
|
||||
//!
|
||||
//! This is the single source of truth for "what SQL function names
|
||||
//! does this playground know about", shared by two consumers:
|
||||
//!
|
||||
//! - **Tab completion** (issue #15): at a `sql_expr_ident` slot the
|
||||
//! completion engine offers these as `CandidateKind::Function`
|
||||
//! candidates, so a learner can discover `sum` / `upper` / … without
|
||||
//! already knowing them.
|
||||
//! - **The typing-time column-typo hint** (issue #16): at the same
|
||||
//! slot, `invalid_ident_at_cursor` flags a partial as "no such
|
||||
//! column" only when it matches neither a schema column *nor* a
|
||||
//! known function name — so a genuine typo still warns early while
|
||||
//! a real function name like `sum` does not get mis-flagged.
|
||||
//!
|
||||
//! Scope (ADR-0031 §1): the SQL expression grammar admits any
|
||||
//! function-call *shape* without knowing which names are real — the
|
||||
//! walker is a structural matcher, not an evaluator. This list is a
|
||||
//! deliberately *curated pedagogical set*, not "every SQLite
|
||||
//! built-in". It covers the aggregates a learner meets first plus the
|
||||
//! common scalar functions, and is the allow-list / discovery source
|
||||
//! layered on top of that shape-only grammar.
|
||||
//!
|
||||
//! Deliberately excluded: `cast`. SQLite's `CAST` uses the
|
||||
//! `CAST(expr AS type)` syntax, which the expression grammar does
|
||||
//! **not** parse as a function call (function args are expressions,
|
||||
//! and `expr AS type` is not one). Offering `cast` as a completion
|
||||
//! candidate would surface a name that does not parse in the call
|
||||
//! shape, so it stays out of the set until the grammar grows a
|
||||
//! dedicated `CAST` form.
|
||||
|
||||
/// The curated SQL function names, lowercase and sorted.
|
||||
///
|
||||
/// Sorted + lowercase is an invariant (pinned by a unit test): the
|
||||
/// completion engine relies on a stable order, and the prefix match
|
||||
/// is case-insensitive against these canonical lowercase spellings.
|
||||
///
|
||||
/// Grouping (all plain `name(args)` call shapes):
|
||||
/// - **Aggregates:** `avg`, `count`, `max`, `min`, `sum`.
|
||||
/// - **Common scalars:** `abs`, `coalesce`, `length`, `lower`,
|
||||
/// `round`, `substr`, `trim`, `upper`.
|
||||
/// - **Broader scalars:** `date`, `datetime`, `hex`, `ifnull`,
|
||||
/// `instr`, `nullif`, `random`, `replace`, `strftime`, `typeof`.
|
||||
pub const KNOWN_SQL_FUNCTIONS: &[&str] = &[
|
||||
"abs",
|
||||
"avg",
|
||||
"coalesce",
|
||||
"count",
|
||||
"date",
|
||||
"datetime",
|
||||
"hex",
|
||||
"ifnull",
|
||||
"instr",
|
||||
"length",
|
||||
"lower",
|
||||
"max",
|
||||
"min",
|
||||
"nullif",
|
||||
"random",
|
||||
"replace",
|
||||
"round",
|
||||
"strftime",
|
||||
"substr",
|
||||
"sum",
|
||||
"trim",
|
||||
"typeof",
|
||||
"upper",
|
||||
];
|
||||
|
||||
/// Whether `partial` is a case-insensitive prefix of at least one
|
||||
/// known function name.
|
||||
///
|
||||
/// An empty `partial` matches every function (it is a prefix of
|
||||
/// all), mirroring the empty-prefix behaviour of the keyword /
|
||||
/// identifier completion sources. Used by `invalid_ident_at_cursor`
|
||||
/// to decide whether a partial at a `sql_expr_ident` slot might still
|
||||
/// resolve to a function name (and so must not be flagged as an
|
||||
/// unknown column).
|
||||
#[must_use]
|
||||
pub fn is_known_function_prefix(partial: &str) -> bool {
|
||||
let lowered = partial.to_lowercase();
|
||||
KNOWN_SQL_FUNCTIONS
|
||||
.iter()
|
||||
.any(|f| f.starts_with(&lowered))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn list_is_sorted_and_lowercase() {
|
||||
// The completion engine and prefix matcher both rely on the
|
||||
// canonical sorted-lowercase invariant.
|
||||
for f in KNOWN_SQL_FUNCTIONS {
|
||||
assert_eq!(
|
||||
*f,
|
||||
f.to_lowercase(),
|
||||
"function name `{f}` must be lowercase",
|
||||
);
|
||||
}
|
||||
let mut sorted = KNOWN_SQL_FUNCTIONS.to_vec();
|
||||
sorted.sort_unstable();
|
||||
assert_eq!(
|
||||
sorted.as_slice(),
|
||||
KNOWN_SQL_FUNCTIONS,
|
||||
"KNOWN_SQL_FUNCTIONS must be declared in sorted order",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_has_no_duplicates() {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for f in KNOWN_SQL_FUNCTIONS {
|
||||
assert!(seen.insert(*f), "duplicate function name `{f}`");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cast_is_excluded() {
|
||||
// `CAST(expr AS type)` is not a plain call shape the grammar
|
||||
// parses — it must not be offered as a candidate (faithfulness
|
||||
// to ADR-0031's call-shape grammar).
|
||||
assert!(
|
||||
!KNOWN_SQL_FUNCTIONS.contains(&"cast"),
|
||||
"`cast` is not a plain-call function and must stay out of the set",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_match_is_case_insensitive() {
|
||||
assert!(is_known_function_prefix("su")); // sum, substr
|
||||
assert!(is_known_function_prefix("SU")); // case-folded
|
||||
assert!(is_known_function_prefix("sum")); // exact
|
||||
assert!(is_known_function_prefix("UPP")); // upper
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_prefix_matches_all() {
|
||||
assert!(is_known_function_prefix(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_prefix_does_not_match() {
|
||||
assert!(!is_known_function_prefix("zzz"));
|
||||
assert!(!is_known_function_prefix("xqz"));
|
||||
// A genuine column-typo shape that prefixes no function.
|
||||
assert!(!is_known_function_prefix("Agx"));
|
||||
}
|
||||
}
|
||||
@@ -1344,6 +1344,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_select_genuine_column_typo_before_from_warns_at_typing_time() {
|
||||
// Issue #16: the gap the issue-#6 trade-off opened. While the
|
||||
// user types `select Agx` (no FROM yet, so the schema-existence
|
||||
// diagnostic stays silent), a genuine column typo must warn at
|
||||
// typing time via the restored `invalid_ident` path — `Agx`
|
||||
// matches neither a schema column nor a known function name.
|
||||
use crate::completion::{SchemaCache, TableColumn};
|
||||
use crate::dsl::types::Type;
|
||||
let mut cache = SchemaCache::default();
|
||||
cache.tables.push("Customers".to_string());
|
||||
let tc = vec![TableColumn {
|
||||
name: "Age".to_string(),
|
||||
user_type: Type::Int,
|
||||
not_null: false,
|
||||
has_default: false,
|
||||
}];
|
||||
for c in &tc {
|
||||
cache.columns.push(c.name.clone());
|
||||
}
|
||||
cache.table_columns.insert("Customers".to_string(), tc);
|
||||
let input = "select Agx";
|
||||
match ambient_hint_in_mode(input, input.len(), None, &cache, Mode::Advanced) {
|
||||
Some(AmbientHint::Prose(p)) => assert!(
|
||||
p.contains("No such") && p.contains("Agx"),
|
||||
"a genuine column typo before FROM must warn at typing time; got: {p:?}",
|
||||
),
|
||||
other => panic!(
|
||||
"`select Agx` must surface a typing-time typo hint; got: {other:?}",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_partial_typing_does_not_leak_bare_double_in_prose() {
|
||||
// Issue #5 (prose half): at `create table Orders (count` (no
|
||||
|
||||
@@ -61,6 +61,13 @@ pub struct Theme {
|
||||
pub tok_punct: Color,
|
||||
pub tok_flag: Color,
|
||||
pub tok_error: Color,
|
||||
/// SQL function-name candidate colour (ADR-0022 Amendment 6,
|
||||
/// issue #15) — a dedicated tone distinct from `tok_keyword`,
|
||||
/// `tok_identifier`, and `tok_type` so a learner can tell a
|
||||
/// callable (`sum`, `upper`) apart from a clause keyword, a
|
||||
/// column reference, and a column type. Drives the
|
||||
/// `CandidateKind::Function` colour in the hint panel.
|
||||
pub tok_function: Color,
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
@@ -95,6 +102,7 @@ impl Theme {
|
||||
tok_punct: Color::Rgb(0x8B, 0x90, 0x9A), // == muted
|
||||
tok_flag: Color::Rgb(0xFF, 0xCB, 0x6B), // amber
|
||||
tok_error: Color::Rgb(0xFF, 0x6B, 0x6B), // == error
|
||||
tok_function: Color::Rgb(0x82, 0xCF, 0xFD), // sky blue — cool like keyword but bluer, clearly apart from purple keyword + teal identifier + pink type
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +133,7 @@ impl Theme {
|
||||
tok_punct: Color::Rgb(0x60, 0x66, 0x73), // == muted
|
||||
tok_flag: Color::Rgb(0xB0, 0x88, 0x00), // mustard
|
||||
tok_error: Color::Rgb(0xC0, 0x39, 0x2B), // == error
|
||||
tok_function: Color::Rgb(0x1A, 0x5F, 0xB0), // strong blue — cool like keyword but bluer, apart from royal-purple keyword + teal identifier + magenta type
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +178,7 @@ mod tests {
|
||||
("tok_string", t.tok_string),
|
||||
("tok_flag", t.tok_flag),
|
||||
("tok_error", t.tok_error),
|
||||
("tok_function", t.tok_function),
|
||||
("warning", t.warning),
|
||||
] {
|
||||
assert_ne!(
|
||||
@@ -188,6 +198,7 @@ mod tests {
|
||||
("tok_string", t.tok_string),
|
||||
("tok_flag", t.tok_flag),
|
||||
("tok_error", t.tok_error),
|
||||
("tok_function", t.tok_function),
|
||||
("warning", t.warning),
|
||||
] {
|
||||
assert_ne!(
|
||||
@@ -220,4 +231,16 @@ mod tests {
|
||||
assert_ne!(t.tok_type, t.tok_identifier);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_colour_is_distinct_from_keyword_identifier_and_type() {
|
||||
// ADR-0022 Amendment 6 / issue #15: function-name candidates
|
||||
// get their own tone so a callable reads apart from a clause
|
||||
// keyword, a column reference, and a column type.
|
||||
for t in [Theme::dark(), Theme::light()] {
|
||||
assert_ne!(t.tok_function, t.tok_keyword);
|
||||
assert_ne!(t.tok_function, t.tok_identifier);
|
||||
assert_ne!(t.tok_function, t.tok_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,6 +1136,7 @@ fn render_candidate_line(
|
||||
crate::completion::CandidateKind::Identifier => theme.tok_identifier,
|
||||
crate::completion::CandidateKind::Flag => theme.tok_flag,
|
||||
crate::completion::CandidateKind::Punct => theme.tok_punct,
|
||||
crate::completion::CandidateKind::Function => theme.tok_function,
|
||||
};
|
||||
let base_fg = if mixed {
|
||||
match items[i].mode {
|
||||
|
||||
Reference in New Issue
Block a user