Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

templater: prep for "branch list -Ttemplate" + minor cleanup #3648

Merged
merged 4 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions cli/src/commit_templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ use crate::template_builder::{
};
use crate::template_parser::{self, FunctionCallNode, TemplateParseError, TemplateParseResult};
use crate::templater::{
self, IntoTemplate, PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty,
TemplatePropertyError, TemplatePropertyExt as _,
self, PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty,
TemplatePropertyExt as _,
};
use crate::{revset_util, text_util};

Expand Down Expand Up @@ -130,9 +130,7 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> {
let type_name = "Commit";
let table = &self.build_fn_table.commit_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
let inner_property = property.and_then(|opt| {
opt.ok_or_else(|| TemplatePropertyError("No commit available".into()))
});
let inner_property = property.try_unwrap(type_name);
build(self, build_ctx, Box::new(inner_property), function)
}
CommitTemplatePropertyKind::CommitList(property) => {
Expand All @@ -154,9 +152,7 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> {
let type_name = "RefName";
let table = &self.build_fn_table.ref_name_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
let inner_property = property.and_then(|opt| {
opt.ok_or_else(|| TemplatePropertyError("No RefName available".into()))
});
let inner_property = property.try_unwrap(type_name);
build(self, build_ctx, Box::new(inner_property), function)
}
CommitTemplatePropertyKind::RefNameList(property) => {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/operation_templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use crate::template_builder::{
};
use crate::template_parser::{self, FunctionCallNode, TemplateParseResult};
use crate::templater::{
IntoTemplate, PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty,
PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty,
TemplatePropertyExt as _, TimestampRange,
};

Expand Down
110 changes: 107 additions & 3 deletions cli/src/template_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ use crate::template_parser::{
TemplateParseError, TemplateParseErrorKind, TemplateParseResult, UnaryOp,
};
use crate::templater::{
CoalesceTemplate, ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate,
ListPropertyTemplate, ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder,
ReformatTemplate, SeparateTemplate, Template, TemplateProperty, TemplatePropertyError,
CoalesceTemplate, ConcatTemplate, ConditionalTemplate, LabelTemplate, ListPropertyTemplate,
ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, ReformatTemplate,
SeparateTemplate, SizeHint, Template, TemplateProperty, TemplatePropertyError,
TemplatePropertyExt as _, TemplateRenderer, TimestampRange,
};
use crate::{text_util, time_util};
Expand All @@ -39,7 +39,11 @@ pub trait TemplateLanguage<'a> {
) -> Self::Property;
fn wrap_boolean(property: impl TemplateProperty<Output = bool> + 'a) -> Self::Property;
fn wrap_integer(property: impl TemplateProperty<Output = i64> + 'a) -> Self::Property;
fn wrap_integer_opt(
property: impl TemplateProperty<Output = Option<i64>> + 'a,
) -> Self::Property;
fn wrap_signature(property: impl TemplateProperty<Output = Signature> + 'a) -> Self::Property;
fn wrap_size_hint(property: impl TemplateProperty<Output = SizeHint> + 'a) -> Self::Property;
fn wrap_timestamp(property: impl TemplateProperty<Output = Timestamp> + 'a) -> Self::Property;
fn wrap_timestamp_range(
property: impl TemplateProperty<Output = TimestampRange> + 'a,
Expand Down Expand Up @@ -81,7 +85,9 @@ macro_rules! impl_core_wrap_property_fns {
wrap_string_list(Vec<String>) => StringList,
wrap_boolean(bool) => Boolean,
wrap_integer(i64) => Integer,
wrap_integer_opt(Option<i64>) => IntegerOpt,
wrap_signature(jj_lib::backend::Signature) => Signature,
wrap_size_hint($crate::templater::SizeHint) => SizeHint,
wrap_timestamp(jj_lib::backend::Timestamp) => Timestamp,
wrap_timestamp_range($crate::templater::TimestampRange) => TimestampRange,
}
Expand Down Expand Up @@ -133,7 +139,9 @@ pub enum CoreTemplatePropertyKind<'a> {
StringList(Box<dyn TemplateProperty<Output = Vec<String>> + 'a>),
Boolean(Box<dyn TemplateProperty<Output = bool> + 'a>),
Integer(Box<dyn TemplateProperty<Output = i64> + 'a>),
IntegerOpt(Box<dyn TemplateProperty<Output = Option<i64>> + 'a>),
Signature(Box<dyn TemplateProperty<Output = Signature> + 'a>),
SizeHint(Box<dyn TemplateProperty<Output = SizeHint> + 'a>),
Timestamp(Box<dyn TemplateProperty<Output = Timestamp> + 'a>),
TimestampRange(Box<dyn TemplateProperty<Output = TimestampRange> + 'a>),

Expand All @@ -158,7 +166,9 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> {
CoreTemplatePropertyKind::StringList(_) => "List<String>",
CoreTemplatePropertyKind::Boolean(_) => "Boolean",
CoreTemplatePropertyKind::Integer(_) => "Integer",
CoreTemplatePropertyKind::IntegerOpt(_) => "Option<Integer>",
CoreTemplatePropertyKind::Signature(_) => "Signature",
CoreTemplatePropertyKind::SizeHint(_) => "SizeHint",
CoreTemplatePropertyKind::Timestamp(_) => "Timestamp",
CoreTemplatePropertyKind::TimestampRange(_) => "TimestampRange",
CoreTemplatePropertyKind::Template(_) => "Template",
Expand All @@ -176,7 +186,11 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> {
}
CoreTemplatePropertyKind::Boolean(property) => Some(property),
CoreTemplatePropertyKind::Integer(_) => None,
CoreTemplatePropertyKind::IntegerOpt(property) => {
Some(Box::new(property.map(|opt| opt.is_some())))
}
CoreTemplatePropertyKind::Signature(_) => None,
CoreTemplatePropertyKind::SizeHint(_) => None,
CoreTemplatePropertyKind::Timestamp(_) => None,
CoreTemplatePropertyKind::TimestampRange(_) => None,
// Template types could also be evaluated to boolean, but it's less likely
Expand All @@ -190,6 +204,9 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> {
fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'a>> {
match self {
CoreTemplatePropertyKind::Integer(property) => Some(property),
CoreTemplatePropertyKind::IntegerOpt(property) => {
Some(Box::new(property.try_unwrap("Integer")))
}
_ => None,
}
}
Expand All @@ -210,7 +227,9 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> {
CoreTemplatePropertyKind::StringList(property) => Some(property.into_template()),
CoreTemplatePropertyKind::Boolean(property) => Some(property.into_template()),
CoreTemplatePropertyKind::Integer(property) => Some(property.into_template()),
CoreTemplatePropertyKind::IntegerOpt(property) => Some(property.into_template()),
CoreTemplatePropertyKind::Signature(property) => Some(property.into_template()),
CoreTemplatePropertyKind::SizeHint(_) => None,
CoreTemplatePropertyKind::Timestamp(property) => Some(property.into_template()),
CoreTemplatePropertyKind::TimestampRange(property) => Some(property.into_template()),
CoreTemplatePropertyKind::Template(template) => Some(template),
Expand Down Expand Up @@ -255,6 +274,7 @@ pub struct CoreTemplateBuildFnTable<'a, L: TemplateLanguage<'a> + ?Sized> {
pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>,
pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64>,
pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>,
pub size_hint_methods: TemplateBuildMethodFnMap<'a, L, SizeHint>,
pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>,
pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>,
}
Expand All @@ -276,6 +296,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
boolean_methods: HashMap::new(),
integer_methods: HashMap::new(),
signature_methods: builtin_signature_methods(),
size_hint_methods: builtin_size_hint_methods(),
timestamp_methods: builtin_timestamp_methods(),
timestamp_range_methods: builtin_timestamp_range_methods(),
}
Expand All @@ -288,6 +309,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
boolean_methods: HashMap::new(),
integer_methods: HashMap::new(),
signature_methods: HashMap::new(),
size_hint_methods: HashMap::new(),
timestamp_methods: HashMap::new(),
timestamp_range_methods: HashMap::new(),
}
Expand All @@ -300,6 +322,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
boolean_methods,
integer_methods,
signature_methods,
size_hint_methods,
timestamp_methods,
timestamp_range_methods,
} = extension;
Expand All @@ -309,6 +332,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
merge_fn_map(&mut self.boolean_methods, boolean_methods);
merge_fn_map(&mut self.integer_methods, integer_methods);
merge_fn_map(&mut self.signature_methods, signature_methods);
merge_fn_map(&mut self.size_hint_methods, size_hint_methods);
merge_fn_map(&mut self.timestamp_methods, timestamp_methods);
merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
}
Expand Down Expand Up @@ -357,11 +381,23 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> {
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, build_ctx, property, function)
}
CoreTemplatePropertyKind::IntegerOpt(property) => {
let type_name = "Integer";
let table = &self.integer_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
let inner_property = property.try_unwrap(type_name);
build(language, build_ctx, Box::new(inner_property), function)
}
CoreTemplatePropertyKind::Signature(property) => {
let table = &self.signature_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, build_ctx, property, function)
}
CoreTemplatePropertyKind::SizeHint(property) => {
let table = &self.size_hint_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, build_ctx, property, function)
}
CoreTemplatePropertyKind::Timestamp(property) => {
let table = &self.timestamp_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
Expand Down Expand Up @@ -684,6 +720,38 @@ fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
map
}

fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
) -> TemplateBuildMethodFnMap<'a, L, SizeHint> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
map.insert("lower", |_language, _build_ctx, self_property, function| {
template_parser::expect_no_arguments(function)?;
let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
Ok(L::wrap_integer(out_property))
});
map.insert("upper", |_language, _build_ctx, self_property, function| {
template_parser::expect_no_arguments(function)?;
let out_property =
self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
Ok(L::wrap_integer_opt(out_property))
});
map.insert("exact", |_language, _build_ctx, self_property, function| {
template_parser::expect_no_arguments(function)?;
let out_property = self_property.and_then(|(lower, upper)| {
let exact = (Some(lower) == upper).then_some(lower);
Ok(exact.map(i64::try_from).transpose()?)
});
Ok(L::wrap_integer_opt(out_property))
});
map.insert("zero", |_language, _build_ctx, self_property, function| {
template_parser::expect_no_arguments(function)?;
let out_property = self_property.map(|(_, upper)| upper == Some(0));
Ok(L::wrap_boolean(out_property))
});
map
}

fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>(
) -> TemplateBuildMethodFnMap<'a, L, Timestamp> {
// Not using maplit::hashmap!{} or custom declarative macro here because
Expand Down Expand Up @@ -1491,6 +1559,12 @@ mod tests {
= Expected expression of type "Boolean", but actual type is "Integer"
"###);

// Optional integer can be converted to boolean, and Some(0) is truthy.
env.add_keyword("none_i64", || L::wrap_integer_opt(Literal(None)));
env.add_keyword("some_i64", || L::wrap_integer_opt(Literal(Some(0))));
insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");

insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r###"
--> 1:4
|
Expand All @@ -1512,12 +1586,19 @@ mod tests {
#[test]
fn test_arithmetic_operation() {
let mut env = TestTemplateEnv::new();
env.add_keyword("none_i64", || L::wrap_integer_opt(Literal(None)));
env.add_keyword("some_i64", || L::wrap_integer_opt(Literal(Some(1))));
env.add_keyword("i64_min", || L::wrap_integer(Literal(i64::MIN)));

insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");

// Since methods of the contained value can be invoked, it makes sense
// to apply operators to optional integers as well.
insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");

// No panic on integer overflow.
insta::assert_snapshot!(
env.render_ok(r#"-i64_min"#),
Expand Down Expand Up @@ -1814,6 +1895,29 @@ mod tests {
insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @"");
}

#[test]
fn test_size_hint_method() {
let mut env = TestTemplateEnv::new();

env.add_keyword("unbounded", || L::wrap_size_hint(Literal((5, None))));
insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");

env.add_keyword("bounded", || L::wrap_size_hint(Literal((0, Some(10)))));
insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");

env.add_keyword("zero", || L::wrap_size_hint(Literal((0, Some(0)))));
insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
}

#[test]
fn test_timestamp_method() {
let mut env = TestTemplateEnv::new();
Expand Down
38 changes: 25 additions & 13 deletions cli/src/templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ pub trait ListTemplate: Template {
Self: 'a;
}

pub trait IntoTemplate<'a> {
fn into_template(self) -> Box<dyn Template + 'a>;
}

impl<T: Template + ?Sized> Template for &T {
fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
<T as Template>::format(self, formatter)
Expand Down Expand Up @@ -78,6 +74,11 @@ impl Template for Signature {
}
}

// In template language, an integer value is represented as i64. However, we use
// usize here because it's more convenient to guarantee that the lower value is
// bounded to 0.
pub type SizeHint = (usize, Option<usize>);

impl Template for String {
fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
write!(formatter, "{self}")
Expand Down Expand Up @@ -352,6 +353,26 @@ pub trait TemplatePropertyExt: TemplateProperty {
{
TemplateFunction::new(self, move |value| Ok(function(value)))
}

/// Translates to a property that will unwrap an extracted `Option` value
/// of the specified `type_name`, mapping `None` to `Err`.
fn try_unwrap<O>(self, type_name: &str) -> impl TemplateProperty<Output = O>
where
Self: TemplateProperty<Output = Option<O>> + Sized,
{
self.and_then(move |opt| {
opt.ok_or_else(|| TemplatePropertyError(format!("No {type_name} available").into()))
})
}

/// Converts this property into `Template`.
fn into_template<'a>(self) -> Box<dyn Template + 'a>
where
Self: Sized + 'a,
Self::Output: Template,
{
Box::new(FormattablePropertyTemplate::new(self))
}
}

impl<P: TemplateProperty + ?Sized> TemplatePropertyExt for P {}
Expand Down Expand Up @@ -401,15 +422,6 @@ where
}
}

impl<'a, O> IntoTemplate<'a> for Box<dyn TemplateProperty<Output = O> + 'a>
where
O: Template + 'a,
{
fn into_template(self) -> Box<dyn Template + 'a> {
Box::new(FormattablePropertyTemplate::new(self))
}
}

/// Adapter to turn template back to string property.
pub struct PlainTextFormattedProperty<T> {
template: T,
Expand Down
2 changes: 1 addition & 1 deletion cli/tests/test_global_opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ fn test_color_ui_messages() {
);
insta::assert_snapshot!(stdout, @r###"
167f90e7600a50f85c4f909b53eaf546faa82879
<Error: No commit available> (elided revisions)
<Error: No Commit available> (elided revisions)
0000000000000000000000000000000000000000
"###);

Expand Down
2 changes: 1 addition & 1 deletion cli/tests/test_tag_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ fn test_tag_list() {
[conflicted_tag]
present: true
conflict: true
normal_target: <Error: No commit available>
normal_target: <Error: No Commit available>
removed_targets: commit1
added_targets: commit2 commit3
[test_tag]
Expand Down
10 changes: 10 additions & 0 deletions docs/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ The following methods are defined.
* `.username() -> String`
* `.timestamp() -> Timestamp`

### SizeHint type

This type cannot be printed. The following methods are defined.

* `.lower() -> Integer`: Lower bound.
* `.upper() -> Option<Integer>`: Upper bound if known.
* `.exact() -> Option<Integer>`: Exact value if upper bound is known and it
equals to the lower bound.
* `.zero() -> Boolean`: True if upper bound is known and is `0`.

### String type

A string can be implicitly converted to `Boolean`. The following methods are
Expand Down