From 47feee14afd894f760c0aedeaf95589179511870 Mon Sep 17 00:00:00 2001 From: HerringtonDarkholme <2883231+HerringtonDarkholme@users.noreply.github.com> Date: Fri, 9 Jun 2023 18:12:41 -0700 Subject: [PATCH] feat: add support for transform Part of #436 --- crates/core/src/meta_var.rs | 64 ++++--------------- crates/core/src/replacer.rs | 55 +++++++++++++++++ crates/core/src/replacer/structural.rs | 26 +++++--- crates/core/src/replacer/template.rs | 85 ++++++++++++++++---------- 4 files changed, 137 insertions(+), 93 deletions(-) diff --git a/crates/core/src/meta_var.rs b/crates/core/src/meta_var.rs index ba128a6ccd..a5715bc025 100644 --- a/crates/core/src/meta_var.rs +++ b/crates/core/src/meta_var.rs @@ -1,17 +1,20 @@ use crate::match_tree::does_node_match_exactly; use crate::matcher::{KindMatcher, Pattern, RegexMatcher}; +use crate::source::Content; use crate::{Doc, Node}; use std::borrow::Cow; use std::collections::HashMap; pub type MetaVariableID = String; +type Underlying = Vec<<::Source as Content>::Underlying>; /// a dictionary that stores metavariable instantiation /// const a = 123 matched with const a = $A will produce env: $A => 123 #[derive(Clone)] pub struct MetaVarEnv<'tree, D: Doc> { single_matched: HashMap>, multi_matched: HashMap>>, + transformed_var: HashMap>, } impl<'tree, D: Doc> MetaVarEnv<'tree, D> { @@ -19,6 +22,7 @@ impl<'tree, D: Doc> MetaVarEnv<'tree, D> { Self { single_matched: HashMap::new(), multi_matched: HashMap::new(), + transformed_var: HashMap::new(), } } @@ -39,14 +43,6 @@ impl<'tree, D: Doc> MetaVarEnv<'tree, D> { Some(self) } - pub fn get(&self, var: &MetaVariable) -> Option> { - match var { - MetaVariable::Named(n, _) => self.single_matched.get(n).map(MatchResult::Single), - MetaVariable::NamedEllipsis(n) => self.multi_matched.get(n).map(MatchResult::Multi), - _ => None, - } - } - pub fn get_match(&self, var: &str) -> Option<&'_ Node<'tree, D>> { self.single_matched.get(var) } @@ -55,6 +51,10 @@ impl<'tree, D: Doc> MetaVarEnv<'tree, D> { self.multi_matched.get(var).cloned().unwrap_or_default() } + pub fn get_transformed(&self, var: &str) -> Option<&Underlying> { + self.transformed_var.get(var) + } + pub fn add_label(&mut self, label: &str, node: Node<'tree, D>) { self .multi_matched @@ -101,6 +101,10 @@ impl<'tree, D: Doc> MetaVarEnv<'tree, D> { } true } + + pub fn insert_transformation(&mut self, name: MetaVariableID, src: Underlying) { + self.transformed_var.insert(name, src); + } } impl<'tree, D: Doc> Default for MetaVarEnv<'tree, D> { @@ -124,13 +128,6 @@ impl<'tree, D: Doc> From> for HashMap { } } -pub enum MatchResult<'a, 'tree, D: Doc> { - /// $A for captured meta var - Single(&'a Node<'tree, D>), - /// $$$A for captured ellipsis - Multi(&'a Vec>), -} - #[derive(Debug, PartialEq, Eq)] pub enum MetaVariable { /// $A for captured meta var @@ -142,13 +139,6 @@ pub enum MetaVariable { /// $$$A for captured ellipsis NamedEllipsis(MetaVariableID), } -// TODO add var_extract -pub enum MetaVarExtract { - Single(), - Multiple(), - // TODO: add transform - // Transformed(), -} #[derive(Clone)] pub struct MetaVarMatchers(HashMap>); @@ -230,36 +220,6 @@ pub(crate) fn extract_meta_var(src: &str, meta_char: char) -> Option Option<(MetaVariable, &str)> { - debug_assert!(src.starts_with(meta_char)); - let mut i = 0; - let (trimmed, is_multi) = loop { - i += 1; - src = &src[meta_char.len_utf8()..]; - if i == 3 { - break (src, true); - } - if !src.starts_with(meta_char) { - break (src, false); - } - }; - // no Anonymous meta var allowed, so _ is not allowed - let i = trimmed - .find(|c: char| !c.is_ascii_uppercase()) - .unwrap_or(trimmed.len()); - // no name found - if i == 0 { - return None; - } - let name = trimmed[..i].to_string(); - let var = if is_multi { - MetaVariable::NamedEllipsis(name) - } else { - MetaVariable::Named(name, true) - }; - Some((var, &trimmed[i..])) -} - fn is_valid_meta_var_char(c: char) -> bool { matches!(c, 'A'..='Z' | '_') } diff --git a/crates/core/src/replacer.rs b/crates/core/src/replacer.rs index 4fb97131ec..7074c582db 100644 --- a/crates/core/src/replacer.rs +++ b/crates/core/src/replacer.rs @@ -1,4 +1,5 @@ use crate::matcher::NodeMatch; +use crate::meta_var::{MetaVariable, MetaVariableID}; use crate::source::{Content, Edit as E}; use crate::Pattern; use crate::{Doc, Node, Root}; @@ -55,3 +56,57 @@ impl<'a, D: Doc> Replacer for Node<'a, D> { self.root.doc.get_source().get_range(range).to_vec() } } + +enum MetaVarExtract { + /// $A for captured meta var + Single(MetaVariableID), + /// $$$A for captured ellipsis + Multiple(MetaVariableID), + Transformed(MetaVariableID), +} + +impl MetaVarExtract { + fn from(value: MetaVariable) -> Option { + match value { + MetaVariable::Named(n, _) => Some(MetaVarExtract::Single(n)), + MetaVariable::NamedEllipsis(n) => Some(MetaVarExtract::Multiple(n)), + _ => None, + } + } +} + +fn split_first_meta_var<'a>( + mut src: &'a str, + meta_char: char, + transform: &[MetaVariableID], +) -> Option<(MetaVarExtract, &'a str)> { + debug_assert!(src.starts_with(meta_char)); + let mut i = 0; + let (trimmed, is_multi) = loop { + i += 1; + src = &src[meta_char.len_utf8()..]; + if i == 3 { + break (src, true); + } + if !src.starts_with(meta_char) { + break (src, false); + } + }; + // no Anonymous meta var allowed, so _ is not allowed + let i = trimmed + .find(|c: char| !c.is_ascii_uppercase()) + .unwrap_or(trimmed.len()); + // no name found + if i == 0 { + return None; + } + let name = trimmed[..i].to_string(); + let var = if is_multi { + MetaVarExtract::Multiple(name) + } else if transform.contains(&name) { + MetaVarExtract::Transformed(name) + } else { + MetaVarExtract::Single(name) + }; + Some((var, &trimmed[i..])) +} diff --git a/crates/core/src/replacer/structural.rs b/crates/core/src/replacer/structural.rs index 41f3156e15..2e88142f29 100644 --- a/crates/core/src/replacer/structural.rs +++ b/crates/core/src/replacer/structural.rs @@ -1,7 +1,7 @@ -use super::{Edit, Underlying}; +use super::{Edit, MetaVarExtract, Underlying}; use crate::language::Language; use crate::matcher::NodeMatch; -use crate::meta_var::{MatchResult, MetaVarEnv}; +use crate::meta_var::MetaVarEnv; use crate::source::Content; use crate::{Doc, Node, Root}; @@ -88,14 +88,19 @@ fn get_meta_var_replacement( return None; } let meta_var = lang.extract_meta_var(&node.text())?; - let replaced = match env.get(&meta_var)? { - MatchResult::Single(replaced) => replaced - .root - .doc - .get_source() - .get_range(replaced.range()) - .to_vec(), - MatchResult::Multi(nodes) => { + let extract = MetaVarExtract::from(meta_var)?; + let replaced = match extract { + MetaVarExtract::Single(name) => { + let replaced = env.get_match(&name)?; + replaced + .root + .doc + .get_source() + .get_range(replaced.range()) + .to_vec() + } + MetaVarExtract::Multiple(name) => { + let nodes = env.get_multiple_matches(&name); if nodes.is_empty() { vec![] } else { @@ -112,6 +117,7 @@ fn get_meta_var_replacement( .to_vec() } } + MetaVarExtract::Transformed(_) => return None, }; Some(replaced) } diff --git a/crates/core/src/replacer/template.rs b/crates/core/src/replacer/template.rs index 202b3b85b1..c8aae9b747 100644 --- a/crates/core/src/replacer/template.rs +++ b/crates/core/src/replacer/template.rs @@ -1,12 +1,13 @@ use super::indent::{ extract_with_deindent, get_indent_at_offset, indent_lines, DeindentedExtract, IndentSensitive, }; -use super::{Replacer, Underlying}; +use super::{split_first_meta_var, MetaVarExtract, Replacer, Underlying}; use crate::language::Language; use crate::matcher::NodeMatch; -use crate::meta_var::{split_first_meta_var, MatchResult, MetaVarEnv, MetaVariable}; +use crate::meta_var::MetaVarEnv; use crate::source::{Content, Doc}; +use std::borrow::Cow; use thiserror::Error; pub enum Fixer { @@ -20,7 +21,7 @@ pub enum FixerError {} impl Fixer { pub fn try_new(template: &str, lang: &L) -> Result { - Ok(create_fixer(template, lang.meta_var_char())) + Ok(create_fixer(template, lang.meta_var_char(), &[])) } } @@ -42,15 +43,21 @@ type Indent = usize; pub struct Template { fragments: Vec>, - vars: Vec<(MetaVariable, Indent)>, + vars: Vec<(MetaVarExtract, Indent)>, } -fn create_fixer(mut template: &str, mv_char: char) -> Fixer { +fn create_fixer( + mut template: &str, + mv_char: char, + transforms: &[String], +) -> Fixer { let mut fragments = vec![]; let mut vars = vec![]; let mut offset = 0; while let Some(i) = template[offset..].find(mv_char) { - if let Some((meta_var, remaining)) = split_first_meta_var(&template[offset + i..], mv_char) { + if let Some((meta_var, remaining)) = + split_first_meta_var(&template[offset + i..], mv_char, transforms) + { fragments.push(C::decode_str(&template[..offset + i]).into_owned()); let indent = get_indent_at_offset::(template[..offset + i].as_bytes()); vars.push((meta_var, indent)); @@ -90,30 +97,7 @@ where ret.extend_from_slice(frag); } for ((var, indent), frag) in vars.zip(frags) { - if let Some(matched) = env.get(var) { - // TODO: abstract this with structral - let (source, range) = match matched { - MatchResult::Single(replaced) => { - let source = replaced.root.doc.get_source(); - let range = replaced.range(); - (source, range) - } - MatchResult::Multi(nodes) => { - if nodes.is_empty() { - continue; - } else { - // NOTE: start_byte is not always index range of source's slice. - // e.g. start_byte is still byte_offset in utf_16 (napi). start_byte - // so we need to call source's get_range method - let start = nodes[0].inner.start_byte() as usize; - let end = nodes[nodes.len() - 1].inner.end_byte() as usize; - let source = nodes[0].root.doc.get_source(); - (source, start..end) - } - } - }; - let extracted = extract_with_deindent(source, range.clone()); - let bytes = indent_lines::(*indent, extracted); + if let Some(bytes) = maybe_get_var(env, var, indent) { ret.extend_from_slice(&bytes); } ret.extend_from_slice(frag); @@ -121,12 +105,51 @@ where ret } +fn maybe_get_var<'e, C, D>( + env: &'e MetaVarEnv, + var: &MetaVarExtract, + indent: &usize, +) -> Option> +where + C: IndentSensitive + 'e, + D: Doc, +{ + let (source, range) = match var { + MetaVarExtract::Transformed(name) => { + let source = env.get_transformed(name)?; + return Some(Cow::Borrowed(source)); + } + MetaVarExtract::Single(name) => { + let replaced = env.get_match(name)?; + let source = replaced.root.doc.get_source(); + let range = replaced.range(); + (source, range) + } + MetaVarExtract::Multiple(name) => { + let nodes = env.get_multiple_matches(name); + if nodes.is_empty() { + return None; + } + // NOTE: start_byte is not always index range of source's slice. + // e.g. start_byte is still byte_offset in utf_16 (napi). start_byte + // so we need to call source's get_range method + let start = nodes[0].inner.start_byte() as usize; + let end = nodes[nodes.len() - 1].inner.end_byte() as usize; + let source = nodes[0].root.doc.get_source(); + (source, start..end) + } + }; + let extracted = extract_with_deindent(source, range); + let bytes = indent_lines::(*indent, extracted); + Some(bytes) +} + // replace meta_var in template string, e.g. "Hello $NAME" -> "Hello World" pub fn gen_replacement(template: &str, nm: &NodeMatch) -> Underlying where D::Source: IndentSensitive, { - let fixer = create_fixer(template, nm.lang().meta_var_char()); + let fixer = create_fixer(template, nm.lang().meta_var_char(), &[]); fixer.generate_replacement(nm) }