templater: add indent(prefix, content) function

The argument order is different from Mercurial's indent() function. I think
indent(prefix, content) is more readable for lengthy content. However,
indent(content, prefix, ...) might be better if we want to add an optional
firstline_prefix argument.
This commit is contained in:
Yuya Nishihara 2023-03-02 17:16:22 +09:00
parent 558aa15e6e
commit 8f8a9c91bc
4 changed files with 98 additions and 1 deletions

View file

@ -55,6 +55,8 @@ The following operators are supported.
The following functions are defined.
* `indent(prefix: Template, content: Template) -> Template`: Indent
non-empty lines by the given `prefix`.
* `label(label: Template, content: Template) -> Template`: Apply label to
the content. The `label` is evaluated as a space-separated string.
* `if(condition: Boolean, then: Template[, else: Template]) -> Template`:

View file

@ -25,7 +25,7 @@ use pest_derive::Parser;
use thiserror::Error;
use crate::templater::{
ConditionalTemplate, IntoTemplate, LabelTemplate, ListTemplate, Literal,
ConditionalTemplate, IndentTemplate, IntoTemplate, LabelTemplate, ListTemplate, Literal,
PlainTextFormattedProperty, SeparateTemplate, Template, TemplateFunction, TemplateProperty,
TemplatePropertyFn, TimestampRange,
};
@ -1045,6 +1045,13 @@ fn build_global_function<'a, L: TemplateLanguage<'a>>(
function: &FunctionCallNode,
) -> TemplateParseResult<Expression<'a, L::Context, L::Property>> {
let expression = match function.name {
"indent" => {
let [prefix_node, content_node] = expect_exact_arguments(function)?;
let prefix = build_expression(language, prefix_node)?.into_template();
let content = build_expression(language, content_node)?.into_template();
let template = Box::new(IndentTemplate::new(prefix, content));
Expression::Template(template)
}
"label" => {
let [label_node, content_node] = expect_exact_arguments(function)?;
let label_property = build_expression(language, label_node)?.into_plain_text();

View file

@ -97,6 +97,46 @@ impl Template<()> for i64 {
}
}
/// Indents each line by the given prefix.
pub struct IndentTemplate<S, T> {
prefix: S,
content: T,
}
impl<S, T> IndentTemplate<S, T> {
pub fn new<C>(prefix: S, content: T) -> Self
where
S: Template<C>,
T: Template<C>,
{
IndentTemplate { prefix, content }
}
}
impl<C, S, T> Template<C> for IndentTemplate<S, T>
where
S: Template<C>,
T: Template<C>,
{
fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> {
let mut recorder = FormatRecorder::new();
self.content.format(context, &mut recorder)?;
let mut new_line = true;
recorder.replay_with(formatter, |formatter, data| {
for line in data.split_inclusive(|&c| c == b'\n') {
if new_line && line != b"\n" {
// Prefix inherits the current labels. This is implementation detail
// and may be fixed later.
self.prefix.format(context, formatter)?;
}
formatter.write_all(line)?;
new_line = line.ends_with(b"\n");
}
Ok(())
})
}
}
pub struct LabelTemplate<T, L> {
content: T,
labels: L,

View file

@ -316,6 +316,54 @@ fn test_templater_signature() {
insta::assert_snapshot!(render(r#"author.username()"#), @"x");
}
#[test]
fn test_templater_indent_function() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
let render = |template| get_colored_template_output(&test_env, &repo_path, "@-", template);
// Empty line shouldn't be indented. Not using insta here because we test
// whitespace existence.
assert_eq!(render(r#"indent("__", "")"#), "");
assert_eq!(render(r#"indent("__", "\n")"#), "\n");
assert_eq!(render(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
// "\n" at end of labeled text
insta::assert_snapshot!(
render(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
@r###"
__a
__b
"###);
// "\n" in labeled text
insta::assert_snapshot!(
render(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
@r###"
__ab
__c
"###);
// Labeled prefix + unlabeled content
insta::assert_snapshot!(
render(r#"indent(label("error", "XX"), "a\nb\n")"#),
@r###"
XXa
XXb
"###);
// Nested indent, silly but works
insta::assert_snapshot!(
render(r#"indent(label("hint", "A"),
label("warning", indent(label("hint", "B"),
label("error", "x\n") ++ "y")))"#),
@r###"
ABx
ABy
"###);
}
#[test]
fn test_templater_label_function() {
let test_env = TestEnvironment::default();