forked from mirrors/jj
templater: migrate global functions to table-based lookup
The original plan was to extend the globals table to implement "revset(expr)". I'm not sure if that's more discoverable than "self.contained_in(revset_expr)" method, but we can decide that later. Anyways, this patch adds typo suggestion for global functions.
This commit is contained in:
parent
85f2f3a439
commit
82b6d073f1
6 changed files with 147 additions and 110 deletions
|
@ -112,7 +112,8 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> {
|
|||
build_ctx: &BuildContext<Self::Property>,
|
||||
function: &FunctionCallNode,
|
||||
) -> TemplateParseResult<Self::Property> {
|
||||
template_builder::build_global_function(self, build_ctx, function)
|
||||
let table = &self.build_fn_table.core;
|
||||
table.build_function(self, build_ctx, function)
|
||||
}
|
||||
|
||||
fn build_method(
|
||||
|
|
|
@ -88,7 +88,8 @@ impl<'a, C: 'a> TemplateLanguage<'a> for GenericTemplateLanguage<'a, C> {
|
|||
build_ctx: &BuildContext<Self::Property>,
|
||||
function: &FunctionCallNode,
|
||||
) -> TemplateParseResult<Self::Property> {
|
||||
template_builder::build_global_function(self, build_ctx, function)
|
||||
let table = &self.build_fn_table.core;
|
||||
table.build_function(self, build_ctx, function)
|
||||
}
|
||||
|
||||
fn build_method(
|
||||
|
|
|
@ -64,7 +64,8 @@ impl TemplateLanguage<'static> for OperationTemplateLanguage {
|
|||
build_ctx: &BuildContext<Self::Property>,
|
||||
function: &FunctionCallNode,
|
||||
) -> TemplateParseResult<Self::Property> {
|
||||
template_builder::build_global_function(self, build_ctx, function)
|
||||
let table = &self.build_fn_table.core;
|
||||
table.build_function(self, build_ctx, function)
|
||||
}
|
||||
|
||||
fn build_method(
|
||||
|
|
|
@ -74,6 +74,9 @@ pub trait TemplateLanguage<'a> {
|
|||
fn build_self(&self) -> Self::Property;
|
||||
|
||||
/// Translates the given global `function` call to a property.
|
||||
///
|
||||
/// This should be delegated to
|
||||
/// `CoreTemplateBuildFnTable::build_function()`.
|
||||
fn build_function(
|
||||
&self,
|
||||
build_ctx: &BuildContext<Self::Property>,
|
||||
|
@ -233,12 +236,20 @@ impl<'a, I: 'a> IntoTemplateProperty<'a, I> for CoreTemplatePropertyKind<'a, I>
|
|||
}
|
||||
}
|
||||
|
||||
/// Function that translates method call node of self type `T`.
|
||||
/// Function that translates global function call node.
|
||||
// The lifetime parameter 'a could be replaced with for<'a> to keep the method
|
||||
// table away from a certain lifetime. That's technically more correct, but I
|
||||
// couldn't find an easy way to expand that to the core template methods, which
|
||||
// are defined for L: TemplateLanguage<'a>. That's why the build fn table is
|
||||
// bound to a named lifetime, and therefore can't be cached statically.
|
||||
pub type TemplateBuildFunctionFn<'a, L> =
|
||||
fn(
|
||||
&L,
|
||||
&BuildContext<<L as TemplateLanguage<'a>>::Property>,
|
||||
&FunctionCallNode,
|
||||
) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>;
|
||||
|
||||
/// Function that translates method call node of self type `T`.
|
||||
pub type TemplateBuildMethodFn<'a, L, T> =
|
||||
fn(
|
||||
&L,
|
||||
|
@ -247,19 +258,22 @@ pub type TemplateBuildMethodFn<'a, L, T> =
|
|||
&FunctionCallNode,
|
||||
) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>;
|
||||
|
||||
/// Table of functions that translate global function call node.
|
||||
pub type TemplateBuildFunctionFnMap<'a, L> = HashMap<&'static str, TemplateBuildFunctionFn<'a, L>>;
|
||||
|
||||
/// Table of functions that translate method call node of self type `T`.
|
||||
pub type TemplateBuildMethodFnMap<'a, L, T> =
|
||||
HashMap<&'static str, TemplateBuildMethodFn<'a, L, T>>;
|
||||
|
||||
/// Symbol table of methods available in the core template.
|
||||
/// Symbol table of functions and methods available in the core template.
|
||||
pub struct CoreTemplateBuildFnTable<'a, L: TemplateLanguage<'a> + ?Sized> {
|
||||
pub functions: TemplateBuildFunctionFnMap<'a, L>,
|
||||
pub string_methods: TemplateBuildMethodFnMap<'a, L, String>,
|
||||
pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>,
|
||||
pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64>,
|
||||
pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>,
|
||||
pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>,
|
||||
pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>,
|
||||
// TODO: add global functions table?
|
||||
}
|
||||
|
||||
pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'s str, F>) {
|
||||
|
@ -271,9 +285,10 @@ pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'
|
|||
}
|
||||
|
||||
impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
|
||||
/// Creates new symbol table containing the builtin methods.
|
||||
/// Creates new symbol table containing the builtin functions and methods.
|
||||
pub fn builtin() -> Self {
|
||||
CoreTemplateBuildFnTable {
|
||||
functions: builtin_functions(),
|
||||
string_methods: builtin_string_methods(),
|
||||
boolean_methods: HashMap::new(),
|
||||
integer_methods: HashMap::new(),
|
||||
|
@ -285,6 +300,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
|
|||
|
||||
pub fn empty() -> Self {
|
||||
CoreTemplateBuildFnTable {
|
||||
functions: HashMap::new(),
|
||||
string_methods: HashMap::new(),
|
||||
boolean_methods: HashMap::new(),
|
||||
integer_methods: HashMap::new(),
|
||||
|
@ -296,6 +312,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
|
|||
|
||||
pub fn merge(&mut self, extension: CoreTemplateBuildFnTable<'a, L>) {
|
||||
let CoreTemplateBuildFnTable {
|
||||
functions,
|
||||
string_methods,
|
||||
boolean_methods,
|
||||
integer_methods,
|
||||
|
@ -304,6 +321,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
|
|||
timestamp_range_methods,
|
||||
} = extension;
|
||||
|
||||
merge_fn_map(&mut self.functions, functions);
|
||||
merge_fn_map(&mut self.string_methods, string_methods);
|
||||
merge_fn_map(&mut self.boolean_methods, boolean_methods);
|
||||
merge_fn_map(&mut self.integer_methods, integer_methods);
|
||||
|
@ -312,6 +330,18 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
|
|||
merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
|
||||
}
|
||||
|
||||
/// Translates the function call node `function` by using this symbol table.
|
||||
pub fn build_function(
|
||||
&self,
|
||||
language: &L,
|
||||
build_ctx: &BuildContext<L::Property>,
|
||||
function: &FunctionCallNode,
|
||||
) -> TemplateParseResult<L::Property> {
|
||||
let table = &self.functions;
|
||||
let build = template_parser::lookup_function(table, function)?;
|
||||
build(language, build_ctx, function)
|
||||
}
|
||||
|
||||
/// Applies the method call node `function` to the given `property` by using
|
||||
/// this symbol table.
|
||||
pub fn build_method(
|
||||
|
@ -895,92 +925,88 @@ where
|
|||
Ok(language.wrap_list_template(Box::new(list_template)))
|
||||
}
|
||||
|
||||
pub fn build_global_function<'a, L: TemplateLanguage<'a> + ?Sized>(
|
||||
language: &L,
|
||||
build_ctx: &BuildContext<L::Property>,
|
||||
function: &FunctionCallNode,
|
||||
) -> TemplateParseResult<L::Property> {
|
||||
match function.name {
|
||||
"fill" => {
|
||||
let [width_node, content_node] = template_parser::expect_exact_arguments(function)?;
|
||||
let width = expect_usize_expression(language, build_ctx, width_node)?;
|
||||
let content = expect_template_expression(language, build_ctx, content_node)?;
|
||||
let template = ReformatTemplate::new(content, move |context, formatter, recorded| {
|
||||
match width.extract(context) {
|
||||
Ok(width) => text_util::write_wrapped(formatter, recorded, width),
|
||||
Err(err) => err.format(&(), formatter),
|
||||
}
|
||||
});
|
||||
Ok(language.wrap_template(Box::new(template)))
|
||||
}
|
||||
"indent" => {
|
||||
let [prefix_node, content_node] = template_parser::expect_exact_arguments(function)?;
|
||||
let prefix = expect_template_expression(language, build_ctx, prefix_node)?;
|
||||
let content = expect_template_expression(language, build_ctx, content_node)?;
|
||||
let template = ReformatTemplate::new(content, move |context, formatter, recorded| {
|
||||
text_util::write_indented(formatter, recorded, |formatter| {
|
||||
prefix.format(context, formatter)
|
||||
})
|
||||
});
|
||||
Ok(language.wrap_template(Box::new(template)))
|
||||
}
|
||||
"label" => {
|
||||
let [label_node, content_node] = template_parser::expect_exact_arguments(function)?;
|
||||
let label_property = expect_plain_text_expression(language, build_ctx, label_node)?;
|
||||
let content = expect_template_expression(language, build_ctx, content_node)?;
|
||||
let labels = TemplateFunction::new(label_property, |s| {
|
||||
Ok(s.split_whitespace().map(ToString::to_string).collect())
|
||||
});
|
||||
Ok(language.wrap_template(Box::new(LabelTemplate::new(content, labels))))
|
||||
}
|
||||
"if" => {
|
||||
let ([condition_node, true_node], [false_node]) =
|
||||
template_parser::expect_arguments(function)?;
|
||||
let condition = expect_boolean_expression(language, build_ctx, condition_node)?;
|
||||
let true_template = expect_template_expression(language, build_ctx, true_node)?;
|
||||
let false_template = false_node
|
||||
.map(|node| expect_template_expression(language, build_ctx, node))
|
||||
.transpose()?;
|
||||
let template = ConditionalTemplate::new(condition, true_template, false_template);
|
||||
Ok(language.wrap_template(Box::new(template)))
|
||||
}
|
||||
"concat" => {
|
||||
let contents = function
|
||||
.args
|
||||
.iter()
|
||||
.map(|node| expect_template_expression(language, build_ctx, node))
|
||||
.try_collect()?;
|
||||
Ok(language.wrap_template(Box::new(ConcatTemplate(contents))))
|
||||
}
|
||||
"separate" => {
|
||||
let ([separator_node], content_nodes) =
|
||||
template_parser::expect_some_arguments(function)?;
|
||||
let separator = expect_template_expression(language, build_ctx, separator_node)?;
|
||||
let contents = content_nodes
|
||||
.iter()
|
||||
.map(|node| expect_template_expression(language, build_ctx, node))
|
||||
.try_collect()?;
|
||||
Ok(language.wrap_template(Box::new(SeparateTemplate::new(separator, contents))))
|
||||
}
|
||||
"surround" => {
|
||||
let [prefix_node, suffix_node, content_node] =
|
||||
template_parser::expect_exact_arguments(function)?;
|
||||
let prefix = expect_template_expression(language, build_ctx, prefix_node)?;
|
||||
let suffix = expect_template_expression(language, build_ctx, suffix_node)?;
|
||||
let content = expect_template_expression(language, build_ctx, content_node)?;
|
||||
let template = ReformatTemplate::new(content, move |context, formatter, recorded| {
|
||||
if recorded.data().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
prefix.format(context, formatter)?;
|
||||
recorded.replay(formatter)?;
|
||||
suffix.format(context, formatter)?;
|
||||
Ok(())
|
||||
});
|
||||
Ok(language.wrap_template(Box::new(template)))
|
||||
}
|
||||
_ => Err(TemplateParseError::no_such_function(function)),
|
||||
}
|
||||
fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
|
||||
// Not using maplit::hashmap!{} or custom declarative macro here because
|
||||
// code completion inside macro is quite restricted.
|
||||
let mut map = TemplateBuildFunctionFnMap::<L>::new();
|
||||
map.insert("fill", |language, build_ctx, function| {
|
||||
let [width_node, content_node] = template_parser::expect_exact_arguments(function)?;
|
||||
let width = expect_usize_expression(language, build_ctx, width_node)?;
|
||||
let content = expect_template_expression(language, build_ctx, content_node)?;
|
||||
let template = ReformatTemplate::new(content, move |context, formatter, recorded| {
|
||||
match width.extract(context) {
|
||||
Ok(width) => text_util::write_wrapped(formatter, recorded, width),
|
||||
Err(err) => err.format(&(), formatter),
|
||||
}
|
||||
});
|
||||
Ok(language.wrap_template(Box::new(template)))
|
||||
});
|
||||
map.insert("indent", |language, build_ctx, function| {
|
||||
let [prefix_node, content_node] = template_parser::expect_exact_arguments(function)?;
|
||||
let prefix = expect_template_expression(language, build_ctx, prefix_node)?;
|
||||
let content = expect_template_expression(language, build_ctx, content_node)?;
|
||||
let template = ReformatTemplate::new(content, move |context, formatter, recorded| {
|
||||
text_util::write_indented(formatter, recorded, |formatter| {
|
||||
prefix.format(context, formatter)
|
||||
})
|
||||
});
|
||||
Ok(language.wrap_template(Box::new(template)))
|
||||
});
|
||||
map.insert("label", |language, build_ctx, function| {
|
||||
let [label_node, content_node] = template_parser::expect_exact_arguments(function)?;
|
||||
let label_property = expect_plain_text_expression(language, build_ctx, label_node)?;
|
||||
let content = expect_template_expression(language, build_ctx, content_node)?;
|
||||
let labels = TemplateFunction::new(label_property, |s| {
|
||||
Ok(s.split_whitespace().map(ToString::to_string).collect())
|
||||
});
|
||||
Ok(language.wrap_template(Box::new(LabelTemplate::new(content, labels))))
|
||||
});
|
||||
map.insert("if", |language, build_ctx, function| {
|
||||
let ([condition_node, true_node], [false_node]) =
|
||||
template_parser::expect_arguments(function)?;
|
||||
let condition = expect_boolean_expression(language, build_ctx, condition_node)?;
|
||||
let true_template = expect_template_expression(language, build_ctx, true_node)?;
|
||||
let false_template = false_node
|
||||
.map(|node| expect_template_expression(language, build_ctx, node))
|
||||
.transpose()?;
|
||||
let template = ConditionalTemplate::new(condition, true_template, false_template);
|
||||
Ok(language.wrap_template(Box::new(template)))
|
||||
});
|
||||
map.insert("concat", |language, build_ctx, function| {
|
||||
let contents = function
|
||||
.args
|
||||
.iter()
|
||||
.map(|node| expect_template_expression(language, build_ctx, node))
|
||||
.try_collect()?;
|
||||
Ok(language.wrap_template(Box::new(ConcatTemplate(contents))))
|
||||
});
|
||||
map.insert("separate", |language, build_ctx, function| {
|
||||
let ([separator_node], content_nodes) = template_parser::expect_some_arguments(function)?;
|
||||
let separator = expect_template_expression(language, build_ctx, separator_node)?;
|
||||
let contents = content_nodes
|
||||
.iter()
|
||||
.map(|node| expect_template_expression(language, build_ctx, node))
|
||||
.try_collect()?;
|
||||
Ok(language.wrap_template(Box::new(SeparateTemplate::new(separator, contents))))
|
||||
});
|
||||
map.insert("surround", |language, build_ctx, function| {
|
||||
let [prefix_node, suffix_node, content_node] =
|
||||
template_parser::expect_exact_arguments(function)?;
|
||||
let prefix = expect_template_expression(language, build_ctx, prefix_node)?;
|
||||
let suffix = expect_template_expression(language, build_ctx, suffix_node)?;
|
||||
let content = expect_template_expression(language, build_ctx, content_node)?;
|
||||
let template = ReformatTemplate::new(content, move |context, formatter, recorded| {
|
||||
if recorded.data().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
prefix.format(context, formatter)?;
|
||||
recorded.replay(formatter)?;
|
||||
suffix.format(context, formatter)?;
|
||||
Ok(())
|
||||
});
|
||||
Ok(language.wrap_template(Box::new(template)))
|
||||
});
|
||||
map
|
||||
}
|
||||
|
||||
/// Builds intermediate expression tree from AST nodes.
|
||||
|
|
|
@ -136,17 +136,6 @@ impl TemplateParseError {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: migrate callers to something like lookup_method()
|
||||
pub(crate) fn no_such_function(function: &FunctionCallNode) -> Self {
|
||||
TemplateParseError::with_span(
|
||||
TemplateParseErrorKind::NoSuchFunction {
|
||||
name: function.name.to_owned(),
|
||||
candidates: vec![],
|
||||
},
|
||||
function.name_span,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: migrate all callers to table-based lookup_method()
|
||||
pub(crate) fn no_such_method(
|
||||
type_name: impl Into<String>,
|
||||
|
@ -916,6 +905,25 @@ pub fn expect_lambda_with<'a, 'i, T>(
|
|||
}
|
||||
}
|
||||
|
||||
/// Looks up `table` by the given function name.
|
||||
pub fn lookup_function<'a, V>(
|
||||
table: &'a HashMap<&str, V>,
|
||||
function: &FunctionCallNode,
|
||||
) -> TemplateParseResult<&'a V> {
|
||||
if let Some(value) = table.get(function.name) {
|
||||
Ok(value)
|
||||
} else {
|
||||
let candidates = collect_similar(function.name, table.keys());
|
||||
Err(TemplateParseError::with_span(
|
||||
TemplateParseErrorKind::NoSuchFunction {
|
||||
name: function.name.to_owned(),
|
||||
candidates,
|
||||
},
|
||||
function.name_span,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up `table` by the given method name.
|
||||
pub fn lookup_method<'a, V>(
|
||||
type_name: impl Into<String>,
|
||||
|
|
|
@ -38,7 +38,7 @@ fn test_templater_parse_error() {
|
|||
[template-aliases]
|
||||
'conflicting' = ''
|
||||
'shorted()' = ''
|
||||
'cap(x)' = 'x'
|
||||
'socat(x)' = 'x'
|
||||
'format_id(id)' = 'id.sort()'
|
||||
"###,
|
||||
);
|
||||
|
@ -60,14 +60,14 @@ fn test_templater_parse_error() {
|
|||
= Method "shorter" doesn't exist for type "CommitOrChangeId"
|
||||
Hint: Did you mean "short", "shortest"?
|
||||
"###);
|
||||
insta::assert_snapshot!(render_err(r#"cat()"#), @r###"
|
||||
insta::assert_snapshot!(render_err(r#"oncat()"#), @r###"
|
||||
Error: Failed to parse template: --> 1:1
|
||||
|
|
||||
1 | cat()
|
||||
| ^-^
|
||||
1 | oncat()
|
||||
| ^---^
|
||||
|
|
||||
= Function "cat" doesn't exist
|
||||
Hint: Did you mean "cap"?
|
||||
= Function "oncat" doesn't exist
|
||||
Hint: Did you mean "concat", "socat"?
|
||||
"###);
|
||||
insta::assert_snapshot!(render_err(r#""".lines().map(|s| se)"#), @r###"
|
||||
Error: Failed to parse template: --> 1:20
|
||||
|
|
Loading…
Reference in a new issue