Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/snippets/elaborate.rs')
| -rw-r--r-- | helix-core/src/snippets/elaborate.rs | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/helix-core/src/snippets/elaborate.rs b/helix-core/src/snippets/elaborate.rs new file mode 100644 index 00000000..0fb5fb7b --- /dev/null +++ b/helix-core/src/snippets/elaborate.rs @@ -0,0 +1,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)) + } +} |