// Copyright 2023 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::io; use itertools::Itertools as _; use jj_lib::backend::ObjectId; use jj_lib::op_store::{OperationId, OperationMetadata}; use jj_lib::operation::Operation; use jj_lib::repo::ReadonlyRepo; use crate::formatter::Formatter; use crate::template_builder::{ self, BuildContext, CoreTemplatePropertyKind, IntoTemplateProperty, TemplateLanguage, }; use crate::template_parser::{ self, FunctionCallNode, TemplateAliasesMap, TemplateParseError, TemplateParseResult, }; use crate::templater::{ IntoTemplate, PlainTextFormattedProperty, Template, TemplateFunction, TemplateProperty, TemplatePropertyFn, TimestampRange, }; struct OperationTemplateLanguage<'b> { head_op_id: &'b OperationId, } impl TemplateLanguage<'static> for OperationTemplateLanguage<'_> { type Context = Operation; type Property = OperationTemplatePropertyKind; template_builder::impl_core_wrap_property_fns!('static, OperationTemplatePropertyKind::Core); fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult { build_operation_keyword(self, name, span) } fn build_method( &self, build_ctx: &BuildContext, property: Self::Property, function: &FunctionCallNode, ) -> TemplateParseResult { match property { OperationTemplatePropertyKind::Core(property) => { template_builder::build_core_method(self, build_ctx, property, function) } OperationTemplatePropertyKind::OperationId(property) => { build_operation_id_method(self, build_ctx, property, function) } } } } impl OperationTemplateLanguage<'_> { fn wrap_operation_id( &self, property: impl TemplateProperty + 'static, ) -> OperationTemplatePropertyKind { OperationTemplatePropertyKind::OperationId(Box::new(property)) } } enum OperationTemplatePropertyKind { Core(CoreTemplatePropertyKind<'static, Operation>), OperationId(Box>), } impl IntoTemplateProperty<'static, Operation> for OperationTemplatePropertyKind { fn try_into_boolean(self) -> Option>> { match self { OperationTemplatePropertyKind::Core(property) => property.try_into_boolean(), _ => None, } } fn try_into_integer(self) -> Option>> { match self { OperationTemplatePropertyKind::Core(property) => property.try_into_integer(), _ => None, } } fn try_into_plain_text(self) -> Option>> { match self { OperationTemplatePropertyKind::Core(property) => property.try_into_plain_text(), _ => { let template = self.try_into_template()?; Some(Box::new(PlainTextFormattedProperty::new(template))) } } } fn try_into_template(self) -> Option>> { match self { OperationTemplatePropertyKind::Core(property) => property.try_into_template(), OperationTemplatePropertyKind::OperationId(property) => Some(property.into_template()), } } } fn build_operation_keyword( language: &OperationTemplateLanguage, name: &str, span: pest::Span, ) -> TemplateParseResult { fn wrap_fn O>(f: F) -> TemplatePropertyFn { TemplatePropertyFn(f) } fn wrap_metadata_fn( f: impl Fn(&OperationMetadata) -> O + 'static, ) -> impl TemplateProperty { wrap_fn(move |op| f(&op.store_operation().metadata)) } let property = match name { "current_operation" => { let head_op_id = language.head_op_id.clone(); language.wrap_boolean(wrap_fn(move |op| op.id() == &head_op_id)) } "description" => { language.wrap_string(wrap_metadata_fn(|metadata| metadata.description.clone())) } "id" => language.wrap_operation_id(wrap_fn(|op| op.id().clone())), "tags" => language.wrap_string(wrap_metadata_fn(|metadata| { // TODO: introduce map type metadata .tags .iter() .map(|(key, value)| format!("{key}: {value}")) .join("\n") })), "time" => language.wrap_timestamp_range(wrap_metadata_fn(|metadata| TimestampRange { start: metadata.start_time.clone(), end: metadata.end_time.clone(), })), "user" => language.wrap_string(wrap_metadata_fn(|metadata| { // TODO: introduce dedicated type and provide accessors? format!("{}@{}", metadata.username, metadata.hostname) })), _ => return Err(TemplateParseError::no_such_keyword(name, span)), }; Ok(property) } impl Template<()> for OperationId { fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { formatter.write_str(&self.hex()) } } fn build_operation_id_method( language: &OperationTemplateLanguage, build_ctx: &BuildContext, self_property: impl TemplateProperty + 'static, function: &FunctionCallNode, ) -> TemplateParseResult { let property = match function.name { "short" => { let ([], [len_node]) = template_parser::expect_arguments(function)?; let len_property = len_node .map(|node| template_builder::expect_integer_expression(language, build_ctx, node)) .transpose()?; language.wrap_string(TemplateFunction::new( (self_property, len_property), |(id, len)| { let mut hex = id.hex(); hex.truncate(len.and_then(|l| l.try_into().ok()).unwrap_or(12)); hex }, )) } _ => return Err(TemplateParseError::no_such_method("OperationId", function)), }; Ok(property) } pub fn parse( repo: &ReadonlyRepo, template_text: &str, aliases_map: &TemplateAliasesMap, ) -> TemplateParseResult>> { let head_op_id = repo.op_id(); let language = OperationTemplateLanguage { head_op_id }; let node = template_parser::parse(template_text, aliases_map)?; template_builder::build(&language, &node) }