Unnamed repository; edit this file 'description' to name the repository.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# Palette based on https://github.com/NLKNguyen/papercolor-theme
# Author: Soc Virnyl Estela <[email protected]>

"ui.linenr.selected" = { fg = "linenr_fg_selected" }
"ui.background" = {bg="background"}
"ui.text" = "foreground"
"ui.text.focus" = { fg = "selection_background", modifiers = ["bold"]}
"ui.selection" = {bg="selection_background", fg="selection_foreground"}
"ui.highlight" = {bg="cursorline_background"}
"ui.cursorline" = {bg="cursorline_background"}
"ui.statusline" = {bg="paper_bar_bg", fg="regular0"}
"ui.statusline.select" = {bg="background", fg="bright7"}
"ui.statusline.normal" = {bg="background", fg="bright3"}
"ui.statusline.inactive" = {bg="bright0", fg="foreground"}
"ui.virtual" = "indent"
"ui.virtual.whitespace" = { fg = "regular5" }
"ui.virtual.ruler" = {bg="cursorline_background"}
"ui.cursor.match" = {bg = "regular5", fg = "regular0"}
"ui.cursor" = {bg = "regular5", fg = "background"}
"ui.window" = {bg = "#D0D0D0", fg = "bright2"}
"ui.help" = {bg = "background", fg = "bright2"}
"ui.popup" = {bg = "#D0D0D0", fg = "bright7"}
"ui.menu" = {bg = "#D0D0D0", fg = "bright7"}
"ui.menu.selected" = {bg = "selection_background", fg="selection_foreground"}

"markup.heading" = { fg = "bright7", modifiers = ["bold"] }
"markup.heading.1" = { fg = "bright2", modifiers = ["bold"] }
"markup.heading.2" = { fg = "bright4", modifiers = ["bold"] }
"markup.heading.3" = { fg = "bright3", modifiers = ["bold"] }
"markup.heading.4" = { fg = "bright4", modifiers = ["bold"] }
"markup.heading.5" = { fg = "bright4", modifiers = ["bold"] }
"markup.heading.6" = { fg = "bright4", modifiers = ["bold"] }
"markup.list" = "regular4"
"markup.bold" = { fg = "foreground", modifiers = ["bold"] }
"markup.italic" = { modifiers = ["italic"] }
"markup.strikethrough" = { modifiers = ["crossed_out"] }
"markup.link.url" = { fg = "regular4", modifiers = ["underlined"] }
"markup.link.text" = "bright2"
"markup.link.label" = { fg = "regular7", modifiers = ["bold"] }
"markup.raw" = "foreground"

"string" = "foreground"
"attribute" = "bright7"
"keyword" = { fg = "regular4", modifiers = ["bold"]}
"keyword.directive" = "regular1"
"namespace" = "regular1"
"type" = "bright2"
"type.builtin" = { fg = "regular4", modifiers = ["bold"]}
"variable" = "foreground"
"variable.builtin" = "cyan"
"variable.other.member" = "regular4"
"variable.parameter" = "foreground"

"special" = "#3E999F"
"function"  = "bright1"
"constructor" = "bright1"
"function.builtin" = { fg = "regular4", modifiers = ["bold"]}
"function.macro" = { fg = "regular1" }
"comment" = { fg = "bright0", modifiers = ["dim"] }
"ui.linenr" = { fg = "bright0" }
"module" = "#af0000"
"constant" = "#5f8700"
"constant.builtin" = "#5f8700"
"constant.numeric" = "#d75f00"
"constant.character.escape" = { fg = "#8700af", modifiers = ["bold"]}
"operator" = { fg = "regular4", modifiers = ["bold"]}

"label" = { fg = "selection_background", modifiers = ["bold", "italic"] }

"diff.plus" = "regular2"
"diff.delta" = "bright0"
"diff.minus" = "bright1"

"warning" = "bright4"
"error" = "regular1"
"info" = "#FFAF00"

"diagnostic.warning".underline = { color = "bright4", style = "curl" } 
"diagnostic.error".underline = { color = "regular1", style = "curl" } 
"diagnostic.info".underline = { color = "#FFAF00", style = "curl" } 
"diagnostic.hint".underline = { color = "#FFAF00", style = "curl" } 


