rototo 0.1.0-alpha.8

Control plane for runtime configuration of your application.
Documentation
use super::*;

pub(super) struct TomlCompletionSpec {
    label: &'static str,
    detail: &'static str,
    insert_text: &'static str,
}

pub(super) const VARIABLE_TOP_LEVEL_COMPLETIONS: &[TomlCompletionSpec] = &[
    TomlCompletionSpec {
        label: "schema_version",
        detail: "variable field",
        insert_text: "schema_version = 1",
    },
    TomlCompletionSpec {
        label: "description",
        detail: "variable field",
        insert_text: "description = \"\"",
    },
    TomlCompletionSpec {
        label: "type",
        detail: "variable field",
        insert_text: "type = \"string\"",
    },
    TomlCompletionSpec {
        label: "[resolve]",
        detail: "variable block",
        insert_text: "[resolve]\ndefault = ",
    },
    TomlCompletionSpec {
        label: "[[resolve.rule]]",
        detail: "variable block",
        insert_text: "[[resolve.rule]]\nwhen = \"\"\nvalue = ",
    },
];

pub(super) const VARIABLE_RESOLVE_COMPLETIONS: &[TomlCompletionSpec] = &[
    TomlCompletionSpec {
        label: "method",
        detail: "variable field",
        insert_text: "method = \"rules\"",
    },
    TomlCompletionSpec {
        label: "default",
        detail: "variable field",
        insert_text: "default = ",
    },
    TomlCompletionSpec {
        label: "from",
        detail: "variable field",
        insert_text: "from = \"\"",
    },
    TomlCompletionSpec {
        label: "filter",
        detail: "variable field",
        insert_text: "filter = \"\"",
    },
    TomlCompletionSpec {
        label: "sort",
        detail: "variable field",
        insert_text: "sort = \"\"",
    },
    TomlCompletionSpec {
        label: "order",
        detail: "variable field",
        insert_text: "order = \"asc\"",
    },
    TomlCompletionSpec {
        label: "limit",
        detail: "variable field",
        insert_text: "limit = ",
    },
    TomlCompletionSpec {
        label: "allocation",
        detail: "variable field",
        insert_text: "allocation = \"\"",
    },
    TomlCompletionSpec {
        label: "[[resolve.rule]]",
        detail: "variable block",
        insert_text: "[[resolve.rule]]\nwhen = \"\"\nvalue = ",
    },
    TomlCompletionSpec {
        label: "[[resolve.assign]]",
        detail: "variable block",
        insert_text: "[[resolve.assign]]\narm = \"\"\nvalue = ",
    },
];

pub(super) const VARIABLE_ASSIGN_COMPLETIONS: &[TomlCompletionSpec] = &[
    TomlCompletionSpec {
        label: "arm",
        detail: "variable field",
        insert_text: "arm = \"\"",
    },
    TomlCompletionSpec {
        label: "value",
        detail: "variable field",
        insert_text: "value = ",
    },
    TomlCompletionSpec {
        label: "[[resolve.assign]]",
        detail: "variable block",
        insert_text: "[[resolve.assign]]\narm = \"\"\nvalue = ",
    },
];

pub(super) const VARIABLE_RULE_COMPLETIONS: &[TomlCompletionSpec] = &[
    TomlCompletionSpec {
        label: "when",
        detail: "variable field",
        insert_text: "when = \"\"",
    },
    TomlCompletionSpec {
        label: "value",
        detail: "variable field",
        insert_text: "value = ",
    },
    TomlCompletionSpec {
        label: "[[resolve.rule]]",
        detail: "variable block",
        insert_text: "[[resolve.rule]]\nwhen = \"\"\nvalue = ",
    },
];

pub(super) const CUSTOM_LINT_FIELD_SELECTORS: &[&str] = &[
    "description",
    "extends",
    "id",
    "json",
    "json.",
    "key",
    "not",
    "predicates",
    "resolve",
    "schema",
    "type",
    "value",
    "value.",
    "values",
];

pub(super) fn variable_field_completion_items(
    snapshot: &PackageLintSnapshot,
    path: &str,
    position: SourcePosition,
) -> Vec<PackageCompletionItem> {
    let context = toml_completion_context(snapshot, path, position);
    match context.table.as_deref() {
        None => toml_completion_items(VARIABLE_TOP_LEVEL_COMPLETIONS, &context),
        Some("resolve") => toml_completion_items(VARIABLE_RESOLVE_COMPLETIONS, &context),
        Some("resolve.rule") => toml_completion_items(VARIABLE_RULE_COMPLETIONS, &context),
        Some("resolve.assign") => toml_completion_items(VARIABLE_ASSIGN_COMPLETIONS, &context),
        Some(_) => Vec::new(),
    }
}

pub(super) fn toml_completion_items(
    specs: &[TomlCompletionSpec],
    context: &TomlCompletionContext,
) -> Vec<PackageCompletionItem> {
    specs
        .iter()
        .filter(|spec| toml_completion_spec_is_available(spec, context))
        .map(|spec| {
            PackageCompletionItem::new(
                spec.label,
                PackageCompletionItemKind::FieldSelector,
                spec.detail,
            )
            .with_insert_text(spec.insert_text)
        })
        .collect()
}

