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