[palette]
background="#eeeeee"
foreground="#444444"
regular0="#eeeeee"
regular1="#af0000"
regular2="#008700"
regular3="#5f8700"
regular4="#0087af"
regular5="#878787"
regular6="#005f87"
regular7="#764e37"
bright0="#bcbcbc"
bright1="#d70000"
bright2="#d70087"
bright3="#8700af"
bright4="#d75f00"
bright5="#d75f00"
bright6="#4c7a5d"
bright7="#005faf"
selection_foreground="#eeeeee"
selection_background="#0087af"
cursorline_background="#fdfdfd"
paper_bar_bg="#005F87"
black="#eeeeee"
red="#d70000"
green="#008700"
yellow="#5f8700"
blue="#0087af"
magenta="#878787"
cyan="#005f87"
gray="#764e37"
light-red="#d70000"
light-green="#d70087"
light-yellow="#8700af"
light-blue="#d75f00"
light-magenta="#d75f00"
light-cyan="#4c7a4d"
light-gray="#005faf"
white="#444444"
linenr_fg_selected="#AF634D"
47 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
use std::mem::swap;
use std::ops::Index;
use std::sync::Arc;

use anyhow::{anyhow, Result};
use helix_stdx::rope::RopeSliceExt;
use helix_stdx::Range;
use regex_cursor::engines::meta::Builder as RegexBuilder;
use regex_cursor::engines::meta::Regex;
use regex_cursor::regex_automata::util::syntax::Config as RegexConfig;
use ropey::RopeSlice;

use crate::case_conversion::to_lower_case_with;
use crate::case_conversion::to_upper_case_with;
use crate::case_conversion::{to_camel_case_with, to_pascal_case_with};
use crate::snippets::parser::{self, CaseChange, FormatItem};
use crate::snippets::{TabstopIdx, LAST_TABSTOP_IDX};
use crate::Tendril;

#[derive(Debug)]
pub struct Snippet {
    elements: Vec<SnippetElement>,
    tabstops: Vec<Tabstop>,
}

