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;