diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index 912234bf08..6940c54ed2 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -488,6 +488,11 @@ fn builtin_string_methods<'a, L: TemplateLanguage<'a>>() -> TemplateBuildMethodF // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = TemplateBuildMethodFnMap::::new(); + map.insert("len", |language, _build_ctx, self_property, function| { + template_parser::expect_no_arguments(function)?; + let out_property = TemplateFunction::new(self_property, |s| Ok(s.len().try_into()?)); + Ok(language.wrap_integer(out_property)) + }); map.insert( "contains", |language, build_ctx, self_property, function| { @@ -762,6 +767,12 @@ where O: Template<()> + Clone + 'a, { let property = match function.name { + "len" => { + template_parser::expect_no_arguments(function)?; + let out_property = + TemplateFunction::new(self_property, |items| Ok(items.len().try_into()?)); + language.wrap_integer(out_property) + } "join" => { let [separator_node] = template_parser::expect_exact_arguments(function)?; let separator = expect_template_expression(language, build_ctx, separator_node)?; @@ -789,6 +800,12 @@ where O: Clone + 'a, { let property = match function.name { + "len" => { + template_parser::expect_no_arguments(function)?; + let out_property = + TemplateFunction::new(self_property, |items| Ok(items.len().try_into()?)); + language.wrap_integer(out_property) + } // No "join" "map" => build_map_operation(language, build_ctx, self_property, function, wrap_item)?, _ => return Err(TemplateParseError::no_such_method("List", function)), @@ -1536,6 +1553,9 @@ mod tests { language.wrap_string(Literal("sep".to_owned())) }); + insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0"); + insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3"); + insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @""); insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c"); // Null separator @@ -1633,6 +1653,10 @@ mod tests { language.wrap_string(Literal("description 1".to_owned())) }); + insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0"); + insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3"); + insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4"); + insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true"); insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false"); insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true"); diff --git a/cli/tests/test_commit_template.rs b/cli/tests/test_commit_template.rs index 13eda0c4fa..6f424cf3bc 100644 --- a/cli/tests/test_commit_template.rs +++ b/cli/tests/test_commit_template.rs @@ -26,17 +26,18 @@ fn test_log_parents() { test_env.jj_cmd_ok(&repo_path, &["new", "@-"]); test_env.jj_cmd_ok(&repo_path, &["new", "@", "@-"]); - let template = r#"commit_id ++ "\nP: " ++ parents.map(|c| c.commit_id()) ++ "\n""#; + let template = + r#"commit_id ++ "\nP: " ++ parents.len() ++ " " ++ parents.map(|c| c.commit_id()) ++ "\n""#; let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", template]); insta::assert_snapshot!(stdout, @r###" @ c067170d4ca1bc6162b64f7550617ec809647f84 - ├─╮ P: 4db490c88528133d579540b6900b8098f0c17701 230dd059e1b059aefc0da06a2e5a7dbf22362f22 + ├─╮ P: 2 4db490c88528133d579540b6900b8098f0c17701 230dd059e1b059aefc0da06a2e5a7dbf22362f22 ◉ │ 4db490c88528133d579540b6900b8098f0c17701 - ├─╯ P: 230dd059e1b059aefc0da06a2e5a7dbf22362f22 + ├─╯ P: 1 230dd059e1b059aefc0da06a2e5a7dbf22362f22 ◉ 230dd059e1b059aefc0da06a2e5a7dbf22362f22 - │ P: 0000000000000000000000000000000000000000 + │ P: 1 0000000000000000000000000000000000000000 ◉ 0000000000000000000000000000000000000000 - P: + P: 0 "###); let template = r#"parents.map(|c| c.commit_id().shortest(4))"#; diff --git a/docs/templates.md b/docs/templates.md index 595b2a142f..c9424f6033 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -105,6 +105,7 @@ No methods are defined. A list can be implicitly converted to `Boolean`. The following methods are defined. +* `.len() -> Integer`: Number of elements in the list. * `.join(separator: Template) -> Template`: Concatenate elements with the given `separator`. * `.map(|item| expression) -> ListTemplate`: Apply template `expression` @@ -164,6 +165,7 @@ The following methods are defined. A string can be implicitly converted to `Boolean`. The following methods are defined. +* `.len() -> Integer`: Length in UTF-8 bytes. * `.contains(needle: Template) -> Boolean` * `.first_line() -> String` * `.lines() -> List`: Split into lines excluding newline characters.