1use 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
62pub trait OperationTemplateEnvironment {
64 fn repo_loader(&self) -> &RepoLoader;
65 fn current_op_id(&self) -> Option<&OperationId>;
66}
67
68pub 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 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 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
164pub 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
174pub 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
182macro_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
297pub 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
398pub 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 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 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
479pub 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 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 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 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 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 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 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 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}