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