impl Snippet {
    pub fn parse(snippet: &str) -> Result<Self> {
        let parsed_snippet = parser::parse(snippet)
            .map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))?;
        Ok(Snippet::new(parsed_snippet))
    }

    pub fn new(elements: Vec<parser::SnippetElement>) -> Snippet {
        let mut res = Snippet {
            elements: Vec::new(),
            tabstops: Vec::new(),
        };
        res.elements = res.elaborate(elements, None).into();
        res.fixup_tabstops();
        res.ensure_last_tabstop();
        res.renumber_tabstops();
        res
    }

    pub fn elements(&self) -> &[SnippetElement] {
        &self.elements
    }

    pub fn tabstops(&self) -> impl Iterator<Item = &Tabstop> {
        self.tabstops.iter()
    }

    fn renumber_tabstops(&mut self) {
        Self::renumber_tabstops_in(&self.tabstops, &mut self.elements);
        for i in 0..self.tabstops.len() {
            if let Some(parent) = self.tabstops[i].parent {
                let parent = self
                    .tabstops
                    .binary_search_by_key(&parent, |tabstop| tabstop.idx)
                    .expect("all tabstops have been resolved");
                self.tabstops[i].parent = Some(TabstopIdx(parent));
            }
            let tabstop = &mut self.tabstops[i];
            if let TabstopKind::Placeholder { default } = &tabstop.kind {
                let mut default = default.clone();
                tabstop.kind = TabstopKind::Empty;
                Self::renumber_tabstops_in(&self.tabstops, Arc::get_mut(&mut default).unwrap());
                self.tabstops[i].kind = TabstopKind::Placeholder { default };
            }
        }
    }

    fn renumber_tabstops_in(tabstops: &[Tabstop], elements: &mut [SnippetElement]) {
        for elem in elements {
            match elem {
                SnippetElement::Tabstop { idx } => {
                    idx.0 = tabstops
                        .binary_search_by_key(&*idx, |tabstop| tabstop.idx)
                        .expect("all tabstops have been resolved")
                }
                SnippetElement::Variable { default, .. } => {
                    if let Some(default) = default {
                        Self::renumber_tabstops_in(tabstops, default);
                    }
                }
                SnippetElement::Text(_) => (),
            }
        }
    }

    fn fixup_tabstops(&mut self) {
        self.tabstops.sort_by_key(|tabstop| tabstop.idx);
        self.tabstops.dedup_by(|tabstop1, tabstop2| {
            if tabstop1.idx != tabstop2.idx {
                return false;
            }
            // use the first non empty tabstop for all multicursor tabstops
            if tabstop2.kind.is_empty() {
                swap(tabstop2, tabstop1)
            }
            true
        })
    }

    fn ensure_last_tabstop(&mut self) {
        if matches!(self.tabstops.last(), Some(tabstop) if tabstop.idx == LAST_TABSTOP_IDX) {
            return;
        }
        self.tabstops.push(Tabstop {
            idx: LAST_TABSTOP_IDX,
            parent: None,
            kind: TabstopKind::Empty,
        });
        self.elements.push(SnippetElement::Tabstop {
            idx: LAST_TABSTOP_IDX,
        })
    }

    fn elaborate(
        &mut self,
        default: Vec<parser::SnippetElement>,
        parent: Option<TabstopIdx>,
    ) -> Box<[SnippetElement]> {
        default
            .into_iter()
            .map(|val| match val {
                parser::SnippetElement::Tabstop {
                    tabstop,
                    transform: None,
                } => SnippetElement::Tabstop {
                    idx: self.elaborate_placeholder(tabstop, parent, Vec::new()),
                },
                parser::SnippetElement::Tabstop {
                    tabstop,
                    transform: Some(transform),
                } => SnippetElement::Tabstop {
                    idx: self.elaborate_transform(tabstop, parent, transform),
                },
                parser::SnippetElement::Placeholder { tabstop, value } => SnippetElement::Tabstop {
                    idx: self.elaborate_placeholder(tabstop, parent, value),
                },
                parser::SnippetElement::Choice { tabstop, choices } => SnippetElement::Tabstop {
                    idx: self.elaborate_choice(tabstop, parent, choices),
                },
                parser::SnippetElement::Variable {
                    name,
                    default,
                    transform,
                } => SnippetElement::Variable {
                    name,
                    default: default.map(|default| self.elaborate(default, parent)),
                    // TODO: error for invalid transforms
                    transform: transform.and_then(Transform::new).map(Box::new),
                },
                parser::SnippetElement::Text(text) => SnippetElement::Text(text),
            })
            .collect()
    }

    fn elaborate_choice(
        &mut self,
        idx: usize,
        parent: Option<TabstopIdx>,
        choices: Vec<Tendril>,
    ) -> TabstopIdx {
        let idx = TabstopIdx::elaborate(idx);
        self.tabstops.push(Tabstop {
            idx,
            parent,
            kind: TabstopKind::Choice {
                choices: choices.into(),
            },
        });
        idx
    }

    fn elaborate_placeholder(
        &mut self,
        idx: usize,
        parent: Option<TabstopIdx>,
        default: Vec<parser::SnippetElement>,
    ) -> TabstopIdx {
        let idx = TabstopIdx::elaborate(idx);
        let default = self.elaborate(default, Some(idx));
        self.tabstops.push(Tabstop {
            idx,
            parent,
            kind: TabstopKind::Placeholder {
                default: default.into(),
            },
        });
        idx
    }

    fn elaborate_transform(
        &mut self,
        idx: usize,
        parent: Option<TabstopIdx>,
        transform: parser::Transform,
    ) -> TabstopIdx {
        let idx = TabstopIdx::elaborate(idx);
        if let Some(transform) = Transform::new(transform) {
            self.tabstops.push(Tabstop {
                idx,
                parent,
                kind: TabstopKind::Transform(Arc::new(transform)),
            })
        } else {
            // TODO: proper error
            self.tabstops.push(Tabstop {
                idx,
                parent,
                kind: TabstopKind::Empty,
            })
        }
        idx
    }
}

impl Index<TabstopIdx> for Snippet {
    type Output = Tabstop;
    fn index(&self, index: TabstopIdx) -> &Tabstop {
        &self.tabstops[index.0]
    }
}

#[derive(Debug)]
pub enum SnippetElement {
    Tabstop {
        idx: TabstopIdx,
    },
    Variable {
        name: Tendril,
        default: Option<Box<[SnippetElement]>>,
        transform: Option<Box<Transform>>,
    },
    Text(Tendril),
}