pub(super) struct TomlCompletionContext {
    table: Option<String>,
    keys: BTreeSet<String>,
    tables: BTreeSet<String>,
}

pub(super) fn toml_completion_context(
    snapshot: &PackageLintSnapshot,
    path: &str,
    position: SourcePosition,
) -> TomlCompletionContext {
    let Some(text) = snapshot.source_text(path) else {
        return TomlCompletionContext {
            table: None,
            keys: BTreeSet::new(),
            tables: BTreeSet::new(),
        };
    };
    let lines = text
        .split('\n')
        .map(|line| line.strip_suffix('\r').unwrap_or(line))
        .collect::<Vec<_>>();
    let tables = lines
        .iter()
        .filter_map(|line| toml_table_header(line))
        .map(str::to_owned)
        .collect::<BTreeSet<_>>();
    let (table, start_line) = toml_table_before_position(&lines, position.line);
    let end_line = toml_table_end_line(&lines, start_line);
    let keys = lines
        .iter()
        .enumerate()
        .skip(start_line)
        .take(end_line.saturating_sub(start_line))
        .filter(|(line, _)| *line != position.line)
        .filter_map(|(_, line)| toml_key(line))
        .map(str::to_owned)
        .collect::<BTreeSet<_>>();

    TomlCompletionContext {
        table,
        keys,
        tables,
    }
}

pub(super) fn toml_completion_spec_is_available(
    spec: &TomlCompletionSpec,
    context: &TomlCompletionContext,
) -> bool {
    if spec.label == "[[resolve.rule]]" {
        return true;
    }
    if spec.label == "[resolve]" {
        return !context.tables.contains("resolve");
    }

    let key = spec.label;
    if context.keys.contains(key) {
        return false;
    }
    true
}

pub(super) fn toml_table_before_position(
    lines: &[&str],
    position_line: usize,
) -> (Option<String>, usize) {
    let mut table = None;
    let mut start_line = 0;
    for (line_number, line) in lines.iter().enumerate().take(position_line) {
        if let Some(header) = toml_table_header(line) {
            table = Some(header.to_owned());
            start_line = line_number + 1;
        }
    }
    (table, start_line)
}

pub(super) fn toml_table_end_line(lines: &[&str], start_line: usize) -> usize {
    lines
        .iter()
        .enumerate()
        .skip(start_line)
        .find_map(|(line_number, line)| toml_table_header(line).map(|_| line_number))
        .unwrap_or(lines.len())
}

pub(super) fn toml_table_header(line: &str) -> Option<&str> {
    let line = line.split('#').next().unwrap_or(line).trim();
    if let Some(rest) = line.strip_prefix("[[") {
        return rest
            .find("]]")
            .map(|end| rest[..end].trim())
            .filter(|name| !name.is_empty());
    }
    if let Some(rest) = line.strip_prefix('[') {
        if rest.starts_with('[') {
            return None;
        }
        return rest
            .find(']')
            .map(|end| rest[..end].trim())
            .filter(|name| !name.is_empty());
    }
    None
}

pub(super) fn toml_key(line: &str) -> Option<&str> {
    let line = line.split('#').next().unwrap_or(line).trim();
    if line.starts_with('[') {
        return None;
    }
    let (key, _) = line.split_once('=')?;
    let key = key.trim();
    if key.is_empty() { None } else { Some(key) }
}

pub(super) fn catalog_entry_field_completion_items(
    snapshot: &PackageLintSnapshot,
    path: &str,
    position: SourcePosition,
) -> Vec<PackageCompletionItem> {
    let Some(catalog_id) = catalog_id_for_entry_path(path) else {
        return Vec::new();
    };
    let context = toml_completion_context(snapshot, path, position);
    if context.table.is_some() {
        return Vec::new();
    }
    let Some(properties) = snapshot
        .index
        .catalogs
        .get(catalog_id)
        .and_then(|catalog| catalog.json.as_ref())
        .and_then(|json| json.get("properties"))
        .and_then(serde_json::Value::as_object)
    else {
        return Vec::new();
    };

    properties
        .keys()
        .filter(|field| !context.keys.contains(field.as_str()))
        .map(|field| {
            PackageCompletionItem::new(
                field.clone(),
                PackageCompletionItemKind::FieldSelector,
                "catalog entry field",
            )
            .with_insert_text(format!("{field} = "))
        })
        .collect()
}

pub(super) fn catalog_id_for_entry_path(path: &str) -> Option<&str> {
    let path = path.strip_prefix("data/catalogs/")?;
    let (directory, entry) = path.split_once('/')?;
    if entry.is_empty() || !entry.ends_with(".toml") {
        return None;
    }
    (!directory.is_empty()).then_some(directory)
}

pub(super) fn custom_lint_field_selector_completion_items() -> Vec<PackageCompletionItem> {
    CUSTOM_LINT_FIELD_SELECTORS
        .iter()
        .copied()
        .map(|field| {
            PackageCompletionItem::new(
                field,
                PackageCompletionItemKind::FieldSelector,
                "custom lint field selector",
            )
        })
        .collect()
}