Skip to main content

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