1use 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
59pub trait OperationTemplateEnvironment {
61 fn repo_loader(&self) -> &RepoLoader;
62 fn current_op_id(&self) -> Option<&OperationId>;
63}
64
65pub 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 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 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
154pub 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
164pub 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
172macro_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
284pub 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
380pub 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 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 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
461pub 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 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 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 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 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 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}