From d0168199f0bc89dcde01d0b31da45d1c17b28fed Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Sun, 3 Mar 2024 18:50:28 +0900 Subject: [PATCH] templater: add general-purpose template engine for value types This serves the role of the formatter in Mercurial, but the provided features are rather restricted compared to mercurial.formatter. That's because both implementation language "Rust" and jj's template language are statically typed. The current implementation works well for simple commands like "config list -T", but it might be not okay for "branch list -T". If we implement branch templating by using the generic mechanism, the commit summary part would have to be evaluated as a separate template: -T 'branch_name ++ target_commit_summary' (target_commit_summary: Template) instead of -T 'branch_name ++ commit_summary(target_commit)' (target_commit: Commit) where the branch template language is a superset of the commit template language. --- cli/src/generic_templater.rs | 163 +++++++++++++++++++++++++++++++++++ cli/src/lib.rs | 1 + 2 files changed, 164 insertions(+) create mode 100644 cli/src/generic_templater.rs diff --git a/cli/src/generic_templater.rs b/cli/src/generic_templater.rs new file mode 100644 index 000000000..71a72f62a --- /dev/null +++ b/cli/src/generic_templater.rs @@ -0,0 +1,163 @@ +// Copyright 2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use crate::template_builder::{ + self, BuildContext, CoreTemplateBuildFnTable, CoreTemplatePropertyKind, IntoTemplateProperty, + TemplateLanguage, +}; +use crate::template_parser::{self, FunctionCallNode, TemplateParseResult}; +use crate::templater::{Template, TemplateProperty}; + +/// General-purpose template language for basic value types. +/// +/// This template language only supports the core template property types (plus +/// the context type `C`.) The context type `C` is usually a tuple or struct of +/// value types. Keyword functions need to be registered to extract properties +/// from the context object. +pub struct GenericTemplateLanguage<'a, C> { + build_fn_table: GenericTemplateBuildFnTable<'a, C>, +} + +impl<'a, C> GenericTemplateLanguage<'a, C> { + /// Sets up environment with no keywords. + /// + /// New keyword functions can be registered by `add_keyword()`. + // It's not "Default" in a way that the core methods table is NOT empty. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self::with_keywords(HashMap::new()) + } + + /// Sets up environment with the given `keywords` table. + pub fn with_keywords(keywords: GenericTemplateBuildKeywordFnMap<'a, C>) -> Self { + GenericTemplateLanguage { + build_fn_table: GenericTemplateBuildFnTable { + core: CoreTemplateBuildFnTable::builtin(), + keywords, + }, + } + } + + /// Registers new function that translates keyword to property. + /// + /// A keyword function returns `Self::Property`, which is basically a + /// closure tagged by its return type. The inner closure is usually wrapped + /// by `TemplatePropertyFn`. + /// + /// ```ignore + /// language.add_keyword("name", |language| { + /// let property = TemplatePropertyFn(|v: &C| Ok(v.to_string())); + /// Ok(language.wrap_string(property)) + /// }); + /// ``` + pub fn add_keyword(&mut self, name: &'static str, build: F) + where + F: Fn(&Self) -> TemplateParseResult> + 'a, + { + self.build_fn_table.keywords.insert(name, Box::new(build)); + } +} + +impl<'a, C: 'a> TemplateLanguage<'a> for GenericTemplateLanguage<'a, C> { + type Context = C; + type Property = GenericTemplatePropertyKind<'a, C>; + + template_builder::impl_core_wrap_property_fns!('a, GenericTemplatePropertyKind::Core); + + fn build_self(&self) -> Self::Property { + // No need to clone the context object because there are no other + // objects of "Self" type, and the context is available everywhere. + GenericTemplatePropertyKind::Self_ + } + + fn build_method( + &self, + build_ctx: &BuildContext, + property: Self::Property, + function: &FunctionCallNode, + ) -> TemplateParseResult { + match property { + GenericTemplatePropertyKind::Core(property) => { + let table = &self.build_fn_table.core; + table.build_method(self, build_ctx, property, function) + } + GenericTemplatePropertyKind::Self_ => { + let table = &self.build_fn_table.keywords; + let build = template_parser::lookup_method("Self", table, function)?; + // For simplicity, only 0-ary method is supported. + template_parser::expect_no_arguments(function)?; + build(self) + } + } + } +} + +pub enum GenericTemplatePropertyKind<'a, C> { + Core(CoreTemplatePropertyKind<'a, C>), + Self_, +} + +impl<'a, C: 'a> IntoTemplateProperty<'a, C> for GenericTemplatePropertyKind<'a, C> { + fn try_into_boolean(self) -> Option + 'a>> { + match self { + GenericTemplatePropertyKind::Core(property) => property.try_into_boolean(), + GenericTemplatePropertyKind::Self_ => None, + } + } + + fn try_into_integer(self) -> Option + 'a>> { + match self { + GenericTemplatePropertyKind::Core(property) => property.try_into_integer(), + GenericTemplatePropertyKind::Self_ => None, + } + } + + fn try_into_plain_text(self) -> Option + 'a>> { + match self { + GenericTemplatePropertyKind::Core(property) => property.try_into_plain_text(), + GenericTemplatePropertyKind::Self_ => None, + } + } + + fn try_into_template(self) -> Option + 'a>> { + match self { + GenericTemplatePropertyKind::Core(property) => property.try_into_template(), + GenericTemplatePropertyKind::Self_ => None, + } + } +} + +/// Function that translates keyword (or 0-ary method call node of the context +/// type `C`.) +/// +/// Because the `GenericTemplateLanguage` doesn't provide a way to pass around +/// global resources, the keyword function is allowed to capture resources. +pub type GenericTemplateBuildKeywordFn<'a, C> = Box< + dyn Fn( + &GenericTemplateLanguage<'a, C>, + ) -> TemplateParseResult> + + 'a, +>; + +/// Table of functions that translate keyword node. +pub type GenericTemplateBuildKeywordFnMap<'a, C> = + HashMap<&'static str, GenericTemplateBuildKeywordFn<'a, C>>; + +/// Symbol table of methods available in the general-purpose template. +struct GenericTemplateBuildFnTable<'a, C: 'a> { + core: CoreTemplateBuildFnTable<'a, GenericTemplateLanguage<'a, C>>, + keywords: GenericTemplateBuildKeywordFnMap<'a, C>, +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 4fc40c934..00cbdfaab 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -23,6 +23,7 @@ pub mod config; pub mod description_util; pub mod diff_util; pub mod formatter; +pub mod generic_templater; pub mod git_util; pub mod graphlog; pub mod merge_tools;