jj_cli/
operation_templater.rs

1// Copyright 2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Template environment for `jj op log`.
16
17use std::any::Any;
18use std::cmp::Ordering;
19use std::collections::HashMap;
20use std::io;
21
22use itertools::Itertools as _;
23use jj_lib::extensions_map::ExtensionsMap;
24use jj_lib::object_id::ObjectId as _;
25use jj_lib::op_store::OperationId;
26use jj_lib::operation::Operation;
27use jj_lib::repo::RepoLoader;
28use jj_lib::settings::UserSettings;
29
30use crate::template_builder;
31use crate::template_builder::BuildContext;
32use crate::template_builder::CoreTemplateBuildFnTable;
33use crate::template_builder::CoreTemplatePropertyKind;
34use crate::template_builder::CoreTemplatePropertyVar;
35use crate::template_builder::TemplateBuildMethodFnMap;
36use crate::template_builder::TemplateLanguage;
37use crate::template_builder::merge_fn_map;
38use crate::template_parser;
39use crate::template_parser::FunctionCallNode;
40use crate::template_parser::TemplateDiagnostics;
41use crate::template_parser::TemplateParseResult;
42use crate::templater::BoxedSerializeProperty;
43use crate::templater::BoxedTemplateProperty;
44use crate::templater::ListTemplate;
45use crate::templater::PlainTextFormattedProperty;
46use crate::templater::Template;
47use crate::templater::TemplateFormatter;
48use crate::templater::TemplatePropertyExt as _;
49use crate::templater::WrapTemplateProperty;
50
51pub trait OperationTemplateLanguageExtension {
52    fn build_fn_table(&self) -> OperationTemplateLanguageBuildFnTable;
53
54    fn build_cache_extensions(&self, extensions: &mut ExtensionsMap);
55}
56
57/// Global resources needed by [`OperationTemplatePropertyKind`] methods.
58pub trait OperationTemplateEnvironment {
59    fn repo_loader(&self) -> &RepoLoader;
60    fn current_op_id(&self) -> Option<&OperationId>;
61}
62
63/// Template environment for `jj op log`.
64pub struct OperationTemplateLanguage {
65    repo_loader: RepoLoader,
66    current_op_id: Option<OperationId>,
67    build_fn_table: OperationTemplateLanguageBuildFnTable,
68    cache_extensions: ExtensionsMap,
69}
70
71impl OperationTemplateLanguage {
72    /// Sets up environment where operation template will be transformed to
73    /// evaluation tree.
74    pub fn new(
75        repo_loader: &RepoLoader,
76        current_op_id: Option<&OperationId>,
77        extensions: &[impl AsRef<dyn OperationTemplateLanguageExtension>],
78    ) -> Self {
79        let mut build_fn_table = OperationTemplateLanguageBuildFnTable::builtin();
80        let mut cache_extensions = ExtensionsMap::empty();
81
82        for extension in extensions {
83            build_fn_table.merge(extension.as_ref().build_fn_table());
84            extension
85                .as_ref()
86                .build_cache_extensions(&mut cache_extensions);
87        }
88
89        Self {
90            // Clone these to keep lifetime simple
91            repo_loader: repo_loader.clone(),
92            current_op_id: current_op_id.cloned(),
93            build_fn_table,
94            cache_extensions,
95        }
96    }
97}
98
99impl TemplateLanguage<'static> for OperationTemplateLanguage {
100    type Property = OperationTemplateLanguagePropertyKind;
101
102    fn settings(&self) -> &UserSettings {
103        self.repo_loader.settings()
104    }
105
106    fn build_function(
107        &self,
108        diagnostics: &mut TemplateDiagnostics,
109        build_ctx: &BuildContext<Self::Property>,
110        function: &FunctionCallNode,
111    ) -> TemplateParseResult<Self::Property> {
112        let table = &self.build_fn_table.core;
113        table.build_function(self, diagnostics, build_ctx, function)
114    }
115
116    fn build_method(
117        &self,
118        diagnostics: &mut TemplateDiagnostics,
119        build_ctx: &BuildContext<Self::Property>,
120        property: Self::Property,
121        function: &FunctionCallNode,
122    ) -> TemplateParseResult<Self::Property> {
123        match property {
124            OperationTemplateLanguagePropertyKind::Core(property) => {
125                let table = &self.build_fn_table.core;
126                table.build_method(self, diagnostics, build_ctx, property, function)
127            }
128            OperationTemplateLanguagePropertyKind::Operation(property) => {
129                let table = &self.build_fn_table.operation;
130                table.build_method(self, diagnostics, build_ctx, property, function)
131            }
132        }
133    }
134}
135
136impl OperationTemplateEnvironment for OperationTemplateLanguage {
137    fn repo_loader(&self) -> &RepoLoader {
138        &self.repo_loader
139    }
140
141    fn current_op_id(&self) -> Option<&OperationId> {
142        self.current_op_id.as_ref()
143    }
144}
145
146impl OperationTemplateLanguage {
147    pub fn cache_extension<T: Any>(&self) -> Option<&T> {
148        self.cache_extensions.get::<T>()
149    }
150}
151
152/// Wrapper for the operation template property types.
153pub trait OperationTemplatePropertyVar<'a>
154where
155    Self: WrapTemplateProperty<'a, Operation>,
156    Self: WrapTemplateProperty<'a, Option<Operation>>,
157    Self: WrapTemplateProperty<'a, Vec<Operation>>,
158    Self: WrapTemplateProperty<'a, OperationId>,
159{
160}
161
162/// Tagged union of the operation template property types.
163pub enum OperationTemplatePropertyKind<'a> {
164    Operation(BoxedTemplateProperty<'a, Operation>),
165    OperationOpt(BoxedTemplateProperty<'a, Option<Operation>>),
166    OperationList(BoxedTemplateProperty<'a, Vec<Operation>>),
167    OperationId(BoxedTemplateProperty<'a, OperationId>),
168}
169
170/// Implements `WrapTemplateProperty<type>` for operation property types.
171///
172/// Use `impl_operation_property_wrappers!(<'a> Kind<'a> => Operation);` to
173/// implement forwarding conversion.
174macro_rules! impl_operation_property_wrappers {
175    ($($head:tt)+) => {
176        $crate::template_builder::impl_property_wrappers!($($head)+ {
177            Operation(jj_lib::operation::Operation),
178            OperationOpt(Option<jj_lib::operation::Operation>),
179            OperationList(Vec<jj_lib::operation::Operation>),
180            OperationId(jj_lib::op_store::OperationId),
181        });
182    };
183}
184
185pub(crate) use impl_operation_property_wrappers;
186
187impl_operation_property_wrappers!(<'a> OperationTemplatePropertyKind<'a>);
188
189impl<'a> OperationTemplatePropertyKind<'a> {
190    pub fn type_name(&self) -> &'static str {
191        match self {
192            Self::Operation(_) => "Operation",
193            Self::OperationOpt(_) => "Option<Operation>",
194            Self::OperationList(_) => "List<Operation>",
195            Self::OperationId(_) => "OperationId",
196        }
197    }
198
199    pub fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>> {
200        match self {
201            Self::Operation(_) => None,
202            Self::OperationOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
203            Self::OperationList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
204            Self::OperationId(_) => None,
205        }
206    }
207
208    pub fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>> {
209        None
210    }
211
212    pub fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'a, String>> {
213        let template = self.try_into_template()?;
214        Some(PlainTextFormattedProperty::new(template).into_dyn())
215    }
216
217    pub fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
218        match self {
219            Self::Operation(property) => Some(property.into_serialize()),
220            Self::OperationOpt(property) => Some(property.into_serialize()),
221            Self::OperationList(property) => Some(property.into_serialize()),
222            Self::OperationId(property) => Some(property.into_serialize()),
223        }
224    }
225
226    pub fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
227        match self {
228            Self::Operation(_) => None,
229            Self::OperationOpt(_) => None,
230            Self::OperationList(_) => None,
231            Self::OperationId(property) => Some(property.into_template()),
232        }
233    }
234
235    pub fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
236        match (self, other) {
237            (Self::Operation(_), _) => None,
238            (Self::OperationOpt(_), _) => None,
239            (Self::OperationList(_), _) => None,
240            (Self::OperationId(_), _) => None,
241        }
242    }
243
244    pub fn try_into_eq_core(
245        self,
246        other: CoreTemplatePropertyKind<'a>,
247    ) -> Option<BoxedTemplateProperty<'a, bool>> {
248        match (self, other) {
249            (Self::Operation(_), _) => None,
250            (Self::OperationOpt(_), _) => None,
251            (Self::OperationList(_), _) => None,
252            (Self::OperationId(_), _) => None,
253        }
254    }
255
256    pub fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
257        match (self, other) {
258            (Self::Operation(_), _) => None,
259            (Self::OperationOpt(_), _) => None,
260            (Self::OperationList(_), _) => None,
261            (Self::OperationId(_), _) => None,
262        }
263    }
264
265    pub fn try_into_cmp_core(
266        self,
267        other: CoreTemplatePropertyKind<'a>,
268    ) -> Option<BoxedTemplateProperty<'a, Ordering>> {
269        match (self, other) {
270            (Self::Operation(_), _) => None,
271            (Self::OperationOpt(_), _) => None,
272            (Self::OperationList(_), _) => None,
273            (Self::OperationId(_), _) => None,
274        }
275    }
276}
277
278/// Tagged property types available in [`OperationTemplateLanguage`].
279pub enum OperationTemplateLanguagePropertyKind {
280    Core(CoreTemplatePropertyKind<'static>),
281    Operation(OperationTemplatePropertyKind<'static>),
282}
283
284template_builder::impl_core_property_wrappers!(OperationTemplateLanguagePropertyKind => Core);
285impl_operation_property_wrappers!(OperationTemplateLanguagePropertyKind => Operation);
286
287impl CoreTemplatePropertyVar<'static> for OperationTemplateLanguagePropertyKind {
288    fn wrap_template(template: Box<dyn Template>) -> Self {
289        Self::Core(CoreTemplatePropertyKind::wrap_template(template))
290    }
291
292    fn wrap_list_template(template: Box<dyn ListTemplate>) -> Self {
293        Self::Core(CoreTemplatePropertyKind::wrap_list_template(template))
294    }
295
296    fn type_name(&self) -> &'static str {
297        match self {
298            Self::Core(property) => property.type_name(),
299            Self::Operation(property) => property.type_name(),
300        }
301    }
302
303    fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'static, bool>> {
304        match self {
305            Self::Core(property) => property.try_into_boolean(),
306            Self::Operation(property) => property.try_into_boolean(),
307        }
308    }
309
310    fn try_into_integer(self) -> Option<BoxedTemplateProperty<'static, i64>> {
311        match self {
312            Self::Core(property) => property.try_into_integer(),
313            Self::Operation(property) => property.try_into_integer(),
314        }
315    }
316
317    fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'static, String>> {
318        match self {
319            Self::Core(property) => property.try_into_stringify(),
320            Self::Operation(property) => property.try_into_stringify(),
321        }
322    }
323
324    fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'static>> {
325        match self {
326            Self::Core(property) => property.try_into_serialize(),
327            Self::Operation(property) => property.try_into_serialize(),
328        }
329    }
330
331    fn try_into_template(self) -> Option<Box<dyn Template>> {
332        match self {
333            Self::Core(property) => property.try_into_template(),
334            Self::Operation(property) => property.try_into_template(),
335        }
336    }
337
338    fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'static, bool>> {
339        match (self, other) {
340            (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_eq(rhs),
341            (Self::Core(lhs), Self::Operation(rhs)) => rhs.try_into_eq_core(lhs),
342            (Self::Operation(lhs), Self::Core(rhs)) => lhs.try_into_eq_core(rhs),
343            (Self::Operation(lhs), Self::Operation(rhs)) => lhs.try_into_eq(rhs),
344        }
345    }
346
347    fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'static, Ordering>> {
348        match (self, other) {
349            (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_cmp(rhs),
350            (Self::Core(lhs), Self::Operation(rhs)) => rhs
351                .try_into_cmp_core(lhs)
352                .map(|property| property.map(Ordering::reverse).into_dyn()),
353            (Self::Operation(lhs), Self::Core(rhs)) => lhs.try_into_cmp_core(rhs),
354            (Self::Operation(lhs), Self::Operation(rhs)) => lhs.try_into_cmp(rhs),
355        }
356    }
357}
358
359impl OperationTemplatePropertyVar<'static> for OperationTemplateLanguagePropertyKind {}
360
361/// Symbol table for the operation template property types.
362pub struct OperationTemplateBuildFnTable<'a, L: ?Sized, P = <L as TemplateLanguage<'a>>::Property> {
363    pub operation_methods: TemplateBuildMethodFnMap<'a, L, Operation, P>,
364    pub operation_list_methods: TemplateBuildMethodFnMap<'a, L, Vec<Operation>, P>,
365    pub operation_id_methods: TemplateBuildMethodFnMap<'a, L, OperationId, P>,
366}
367
368impl<L: ?Sized, P> OperationTemplateBuildFnTable<'_, L, P> {
369    pub fn empty() -> Self {
370        Self {
371            operation_methods: HashMap::new(),
372            operation_list_methods: HashMap::new(),
373            operation_id_methods: HashMap::new(),
374        }
375    }
376
377    pub fn merge(&mut self, other: Self) {
378        let Self {
379            operation_methods,
380            operation_list_methods,
381            operation_id_methods,
382        } = other;
383
384        merge_fn_map(&mut self.operation_methods, operation_methods);
385        merge_fn_map(&mut self.operation_list_methods, operation_list_methods);
386        merge_fn_map(&mut self.operation_id_methods, operation_id_methods);
387    }
388}
389
390impl<'a, L> OperationTemplateBuildFnTable<'a, L, L::Property>
391where
392    L: TemplateLanguage<'a> + OperationTemplateEnvironment + ?Sized,
393    L::Property: OperationTemplatePropertyVar<'a>,
394{
395    /// Creates new symbol table containing the builtin methods.
396    pub fn builtin() -> Self {
397        Self {
398            operation_methods: builtin_operation_methods(),
399            operation_list_methods: template_builder::builtin_unformattable_list_methods(),
400            operation_id_methods: builtin_operation_id_methods(),
401        }
402    }
403
404    /// Applies the method call node `function` to the given `property` by using
405    /// this symbol table.
406    pub fn build_method(
407        &self,
408        language: &L,
409        diagnostics: &mut TemplateDiagnostics,
410        build_ctx: &BuildContext<L::Property>,
411        property: OperationTemplatePropertyKind<'a>,
412        function: &FunctionCallNode,
413    ) -> TemplateParseResult<L::Property> {
414        let type_name = property.type_name();
415        match property {
416            OperationTemplatePropertyKind::Operation(property) => {
417                let table = &self.operation_methods;
418                let build = template_parser::lookup_method(type_name, table, function)?;
419                build(language, diagnostics, build_ctx, property, function)
420            }
421            OperationTemplatePropertyKind::OperationOpt(property) => {
422                let type_name = "Operation";
423                let table = &self.operation_methods;
424                let build = template_parser::lookup_method(type_name, table, function)?;
425                let inner_property = property.try_unwrap(type_name).into_dyn();
426                build(language, diagnostics, build_ctx, inner_property, function)
427            }
428            OperationTemplatePropertyKind::OperationList(property) => {
429                let table = &self.operation_list_methods;
430                let build = template_parser::lookup_method(type_name, table, function)?;
431                build(language, diagnostics, build_ctx, property, function)
432            }
433            OperationTemplatePropertyKind::OperationId(property) => {
434                let table = &self.operation_id_methods;
435                let build = template_parser::lookup_method(type_name, table, function)?;
436                build(language, diagnostics, build_ctx, property, function)
437            }
438        }
439    }
440}
441
442/// Symbol table of methods available in [`OperationTemplateLanguage`].
443pub struct OperationTemplateLanguageBuildFnTable {
444    pub core: CoreTemplateBuildFnTable<'static, OperationTemplateLanguage>,
445    pub operation: OperationTemplateBuildFnTable<'static, OperationTemplateLanguage>,
446}
447
448impl OperationTemplateLanguageBuildFnTable {
449    pub fn empty() -> Self {
450        Self {
451            core: CoreTemplateBuildFnTable::empty(),
452            operation: OperationTemplateBuildFnTable::empty(),
453        }
454    }
455
456    fn merge(&mut self, other: Self) {
457        let Self { core, operation } = other;
458
459        self.core.merge(core);
460        self.operation.merge(operation);
461    }
462
463    /// Creates new symbol table containing the builtin methods.
464    fn builtin() -> Self {
465        Self {
466            core: CoreTemplateBuildFnTable::builtin(),
467            operation: OperationTemplateBuildFnTable::builtin(),
468        }
469    }
470}
471
472fn builtin_operation_methods<'a, L>() -> TemplateBuildMethodFnMap<'a, L, Operation>
473where
474    L: TemplateLanguage<'a> + OperationTemplateEnvironment + ?Sized,
475    L::Property: OperationTemplatePropertyVar<'a>,
476{
477    // Not using maplit::hashmap!{} or custom declarative macro here because
478    // code completion inside macro is quite restricted.
479    let mut map = TemplateBuildMethodFnMap::<L, Operation>::new();
480    map.insert(
481        "current_operation",
482        |language, _diagnostics, _build_ctx, self_property, function| {
483            function.expect_no_arguments()?;
484            let current_op_id = language.current_op_id().cloned();
485            let out_property = self_property.map(move |op| Some(op.id()) == current_op_id.as_ref());
486            Ok(out_property.into_dyn_wrapped())
487        },
488    );
489    map.insert(
490        "description",
491        |_language, _diagnostics, _build_ctx, self_property, function| {
492            function.expect_no_arguments()?;
493            let out_property = self_property.map(|op| op.metadata().description.clone());
494            Ok(out_property.into_dyn_wrapped())
495        },
496    );
497    map.insert(
498        "id",
499        |_language, _diagnostics, _build_ctx, self_property, function| {
500            function.expect_no_arguments()?;
501            let out_property = self_property.map(|op| op.id().clone());
502            Ok(out_property.into_dyn_wrapped())
503        },
504    );
505    map.insert(
506        "tags",
507        |_language, _diagnostics, _build_ctx, self_property, function| {
508            function.expect_no_arguments()?;
509            let out_property = self_property.map(|op| {
510                // TODO: introduce map type
511                op.metadata()
512                    .tags
513                    .iter()
514                    .map(|(key, value)| format!("{key}: {value}"))
515                    .join("\n")
516            });
517            Ok(out_property.into_dyn_wrapped())
518        },
519    );
520    map.insert(
521        "snapshot",
522        |_language, _diagnostics, _build_ctx, self_property, function| {
523            function.expect_no_arguments()?;
524            let out_property = self_property.map(|op| op.metadata().is_snapshot);
525            Ok(out_property.into_dyn_wrapped())
526        },
527    );
528    map.insert(
529        "time",
530        |_language, _diagnostics, _build_ctx, self_property, function| {
531            function.expect_no_arguments()?;
532            let out_property = self_property.map(|op| op.metadata().time.clone());
533            Ok(out_property.into_dyn_wrapped())
534        },
535    );
536    map.insert(
537        "user",
538        |_language, _diagnostics, _build_ctx, self_property, function| {
539            function.expect_no_arguments()?;
540            let out_property = self_property.map(|op| {
541                // TODO: introduce dedicated type and provide accessors?
542                format!("{}@{}", op.metadata().username, op.metadata().hostname)
543            });
544            Ok(out_property.into_dyn_wrapped())
545        },
546    );
547    map.insert(
548        "root",
549        |language, _diagnostics, _build_ctx, self_property, function| {
550            function.expect_no_arguments()?;
551            let op_store = language.repo_loader().op_store();
552            let root_op_id = op_store.root_operation_id().clone();
553            let out_property = self_property.map(move |op| op.id() == &root_op_id);
554            Ok(out_property.into_dyn_wrapped())
555        },
556    );
557    map.insert(
558        "parents",
559        |_language, _diagnostics, _build_ctx, self_property, function| {
560            function.expect_no_arguments()?;
561            let out_property = self_property.and_then(|op| {
562                let ops: Vec<_> = op.parents().try_collect()?;
563                Ok(ops)
564            });
565            Ok(out_property.into_dyn_wrapped())
566        },
567    );
568    map
569}
570
571impl Template for OperationId {
572    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
573        write!(formatter, "{}", self.hex())
574    }
575}
576
577fn builtin_operation_id_methods<'a, L>() -> TemplateBuildMethodFnMap<'a, L, OperationId>
578where
579    L: TemplateLanguage<'a> + OperationTemplateEnvironment + ?Sized,
580    L::Property: OperationTemplatePropertyVar<'a>,
581{
582    // Not using maplit::hashmap!{} or custom declarative macro here because
583    // code completion inside macro is quite restricted.
584    let mut map = TemplateBuildMethodFnMap::<L, OperationId>::new();
585    map.insert(
586        "short",
587        |language, diagnostics, build_ctx, self_property, function| {
588            let ([], [len_node]) = function.expect_arguments()?;
589            let len_property = len_node
590                .map(|node| {
591                    template_builder::expect_usize_expression(
592                        language,
593                        diagnostics,
594                        build_ctx,
595                        node,
596                    )
597                })
598                .transpose()?;
599            let out_property = (self_property, len_property).map(|(id, len)| {
600                let mut hex = id.hex();
601                hex.truncate(len.unwrap_or(12));
602                hex
603            });
604            Ok(out_property.into_dyn_wrapped())
605        },
606    );
607    map
608}