Skip to content

Commit

Permalink
feat: add support for transform
Browse files Browse the repository at this point in the history
Part of #436
  • Loading branch information
HerringtonDarkholme committed Jun 10, 2023
1 parent 3c0276e commit 47feee1
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 93 deletions.
64 changes: 12 additions & 52 deletions crates/core/src/meta_var.rs
@@ -1,24 +1,28 @@
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<D> = Vec<<<D as Doc>::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<MetaVariableID, Node<'tree, D>>,
multi_matched: HashMap<MetaVariableID, Vec<Node<'tree, D>>>,
transformed_var: HashMap<MetaVariableID, Underlying<D>>,
}

impl<'tree, D: Doc> MetaVarEnv<'tree, D> {
pub fn new() -> Self {
Self {
single_matched: HashMap::new(),
multi_matched: HashMap::new(),
transformed_var: HashMap::new(),
}
}

Expand All @@ -39,14 +43,6 @@ impl<'tree, D: Doc> MetaVarEnv<'tree, D> {
Some(self)
}

pub fn get(&self, var: &MetaVariable) -> Option<MatchResult<'_, 'tree, D>> {
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)
}
Expand All @@ -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<D>> {
self.transformed_var.get(var)
}

pub fn add_label(&mut self, label: &str, node: Node<'tree, D>) {
self
.multi_matched
Expand Down Expand Up @@ -101,6 +101,10 @@ impl<'tree, D: Doc> MetaVarEnv<'tree, D> {
}
true
}

pub fn insert_transformation(&mut self, name: MetaVariableID, src: Underlying<D>) {
self.transformed_var.insert(name, src);
}
}

impl<'tree, D: Doc> Default for MetaVarEnv<'tree, D> {
Expand All @@ -124,13 +128,6 @@ impl<'tree, D: Doc> From<MetaVarEnv<'tree, D>> for HashMap<String, String> {
}
}

pub enum MatchResult<'a, 'tree, D: Doc> {
/// $A for captured meta var
Single(&'a Node<'tree, D>),
/// $$$A for captured ellipsis
Multi(&'a Vec<Node<'tree, D>>),
}

#[derive(Debug, PartialEq, Eq)]
pub enum MetaVariable {
/// $A for captured meta var
Expand All @@ -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<D: Doc>(HashMap<MetaVariableID, MetaVarMatcher<D>>);
Expand Down Expand Up @@ -230,36 +220,6 @@ pub(crate) fn extract_meta_var(src: &str, meta_char: char) -> Option<MetaVariabl
}
}

pub fn split_first_meta_var(mut src: &str, meta_char: char) -> 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' | '_')
}
Expand Down
55 changes: 55 additions & 0 deletions 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};
Expand Down Expand Up @@ -55,3 +56,57 @@ impl<'a, D: Doc> Replacer<D> 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<Self> {
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..]))
}
26 changes: 16 additions & 10 deletions 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};

Expand Down Expand Up @@ -88,14 +88,19 @@ fn get_meta_var_replacement<D: Doc>(
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 {
Expand All @@ -112,6 +117,7 @@ fn get_meta_var_replacement<D: Doc>(
.to_vec()
}
}
MetaVarExtract::Transformed(_) => return None,
};
Some(replaced)
}
Expand Down
85 changes: 54 additions & 31 deletions 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<C: IndentSensitive> {
Expand All @@ -20,7 +21,7 @@ pub enum FixerError {}

impl<C: IndentSensitive> Fixer<C> {
pub fn try_new<L: Language>(template: &str, lang: &L) -> Result<Self, FixerError> {
Ok(create_fixer(template, lang.meta_var_char()))
Ok(create_fixer(template, lang.meta_var_char(), &[]))
}
}

Expand All @@ -42,15 +43,21 @@ type Indent = usize;

pub struct Template<C: IndentSensitive> {
fragments: Vec<Vec<C::Underlying>>,
vars: Vec<(MetaVariable, Indent)>,
vars: Vec<(MetaVarExtract, Indent)>,
}

fn create_fixer<C: IndentSensitive>(mut template: &str, mv_char: char) -> Fixer<C> {
fn create_fixer<C: IndentSensitive>(
mut template: &str,
mv_char: char,
transforms: &[String],
) -> Fixer<C> {
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::<String>(template[..offset + i].as_bytes());
vars.push((meta_var, indent));
Expand Down Expand Up @@ -90,43 +97,59 @@ 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::<D::Source>(*indent, extracted);
if let Some(bytes) = maybe_get_var(env, var, indent) {
ret.extend_from_slice(&bytes);
}
ret.extend_from_slice(frag);
}
ret
}

fn maybe_get_var<'e, C, D>(
env: &'e MetaVarEnv<D>,
var: &MetaVarExtract,
indent: &usize,
) -> Option<Cow<'e, [C::Underlying]>>
where
C: IndentSensitive + 'e,
D: Doc<Source = C>,
{
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::<D::Source>(*indent, extracted);
Some(bytes)
}

// replace meta_var in template string, e.g. "Hello $NAME" -> "Hello World"
pub fn gen_replacement<D: Doc>(template: &str, nm: &NodeMatch<D>) -> Underlying<D::Source>
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)
}

Expand Down

0 comments on commit 47feee1

Please sign in to comment.