forked from mirrors/jj
templater: add separate(sep, contents...) function
This is a copy of Mercurial's separate() function.
This commit is contained in:
parent
84ee0edc51
commit
13b5661094
3 changed files with 129 additions and 3 deletions
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -222,6 +222,57 @@ fn test_templater_label_function() {
|
|||
render(r#"label(if(empty, "error", "warning"), "text")"#), @"[38;5;1mtext[39m");
|
||||
}
|
||||
|
||||
#[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")"#),
|
||||
@"[38;5;3ma[39m 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)"#), @" <> [38;5;2mtrue[39m");
|
||||
|
||||
// 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,
|
||||
|
|
Loading…
Reference in a new issue