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
15use std::any::Any;
16use std::cmp::Ordering;
17use std::collections::HashMap;
18use std::io;
19
20use itertools::Itertools as _;
21use jj_lib::extensions_map::ExtensionsMap;
22use jj_lib::object_id::ObjectId as _;
23use jj_lib::op_store::OperationId;
24use jj_lib::operation::Operation;
25use jj_lib::repo::RepoLoader;
26use jj_lib::settings::UserSettings;
27
28use crate::template_builder;
29use crate::template_builder::merge_fn_map;
30use crate::template_builder::BuildContext;
31use crate::template_builder::CoreTemplateBuildFnTable;
32use crate::template_builder::CoreTemplatePropertyKind;
33use crate::template_builder::CoreTemplatePropertyVar;
34use crate::template_builder::TemplateBuildMethodFnMap;
35use crate::template_builder::TemplateLanguage;
36use crate::template_parser;
37use crate::template_parser::FunctionCallNode;
38use crate::template_parser::TemplateDiagnostics;
39use crate::template_parser::TemplateParseResult;
40use crate::templater::BoxedTemplateProperty;
41use crate::templater::ListTemplate;
42use crate::templater::PlainTextFormattedProperty;
43use crate::templater::Template;
44use crate::templater::TemplateFormatter;
45use crate::templater::TemplatePropertyExt as _;
46use crate::templater::TimestampRange;
47
48pub trait OperationTemplateLanguageExtension {
49    fn build_fn_table(&self) -> OperationTemplateBuildFnTable;
50
51    fn build_cache_extensions(&self, extensions: &mut ExtensionsMap);
52}
53
54pub struct OperationTemplateLanguage {
55    repo_loader: RepoLoader,
56    current_op_id: Option<OperationId>,
57    build_fn_table: OperationTemplateBuildFnTable,
58    cache_extensions: ExtensionsMap,
59}
60
61impl OperationTemplateLanguage {
62    /// Sets up environment where operation template will be transformed to
63    /// evaluation tree.
64    pub fn new(
65        repo_loader: &RepoLoader,
66        current_op_id: Option<&OperationId>,
67        extensions: &[impl AsRef<dyn OperationTemplateLanguageExtension>],
68    ) -> Self {
69        let mut build_fn_table = OperationTemplateBuildFnTable::builtin();
70        let mut cache_extensions = ExtensionsMap::empty();
71
72        for extension in extensions {
73            build_fn_table.merge(extension.as_ref().build_fn_table());
74            extension
75                .as_ref()
76                .build_cache_extensions(&mut cache_extensions);
77        }
78
79        OperationTemplateLanguage {
80            // Clone these to keep lifetime simple
81            repo_loader: repo_loader.clone(),
82            current_op_id: current_op_id.cloned(),
83            build_fn_table,
84            cache_extensions,
85        }
86    }
87}
88
89impl TemplateLanguage<'static> for OperationTemplateLanguage {
90    type Property = OperationTemplatePropertyKind;
91
92    fn settings(&self) -> &UserSettings {
93        self.repo_loader.settings()
94    }
95
96    fn build_function(
97        &self,
98        diagnostics: &mut TemplateDiagnostics,
99        build_ctx: &BuildContext<Self::Property>,
100        function: &FunctionCallNode,
101    ) -> TemplateParseResult<Self::Property> {
102        let table = &self.build_fn_table.core;
103        table.build_function(self, diagnostics, build_ctx, function)
104    }
105
106    fn build_method(
107        &self,
108        diagnostics: &mut TemplateDiagnostics,
109        build_ctx: &BuildContext<Self::Property>,
110        property: Self::Property,
111        function: &FunctionCallNode,
112    ) -> TemplateParseResult<Self::Property> {
113        let type_name = property.type_name();
114        match property {
115            OperationTemplatePropertyKind::Core(property) => {
116                let table = &self.build_fn_table.core;
117                table.build_method(self, diagnostics, build_ctx, property, function)
118            }
119            OperationTemplatePropertyKind::Operation(property) => {
120                let table = &self.build_fn_table.operation_methods;
121                let build = template_parser::lookup_method(type_name, table, function)?;
122                build(self, diagnostics, build_ctx, property, function)
123            }
124            OperationTemplatePropertyKind::OperationId(property) => {
125                let table = &self.build_fn_table.operation_id_methods;
126                let build = template_parser::lookup_method(type_name, table, function)?;
127                build(self, diagnostics, build_ctx, property, function)
128            }
129        }
130    }
131}
132
133impl OperationTemplateLanguage {
134    pub fn cache_extension<T: Any>(&self) -> Option<&T> {
135        self.cache_extensions.get::<T>()
136    }
137}
138
139pub enum OperationTemplatePropertyKind {
140    Core(CoreTemplatePropertyKind<'static>),
141    Operation(BoxedTemplateProperty<'static, Operation>),
142    OperationId(BoxedTemplateProperty<'static, OperationId>),
143}
144
145template_builder::impl_core_property_wrappers!(OperationTemplatePropertyKind => Core);
146template_builder::impl_property_wrappers!(OperationTemplatePropertyKind {
147    Operation(Operation),
148    OperationId(OperationId),
149});
150
151impl CoreTemplatePropertyVar<'static> for OperationTemplatePropertyKind {
152    fn wrap_template(template: Box<dyn Template>) -> Self {
153        Self::Core(CoreTemplatePropertyKind::wrap_template(template))
154    }
155
156    fn wrap_list_template(template: Box<dyn ListTemplate>) -> Self {
157        Self::Core(CoreTemplatePropertyKind::wrap_list_template(template))
158    }
159
160    fn type_name(&self) -> &'static str {
161        match self {
162            Self::Core(property) => property.type_name(),
163            Self::Operation(_) => "Operation",
164            Self::OperationId(_) => "OperationId",
165        }
166    }
167
168    fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'static, bool>> {
169        match self {
170            Self::Core(property) => property.try_into_boolean(),
171            Self::Operation(_) => None,
172            Self::OperationId(_) => None,
173        }
174    }
175
176    fn try_into_integer(self) -> Option<BoxedTemplateProperty<'static, i64>> {
177        match self {
178            Self::Core(property) => property.try_into_integer(),
179            _ => None,
180        }
181    }
182
183    fn try_into_plain_text(self) -> Option<BoxedTemplateProperty<'static, String>> {
184        match self {
185            Self::Core(property) => property.try_into_plain_text(),
186            _ => {
187                let template = self.try_into_template()?;
188                Some(PlainTextFormattedProperty::new(template).into_dyn())
189            }
190        }
191    }
192
193    fn try_into_template(self) -> Option<Box<dyn Template>> {
194        match self {
195            Self::Core(property) => property.try_into_template(),
196            Self::Operation(_) => None,
197            Self::OperationId(property) => Some(property.into_template()),
198        }
199    }
200
201    fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'static, bool>> {
202        match (self, other) {
203            (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_eq(rhs),
204            (Self::Core(_), _) => None,
205            (Self::Operation(_), _) => None,
206            (Self::OperationId(_), _) => None,
207        }
208    }
209
210    fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'static, Ordering>> {
211        match (self, other) {
212            (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_cmp(rhs),
213            (Self::Core(_), _) => None,
214            (Self::Operation(_), _) => None,
215            (Self::OperationId(_), _) => None,
216        }
217    }
218}
219
220/// Table of functions that translate method call node of self type `T`.
221pub type OperationTemplateBuildMethodFnMap<T> =
222    TemplateBuildMethodFnMap<'static, OperationTemplateLanguage, T>;
223
224/// Symbol table of methods available in the operation template.
225pub struct OperationTemplateBuildFnTable {
226    pub core: CoreTemplateBuildFnTable<'static, OperationTemplateLanguage>,
227    pub operation_methods: OperationTemplateBuildMethodFnMap<Operation>,
228    pub operation_id_methods: OperationTemplateBuildMethodFnMap<OperationId>,
229}
230
231impl OperationTemplateBuildFnTable {
232    /// Creates new symbol table containing the builtin methods.
233    fn builtin() -> Self {
234        OperationTemplateBuildFnTable {
235            core: CoreTemplateBuildFnTable::builtin(),
236            operation_methods: builtin_operation_methods(),
237            operation_id_methods: builtin_operation_id_methods(),
238        }
239    }
240
241    pub fn empty() -> Self {
242        OperationTemplateBuildFnTable {
243            core: CoreTemplateBuildFnTable::empty(),
244            operation_methods: HashMap::new(),
245            operation_id_methods: HashMap::new(),
246        }
247    }
248
249    fn merge(&mut self, other: OperationTemplateBuildFnTable) {
250        let OperationTemplateBuildFnTable {
251            core,
252            operation_methods,
253            operation_id_methods,
254        } = other;
255
256        self.core.merge(core);
257        merge_fn_map(&mut self.operation_methods, operation_methods);
258        merge_fn_map(&mut self.operation_id_methods, operation_id_methods);
259    }
260}
261
262fn builtin_operation_methods() -> OperationTemplateBuildMethodFnMap<Operation> {
263    // Not using maplit::hashmap!{} or custom declarative macro here because
264    // code completion inside macro is quite restricted.
265    let mut map = OperationTemplateBuildMethodFnMap::<Operation>::new();
266    map.insert(
267        "current_operation",
268        |language, _diagnostics, _build_ctx, self_property, function| {
269            function.expect_no_arguments()?;
270            let current_op_id = language.current_op_id.clone();
271            let out_property = self_property.map(move |op| Some(op.id()) == current_op_id.as_ref());
272            Ok(out_property.into_dyn_wrapped())
273        },
274    );
275    map.insert(
276        "description",
277        |_language, _diagnostics, _build_ctx, self_property, function| {
278            function.expect_no_arguments()?;
279            let out_property = self_property.map(|op| op.metadata().description.clone());
280            Ok(out_property.into_dyn_wrapped())
281        },
282    );
283    map.insert(
284        "id",
285        |_language, _diagnostics, _build_ctx, self_property, function| {
286            function.expect_no_arguments()?;
287            let out_property = self_property.map(|op| op.id().clone());
288            Ok(out_property.into_dyn_wrapped())
289        },
290    );
291    map.insert(
292        "tags",
293        |_language, _diagnostics, _build_ctx, self_property, function| {
294            function.expect_no_arguments()?;
295            let out_property = self_property.map(|op| {
296                // TODO: introduce map type
297                op.metadata()
298                    .tags
299                    .iter()
300                    .map(|(key, value)| format!("{key}: {value}"))
301                    .join("\n")
302            });
303            Ok(out_property.into_dyn_wrapped())
304        },
305    );
306    map.insert(
307        "snapshot",
308        |_language, _diagnostics, _build_ctx, self_property, function| {
309            function.expect_no_arguments()?;
310            let out_property = self_property.map(|op| op.metadata().is_snapshot);
311            Ok(out_property.into_dyn_wrapped())
312        },
313    );
314    map.insert(
315        "time",
316        |_language, _diagnostics, _build_ctx, self_property, function| {
317            function.expect_no_arguments()?;
318            let out_property = self_property.map(|op| TimestampRange {
319                start: op.metadata().start_time,
320                end: op.metadata().end_time,
321            });
322            Ok(out_property.into_dyn_wrapped())
323        },
324    );
325    map.insert(
326        "user",
327        |_language, _diagnostics, _build_ctx, self_property, function| {
328            function.expect_no_arguments()?;
329            let out_property = self_property.map(|op| {
330                // TODO: introduce dedicated type and provide accessors?
331                format!("{}@{}", op.metadata().username, op.metadata().hostname)
332            });
333            Ok(out_property.into_dyn_wrapped())
334        },
335    );
336    map.insert(
337        "root",
338        |language, _diagnostics, _build_ctx, self_property, function| {
339            function.expect_no_arguments()?;
340            let root_op_id = language.repo_loader.op_store().root_operation_id().clone();
341            let out_property = self_property.map(move |op| op.id() == &root_op_id);
342            Ok(out_property.into_dyn_wrapped())
343        },
344    );
345    map
346}
347
348impl Template for OperationId {
349    fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
350        write!(formatter, "{}", self.hex())
351    }
352}
353
354fn builtin_operation_id_methods() -> OperationTemplateBuildMethodFnMap<OperationId> {
355    // Not using maplit::hashmap!{} or custom declarative macro here because
356    // code completion inside macro is quite restricted.
357    let mut map = OperationTemplateBuildMethodFnMap::<OperationId>::new();
358    map.insert(
359        "short",
360        |language, diagnostics, build_ctx, self_property, function| {
361            let ([], [len_node]) = function.expect_arguments()?;
362            let len_property = len_node
363                .map(|node| {
364                    template_builder::expect_usize_expression(
365                        language,
366                        diagnostics,
367                        build_ctx,
368                        node,
369                    )
370                })
371                .transpose()?;
372            let out_property = (self_property, len_property).map(|(id, len)| {
373                let mut hex = id.hex();
374                hex.truncate(len.unwrap_or(12));
375                hex
376            });
377            Ok(out_property.into_dyn_wrapped())
378        },
379    );
380    map
381}