ok/jj
1
0
Fork 0
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:
Yuya Nishihara 2024-03-14 19:42:08 +09:00
parent 85f2f3a439
commit 82b6d073f1
6 changed files with 147 additions and 110 deletions

View file

@ -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(

View file

@ -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(

View file

@ -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(

View file

@ -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.

View file

@ -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>,

View file

@ -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