hamelin_lib 0.21.9

Core library for Hamelin query language
Documentation
use std::io::Cursor;
use std::ops::{Range, RangeInclusive};

use ariadne::{Label, Report, ReportKind, Source};
use serde::Serialize;
use tsify::Tsify;

/// Completion result with an exclusive range matching CodeMirror's convention.
/// `at` is a half-open range [start, end) where `start..end` covers the text to replace.
/// An empty range like `5..5` means "insert at position 5, replace nothing."
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Tsify)]
#[tsify(into_wasm_abi)]
pub struct Completion {
    pub at: Range<usize>,
    pub filter: Option<bool>,
    pub items: Vec<CompletionItem>,
}

impl Completion {
    /// Create a new Completion from an inclusive interval (as produced by `interval()`).
    /// Converts the inclusive end to an exclusive end internally.
    pub fn new(at: RangeInclusive<usize>) -> Self {
        Self::new_exclusive(*at.start()..*at.end() + 1)
    }

    /// Create a new Completion from an exclusive range (matching CodeMirror's convention and allowing empty ranges).
    pub fn new_exclusive(at: Range<usize>) -> Self {
        Self {
            at,
            filter: None,
            items: Vec::default(),
        }
    }

    pub fn add_item(&mut self, item: CompletionItem) {
        self.items.push(item);
    }

    pub fn add_items(&mut self, items: Vec<CompletionItem>) {
        self.items.extend(items);
    }

    pub fn filter(&mut self, filter: bool) {
        self.filter = Some(filter);
    }

    pub fn make_pretty(&self, hamelin: &String) -> Result<String, std::fmt::Error> {
        // Needed to make write!(string_buf, ...) work
        use std::fmt::Write;

        let input = hamelin.as_str();
        let source = Source::from(input);
        let mut string_buf = String::new();

        let mut colors = ariadne::ColorGenerator::default();
        let kind = ReportKind::Advice;
        let mut report =
            Report::build(kind, self.at.clone()).with_message("Completion suggestions to offer");

        for item in self.items.iter() {
            report = report.with_label(
                Label::new(self.at.clone())
                    .with_message(format!(
                        "{} {}({:?})",
                        item.label,
                        item.detail
                            .as_ref()
                            .map(|d| format!("[{}] ", d))
                            .unwrap_or_default(),
                        item.kind
                    ))
                    .with_color(colors.next()),
            );
        }

        if let Some(filter) = self.filter {
            report = report.with_note(format!("editor should filter: {}", filter))
        }

        let final_report = report.finish();
        let buf = Vec::new();
        let mut cursor = Cursor::new(buf);
        final_report
            .write(source, &mut cursor)
            .map_err(|_| std::fmt::Error)?;
        write!(string_buf, "{}", String::from_utf8_lossy(cursor.get_ref()))?;

        Ok(string_buf)
    }
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq, Tsify)]
#[tsify(into_wasm_abi)]
pub struct CompletionItem {
    /// What to show
    pub label: String,
    /// What to actually put down
    pub apply_string: String,
    /// Small text next to the label.
    pub detail: Option<String>,
    /// Floating shit out to the right.
    pub description: Option<String>,
    /// Icon
    pub kind: CompletionItemKind,
    // TODO: Structure this, somehow?
    //       Yeeting back code-mirror snippet style doesn't seem right.
    pub snippet: Option<String>,
    /// The group header this item sorts under. Currently only ever a space name.
    ///
    /// Unlike [`Self::boost`] this is not a nudge. Members of a section never interleave
    /// with anything outside it: the editor ranks the sections against each other, by the
    /// best-matching member of each, and every member then moves as a block. An item with
    /// no section takes no group offset at all, which places it above *every* section
    /// however badly it matches -- so leaving something unsectioned is a strong claim, not
    /// a neutral default.
    ///
    /// Most completions do carry no section -- fields, functions, commands, keywords --
    /// which is only harmless because they never share a completion set with sectioned
    /// ones. Among the `FROM` dataset completions, which do, pipeline definitions are the
    /// only unsectioned item.
    pub section: Option<String>,
    /// Ranking nudge applied by the editor on top of its own match score.
    ///
    /// Only meaningful when the editor is doing the filtering: a completion that
    /// sets `filter(false)` is emitted in order and this is ignored. Keep values
    /// inside ±99, which is what CodeMirror documents.
    pub boost: Option<i32>,
}

impl CompletionItem {
    pub fn new<S>(label: S) -> Self
    where
        S: Into<String> + Clone,
    {
        Self {
            label: label.clone().into(),
            apply_string: label.into(),
            detail: None,
            description: None,
            kind: CompletionItemKind::Text,
            snippet: None,
            section: None,
            boost: None,
        }
    }

    pub fn with_apply_string<S>(mut self, apply_string: S) -> Self
    where
        S: Into<String>,
    {
        self.apply_string = apply_string.into();
        self
    }

    pub fn with_detail<S>(mut self, detail: S) -> Self
    where
        S: Into<String>,
    {
        self.detail = Some(detail.into());
        self
    }

    pub fn with_description<S>(mut self, description: S) -> Self
    where
        S: Into<String>,
    {
        self.description = Some(description.into());
        self
    }

    pub fn with_kind(mut self, kind: CompletionItemKind) -> Self {
        self.kind = kind;
        self
    }

    pub fn with_snippet(mut self, snippet: String) -> Self {
        self.snippet = Some(snippet);
        self
    }

    pub fn with_section(mut self, section: String) -> Self {
        self.section = Some(section);
        self
    }

    pub fn with_boost(mut self, boost: i32) -> Self {
        self.boost = Some(boost);
        self
    }
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq, Tsify)]
#[tsify(into_wasm_abi)]
pub enum CompletionItemKind {
    Text,
    Variable,
    Function,
    Command,
    Keyword,
}