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