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