diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index a341df4214..43a6d9d16a 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -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}; @@ -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) => { @@ -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) => { diff --git a/cli/src/operation_templater.rs b/cli/src/operation_templater.rs index 4c884fd4c0..0a66dabce4 100644 --- a/cli/src/operation_templater.rs +++ b/cli/src/operation_templater.rs @@ -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, }; diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index b70b9d6207..31ae846756 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -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}; @@ -39,7 +39,11 @@ pub trait TemplateLanguage<'a> { ) -> Self::Property; fn wrap_boolean(property: impl TemplateProperty + 'a) -> Self::Property; fn wrap_integer(property: impl TemplateProperty + 'a) -> Self::Property; + fn wrap_integer_opt( + property: impl TemplateProperty> + 'a, + ) -> Self::Property; fn wrap_signature(property: impl TemplateProperty + 'a) -> Self::Property; + fn wrap_size_hint(property: impl TemplateProperty + 'a) -> Self::Property; fn wrap_timestamp(property: impl TemplateProperty + 'a) -> Self::Property; fn wrap_timestamp_range( property: impl TemplateProperty + 'a, @@ -81,7 +85,9 @@ macro_rules! impl_core_wrap_property_fns { wrap_string_list(Vec) => StringList, wrap_boolean(bool) => Boolean, wrap_integer(i64) => Integer, + wrap_integer_opt(Option) => 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, } @@ -133,7 +139,9 @@ pub enum CoreTemplatePropertyKind<'a> { StringList(Box> + 'a>), Boolean(Box + 'a>), Integer(Box + 'a>), + IntegerOpt(Box> + 'a>), Signature(Box + 'a>), + SizeHint(Box + 'a>), Timestamp(Box + 'a>), TimestampRange(Box + 'a>), @@ -158,7 +166,9 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> { CoreTemplatePropertyKind::StringList(_) => "List", CoreTemplatePropertyKind::Boolean(_) => "Boolean", CoreTemplatePropertyKind::Integer(_) => "Integer", + CoreTemplatePropertyKind::IntegerOpt(_) => "Option", CoreTemplatePropertyKind::Signature(_) => "Signature", + CoreTemplatePropertyKind::SizeHint(_) => "SizeHint", CoreTemplatePropertyKind::Timestamp(_) => "Timestamp", CoreTemplatePropertyKind::TimestampRange(_) => "TimestampRange", CoreTemplatePropertyKind::Template(_) => "Template", @@ -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 @@ -190,6 +204,9 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> { fn try_into_integer(self) -> Option + 'a>> { match self { CoreTemplatePropertyKind::Integer(property) => Some(property), + CoreTemplatePropertyKind::IntegerOpt(property) => { + Some(Box::new(property.try_unwrap("Integer"))) + } _ => None, } } @@ -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), @@ -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>, } @@ -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(), } @@ -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(), } @@ -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; @@ -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); } @@ -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)?; @@ -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::::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 @@ -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 | @@ -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"#), @""); + insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1"); + // No panic on integer overflow. insta::assert_snapshot!( env.render_ok(r#"-i64_min"#), @@ -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(); diff --git a/cli/src/templater.rs b/cli/src/templater.rs index 284fcee64f..b0b0f2cad2 100644 --- a/cli/src/templater.rs +++ b/cli/src/templater.rs @@ -39,10 +39,6 @@ pub trait ListTemplate: Template { Self: 'a; } -pub trait IntoTemplate<'a> { - fn into_template(self) -> Box; -} - impl Template for &T { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { ::format(self, formatter) @@ -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); + impl Template for String { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter, "{self}") @@ -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(self, type_name: &str) -> impl TemplateProperty + where + Self: TemplateProperty> + 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 + where + Self: Sized + 'a, + Self::Output: Template, + { + Box::new(FormattablePropertyTemplate::new(self)) + } } impl TemplatePropertyExt for P {} @@ -401,15 +422,6 @@ where } } -impl<'a, O> IntoTemplate<'a> for Box + 'a> -where - O: Template + 'a, -{ - fn into_template(self) -> Box { - Box::new(FormattablePropertyTemplate::new(self)) - } -} - /// Adapter to turn template back to string property. pub struct PlainTextFormattedProperty { template: T, diff --git a/cli/tests/test_global_opts.rs b/cli/tests/test_global_opts.rs index a0c145de54..fe4567896f 100644 --- a/cli/tests/test_global_opts.rs +++ b/cli/tests/test_global_opts.rs @@ -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 "###); diff --git a/cli/tests/test_tag_command.rs b/cli/tests/test_tag_command.rs index 491c41c6ec..b74980ab73 100644 --- a/cli/tests/test_tag_command.rs +++ b/cli/tests/test_tag_command.rs @@ -100,7 +100,7 @@ fn test_tag_list() { [conflicted_tag] present: true conflict: true - normal_target: + normal_target: removed_targets: commit1 added_targets: commit2 commit3 [test_tag] diff --git a/docs/templates.md b/docs/templates.md index fe446db88e..2869f553ae 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -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`: Upper bound if known. +* `.exact() -> Option`: 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