mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-19 19:08:08 +00:00
templater: add coalesce() function that selects first non-empty content
This can be used to flatten nested "if()"s. It's not exactly the same as "case" or "switch" expression, but works reasonably well in template. It's not uncommon to show placeholder text in place of an empty content, and a nullish value (e.g. empty string, list, option) is usually rendered as an empty text.
This commit is contained in:
parent
6c5a54a29f
commit
a2a9b7decb
6 changed files with 72 additions and 24 deletions
|
@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
* Commit templates now support `immutable` keyword.
|
||||
|
||||
* New template function `coalesce(content, ..)` is added.
|
||||
|
||||
* Timestamps are now shown in local timezone and without milliseconds and
|
||||
timezone offset by default.
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ concat(
|
|||
"Author: " ++ format_detailed_signature(author) ++ "\n",
|
||||
"Committer: " ++ format_detailed_signature(committer) ++ "\n",
|
||||
"\n",
|
||||
indent(" ", if(description, description, description_placeholder ++ "\n")),
|
||||
indent(" ", coalesce(description, description_placeholder ++ "\n")),
|
||||
"\n",
|
||||
)
|
||||
'''
|
||||
|
|
|
@ -22,10 +22,10 @@ use crate::template_parser::{
|
|||
TemplateParseError, TemplateParseErrorKind, TemplateParseResult, UnaryOp,
|
||||
};
|
||||
use crate::templater::{
|
||||
ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate, ListPropertyTemplate,
|
||||
ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, ReformatTemplate,
|
||||
SeparateTemplate, Template, TemplateProperty, TemplatePropertyError, TemplatePropertyExt as _,
|
||||
TemplateRenderer, TimestampRange,
|
||||
CoalesceTemplate, ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate,
|
||||
ListPropertyTemplate, ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder,
|
||||
ReformatTemplate, SeparateTemplate, Template, TemplateProperty, TemplatePropertyError,
|
||||
TemplatePropertyExt as _, TemplateRenderer, TimestampRange,
|
||||
};
|
||||
use crate::{text_util, time_util};
|
||||
|
||||
|
@ -921,6 +921,14 @@ fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFun
|
|||
let template = ConditionalTemplate::new(condition, true_template, false_template);
|
||||
Ok(L::wrap_template(Box::new(template)))
|
||||
});
|
||||
map.insert("coalesce", |language, build_ctx, function| {
|
||||
let contents = function
|
||||
.args
|
||||
.iter()
|
||||
.map(|node| expect_template_expression(language, build_ctx, node))
|
||||
.try_collect()?;
|
||||
Ok(L::wrap_template(Box::new(CoalesceTemplate(contents))))
|
||||
});
|
||||
map.insert("concat", |language, build_ctx, function| {
|
||||
let contents = function
|
||||
.args
|
||||
|
@ -1993,6 +2001,30 @@ mod tests {
|
|||
@"[38;5;1mtext[39m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coalesce_function() {
|
||||
let mut env = TestTemplateEnv::new();
|
||||
env.add_keyword("bad_string", || L::wrap_string(new_error_property("Bad")));
|
||||
env.add_keyword("empty_string", || L::wrap_string(Literal("".to_owned())));
|
||||
env.add_keyword("non_empty_string", || {
|
||||
L::wrap_string(Literal("a".to_owned()))
|
||||
});
|
||||
|
||||
insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
|
||||
insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
|
||||
insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
|
||||
insta::assert_snapshot!(
|
||||
env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
|
||||
|
||||
// "false" is not empty
|
||||
insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
|
||||
|
||||
// Error is not empty
|
||||
insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
|
||||
// but can be short-circuited
|
||||
insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concat_function() {
|
||||
let mut env = TestTemplateEnv::new();
|
||||
|
|
|
@ -184,6 +184,22 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Renders contents in order, and returns the first non-empty output.
|
||||
pub struct CoalesceTemplate<T>(pub Vec<T>);
|
||||
|
||||
impl<T: Template> Template for CoalesceTemplate<T> {
|
||||
fn format(&self, formatter: &mut dyn Formatter) -> io::Result<()> {
|
||||
let Some((last, contents)) = self.0.split_last() else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(recorder) = contents.iter().find_map(record_non_empty) {
|
||||
recorder?.replay(formatter)
|
||||
} else {
|
||||
last.format(formatter) // no need to capture the last content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConcatTemplate<T>(pub Vec<T>);
|
||||
|
||||
impl<T: Template> Template for ConcatTemplate<T> {
|
||||
|
@ -248,18 +264,7 @@ where
|
|||
T: Template,
|
||||
{
|
||||
fn format(&self, formatter: &mut dyn Formatter) -> io::Result<()> {
|
||||
let mut content_recorders = self
|
||||
.contents
|
||||
.iter()
|
||||
.filter_map(|template| {
|
||||
let mut recorder = FormatRecorder::new();
|
||||
match template.format(&mut recorder) {
|
||||
Ok(()) if recorder.data().is_empty() => None, // omit empty content
|
||||
Ok(()) => Some(Ok(recorder)),
|
||||
Err(e) => Some(Err(e)),
|
||||
}
|
||||
})
|
||||
.fuse();
|
||||
let mut content_recorders = self.contents.iter().filter_map(record_non_empty).fuse();
|
||||
if let Some(recorder) = content_recorders.next() {
|
||||
recorder?.replay(formatter)?;
|
||||
}
|
||||
|
@ -697,3 +702,12 @@ fn format_error_inline(formatter: &mut dyn Formatter, err: &dyn error::Error) ->
|
|||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn record_non_empty(template: impl Template) -> Option<io::Result<FormatRecorder>> {
|
||||
let mut recorder = FormatRecorder::new();
|
||||
match template.format(&mut recorder) {
|
||||
Ok(()) if recorder.data().is_empty() => None, // omit empty content
|
||||
Ok(()) => Some(Ok(recorder)),
|
||||
Err(e) => Some(Err(e)),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -241,13 +241,11 @@ For example:
|
|||
```toml
|
||||
[templates]
|
||||
log_node = '''
|
||||
if(self,
|
||||
if(current_working_copy, "@",
|
||||
if(root, "┴",
|
||||
if(immutable, "●", "○")
|
||||
)
|
||||
),
|
||||
"🮀",
|
||||
coalesce(
|
||||
if(!self, "🮀"),
|
||||
if(current_working_copy, "@"),
|
||||
if(root, "┴"),
|
||||
if(immutable, "●", "○"),
|
||||
)
|
||||
'''
|
||||
op_log_node = 'if(current_operation, "@", "○")'
|
||||
|
|
|
@ -47,6 +47,8 @@ The following functions are defined.
|
|||
the content. The `label` is evaluated as a space-separated string.
|
||||
* `if(condition: Boolean, then: Template[, else: Template]) -> Template`:
|
||||
Conditionally evaluate `then`/`else` template content.
|
||||
* `coalesce(content: Template...) -> Template`: Returns the first **non-empty**
|
||||
content.
|
||||
* `concat(content: Template...) -> Template`:
|
||||
Same as `content_1 ++ ... ++ content_n`.
|
||||
* `separate(separator: Template, content: Template...) -> Template`:
|
||||
|
|
Loading…
Reference in a new issue