#[derive(Debug)]
pub struct Tabstop {
    idx: TabstopIdx,
    pub parent: Option<TabstopIdx>,
    pub kind: TabstopKind,
}

#[derive(Debug)]
pub enum TabstopKind {
    Choice { choices: Arc<[Tendril]> },
    Placeholder { default: Arc<[SnippetElement]> },
    Empty,
    Transform(Arc<Transform>),
}

impl TabstopKind {
    pub fn is_empty(&self) -> bool {
        matches!(self, TabstopKind::Empty)
    }
}

#[derive(Debug)]
pub struct Transform {
    regex: Regex,
    regex_str: Box<str>,
    global: bool,
    replacement: Box<[FormatItem]>,
}

impl PartialEq for Transform {
    fn eq(&self, other: &Self) -> bool {
        self.replacement == other.replacement
            && self.global == other.global
            // doens't compare m and i setting but close enough
            && self.regex_str == other.regex_str
    }
}

impl Transform {
    fn new(transform: parser::Transform) -> Option<Transform> {
        let mut config = RegexConfig::new();
        let mut global = false;
        let mut invalid_config = false;
        for c in transform.options.chars() {
            match c {
                'i' => {
                    config = config.case_insensitive(true);
                }
                'm' => {
                    config = config.multi_line(true);
                }
                'g' => {
                    global = true;
                }
                // we ignore 'u' since we always want to
                // do unicode aware matching
                _ => invalid_config = true,
            }
        }
        if invalid_config {
            log::error!("invalid transform configuration characters {transform:?}");
        }
        let regex = match RegexBuilder::new().syntax(config).build(&transform.regex) {
            Ok(regex) => regex,
            Err(err) => {
                log::error!("invalid transform {err} {transform:?}");
                return None;
            }
        };
        Some(Transform {
            regex,
            regex_str: transform.regex.as_str().into(),
            global,
            replacement: transform.replacement.into(),
        })
    }

    pub fn apply(&self, mut doc: RopeSlice<'_>, range: Range) -> Tendril {
        let mut buf = Tendril::new();
        let it = self
            .regex
            .captures_iter(doc.regex_input_at(range))
            .enumerate();
        doc = doc.slice(range);
        let mut last_match = 0;
        for (_, cap) in it {
            // unwrap on 0 is OK because captures only reports matches
            let m = cap.get_group(0).unwrap();
            buf.extend(doc.byte_slice(last_match..m.start).chunks());
            last_match = m.end;
            for fmt in &*self.replacement {
                match *fmt {
                    FormatItem::Text(ref text) => {
                        buf.push_str(text);
                    }
                    FormatItem::Capture(i) => {
                        if let Some(cap) = cap.get_group(i) {
                            buf.extend(doc.byte_slice(cap.range()).chunks());
                        }
                    }
                    FormatItem::CaseChange(i, change) => {
                        if let Some(cap) = cap.get_group(i).filter(|i| !i.is_empty()) {
                            let mut chars = doc.byte_slice(cap.range()).chars();
                            match change {
                                CaseChange::Upcase => to_upper_case_with(chars, &mut buf),
                                CaseChange::Downcase => to_lower_case_with(chars, &mut buf),
                                CaseChange::Capitalize => {
                                    let first_char = chars.next().unwrap();
                                    buf.extend(first_char.to_uppercase());
                                    buf.extend(chars);
                                }
                                CaseChange::PascalCase => to_pascal_case_with(chars, &mut buf),
                                CaseChange::CamelCase => to_camel_case_with(chars, &mut buf),
                            }
                        }
                    }
                    FormatItem::Conditional(i, ref if_, ref else_) => {
                        if cap.get_group(i).map_or(true, |mat| mat.is_empty()) {
                            buf.push_str(else_)
                        } else {
                            buf.push_str(if_)
                        }
                    }
                }
            }
            if !self.global {
                break;
            }
        }
        buf.extend(doc.byte_slice(last_match..).chunks());
        buf
    }
}

impl TabstopIdx {
    fn elaborate(idx: usize) -> Self {
        TabstopIdx(idx.wrapping_sub(1))
    }
}