ok/jj
1
0
Fork 0
forked from mirrors/jj

templater: add separate(sep, contents...) function

This is a copy of Mercurial's separate() function.
This commit is contained in:
Yuya Nishihara 2023-02-03 14:36:01 +09:00
parent 84ee0edc51
commit 13b5661094
3 changed files with 129 additions and 3 deletions

View file

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::ops::RangeInclusive;
use std::ops::{RangeFrom, RangeInclusive};
use std::{error, fmt};
use itertools::Itertools as _;
@ -29,8 +29,8 @@ use thiserror::Error;
use crate::templater::{
BranchProperty, CommitOrChangeId, ConditionalTemplate, FormattablePropertyTemplate,
GitHeadProperty, GitRefsProperty, IdWithHighlightedPrefix, LabelTemplate, ListTemplate,
Literal, PlainTextFormattedProperty, TagProperty, Template, TemplateFunction, TemplateProperty,
TemplatePropertyFn, WorkingCopiesProperty,
Literal, PlainTextFormattedProperty, SeparateTemplate, TagProperty, Template, TemplateFunction,
TemplateProperty, TemplatePropertyFn, WorkingCopiesProperty,
};
use crate::{cli_util, time_util};
@ -59,6 +59,8 @@ pub enum TemplateParseErrorKind {
InvalidArgumentCountExact(usize),
#[error("Expected {} to {} arguments", .0.start(), .0.end())]
InvalidArgumentCountRange(RangeInclusive<usize>),
#[error("Expected at least {} arguments", .0.start)]
InvalidArgumentCountRangeFrom(RangeFrom<usize>),
#[error(r#"Expected argument of type "{0}""#)]
InvalidArgumentType(String),
}
@ -112,6 +114,13 @@ impl TemplateParseError {
)
}
fn invalid_argument_count_range_from(count: RangeFrom<usize>, span: pest::Span<'_>) -> Self {
TemplateParseError::with_span(
TemplateParseErrorKind::InvalidArgumentCountRangeFrom(count),
span,
)
}
fn invalid_argument_type(expected_type_name: impl Into<String>, span: pest::Span<'_>) -> Self {
TemplateParseError::with_span(
TemplateParseErrorKind::InvalidArgumentType(expected_type_name.into()),
@ -533,6 +542,21 @@ fn parse_commit_term<'a>(
));
Expression::Template(template)
}
"separate" => {
let arg_count_error =
|| TemplateParseError::invalid_argument_count_range_from(1.., args_span);
let separator_pair = args.next().ok_or_else(arg_count_error)?;
let separator = parse_commit_template_rule(repo, workspace_id, separator_pair)?
.into_template();
let contents = args
.map(|pair| {
parse_commit_template_rule(repo, workspace_id, pair)
.map(|x| x.into_template())
})
.try_collect()?;
let template = Box::new(SeparateTemplate::new(separator, contents));
Expression::Template(template)
}
_ => return Err(TemplateParseError::no_such_function(&name)),
};
Ok(expression)

View file

@ -135,6 +135,57 @@ impl<C, T: Template<C>> Template<C> for ListTemplate<T> {
}
}
/// Like `ListTemplate`, but inserts a separator between non-empty templates.
pub struct SeparateTemplate<S, T> {
separator: S,
contents: Vec<T>,
}
impl<S, T> SeparateTemplate<S, T> {
pub fn new<C>(separator: S, contents: Vec<T>) -> Self
where
S: Template<C>,
T: Template<C>,
{
SeparateTemplate {
separator,
contents,
}
}
}
impl<C, S, T> Template<C> for SeparateTemplate<S, T>
where
S: Template<C>,
T: Template<C>,
{
fn format(&self, context: &C, formatter: &mut dyn Formatter) -> io::Result<()> {
// TemplateProperty may be evaluated twice, by has_content() and format().
// If that's too expensive, we can instead create a buffered formatter
// inheriting the state, and write to it to test the emptiness. In this case,
// the formatter should guarantee push/pop_label() is noop without content.
let mut content_templates = self
.contents
.iter()
.filter(|template| template.has_content(context))
.fuse();
if let Some(template) = content_templates.next() {
template.format(context, formatter)?;
}
for template in content_templates {
self.separator.format(context, formatter)?;
template.format(context, formatter)?;
}
Ok(())
}
fn has_content(&self, context: &C) -> bool {
self.contents
.iter()
.any(|template| template.has_content(context))
}
}
pub trait TemplateProperty<C> {
type Output;

View file

@ -222,6 +222,57 @@ fn test_templater_label_function() {
render(r#"label(if(empty, "error", "warning"), "text")"#), @"text");
}
#[test]
fn test_templater_separate_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);
insta::assert_snapshot!(render(r#"separate(" ")"#), @"");
insta::assert_snapshot!(render(r#"separate(" ", "")"#), @"");
insta::assert_snapshot!(render(r#"separate(" ", "a")"#), @"a");
insta::assert_snapshot!(render(r#"separate(" ", "a", "b")"#), @"a b");
insta::assert_snapshot!(render(r#"separate(" ", "a", "", "b")"#), @"a b");
insta::assert_snapshot!(render(r#"separate(" ", "a", "b", "")"#), @"a b");
insta::assert_snapshot!(render(r#"separate(" ", "", "a", "b")"#), @"a b");
// Labeled
insta::assert_snapshot!(
render(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
@"a b");
// List template
insta::assert_snapshot!(render(r#"separate(" ", "a", ("" ""))"#), @"a");
insta::assert_snapshot!(render(r#"separate(" ", "a", ("" "b"))"#), @"a b");
// Nested separate
insta::assert_snapshot!(
render(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
insta::assert_snapshot!(
render(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
insta::assert_snapshot!(
render(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
// Conditional template
insta::assert_snapshot!(
render(r#"separate(" ", "a", if("t", ""))"#), @"a");
insta::assert_snapshot!(
render(r#"separate(" ", "a", if("t", "", "f"))"#), @"a");
insta::assert_snapshot!(
render(r#"separate(" ", "a", if("", "t", ""))"#), @"a");
insta::assert_snapshot!(
render(r#"separate(" ", "a", if("t", "t", "f"))"#), @"a t");
// Separate keywords
insta::assert_snapshot!(
render(r#"separate(" ", author, description, empty)"#), @" <> true");
// Keyword as separator
insta::assert_snapshot!(
render(r#"separate(author, "X", "Y", "Z")"#), @"X <>Y <>Z");
}
fn get_template_output(
test_env: &TestEnvironment,
repo_path: &Path,