1use std::any::Any;
16use std::cmp::Ordering;
17use std::collections::HashMap;
18use std::io;
19
20use itertools::Itertools as _;
21use jj_lib::extensions_map::ExtensionsMap;
22use jj_lib::object_id::ObjectId as _;
23use jj_lib::op_store::OperationId;
24use jj_lib::operation::Operation;
25use jj_lib::repo::RepoLoader;
26use jj_lib::settings::UserSettings;
27
28use crate::template_builder;
29use crate::template_builder::merge_fn_map;
30use crate::template_builder::BuildContext;
31use crate::template_builder::CoreTemplateBuildFnTable;
32use crate::template_builder::CoreTemplatePropertyKind;
33use crate::template_builder::CoreTemplatePropertyVar;
34use crate::template_builder::TemplateBuildMethodFnMap;
35use crate::template_builder::TemplateLanguage;
36use crate::template_parser;
37use crate::template_parser::FunctionCallNode;
38use crate::template_parser::TemplateDiagnostics;
39use crate::template_parser::TemplateParseResult;
40use crate::templater::BoxedTemplateProperty;
41use crate::templater::ListTemplate;
42use crate::templater::PlainTextFormattedProperty;
43use crate::templater::Template;
44use crate::templater::TemplateFormatter;
45use crate::templater::TemplatePropertyExt as _;
46use crate::templater::TimestampRange;
47
48pub trait OperationTemplateLanguageExtension {
49 fn build_fn_table(&self) -> OperationTemplateBuildFnTable;
50
51 fn build_cache_extensions(&self, extensions: &mut ExtensionsMap);
52}
53
54pub struct OperationTemplateLanguage {
55 repo_loader: RepoLoader,
56 current_op_id: Option<OperationId>,
57 build_fn_table: OperationTemplateBuildFnTable,
58 cache_extensions: ExtensionsMap,
59}
60
61impl OperationTemplateLanguage {
62 pub fn new(
65 repo_loader: &RepoLoader,
66 current_op_id: Option<&OperationId>,
67 extensions: &[impl AsRef<dyn OperationTemplateLanguageExtension>],
68 ) -> Self {
69 let mut build_fn_table = OperationTemplateBuildFnTable::builtin();
70 let mut cache_extensions = ExtensionsMap::empty();
71
72 for extension in extensions {
73 build_fn_table.merge(extension.as_ref().build_fn_table());
74 extension
75 .as_ref()
76 .build_cache_extensions(&mut cache_extensions);
77 }
78
79 OperationTemplateLanguage {
80 repo_loader: repo_loader.clone(),
82 current_op_id: current_op_id.cloned(),
83 build_fn_table,
84 cache_extensions,
85 }
86 }
87}
88
89impl TemplateLanguage<'static> for OperationTemplateLanguage {
90 type Property = OperationTemplatePropertyKind;
91
92 fn settings(&self) -> &UserSettings {
93 self.repo_loader.settings()
94 }
95
96 fn build_function(
97 &self,
98 diagnostics: &mut TemplateDiagnostics,
99 build_ctx: &BuildContext<Self::Property>,
100 function: &FunctionCallNode,
101 ) -> TemplateParseResult<Self::Property> {
102 let table = &self.build_fn_table.core;
103 table.build_function(self, diagnostics, build_ctx, function)
104 }
105
106 fn build_method(
107 &self,
108 diagnostics: &mut TemplateDiagnostics,
109 build_ctx: &BuildContext<Self::Property>,
110 property: Self::Property,
111 function: &FunctionCallNode,
112 ) -> TemplateParseResult<Self::Property> {
113 let type_name = property.type_name();
114 match property {
115 OperationTemplatePropertyKind::Core(property) => {
116 let table = &self.build_fn_table.core;
117 table.build_method(self, diagnostics, build_ctx, property, function)
118 }
119 OperationTemplatePropertyKind::Operation(property) => {
120 let table = &self.build_fn_table.operation_methods;
121 let build = template_parser::lookup_method(type_name, table, function)?;
122 build(self, diagnostics, build_ctx, property, function)
123 }
124 OperationTemplatePropertyKind::OperationId(property) => {
125 let table = &self.build_fn_table.operation_id_methods;
126 let build = template_parser::lookup_method(type_name, table, function)?;
127 build(self, diagnostics, build_ctx, property, function)
128 }
129 }
130 }
131}
132
133impl OperationTemplateLanguage {
134 pub fn cache_extension<T: Any>(&self) -> Option<&T> {
135 self.cache_extensions.get::<T>()
136 }
137}
138
139pub enum OperationTemplatePropertyKind {
140 Core(CoreTemplatePropertyKind<'static>),
141 Operation(BoxedTemplateProperty<'static, Operation>),
142 OperationId(BoxedTemplateProperty<'static, OperationId>),
143}
144
145template_builder::impl_core_property_wrappers!(OperationTemplatePropertyKind => Core);
146template_builder::impl_property_wrappers!(OperationTemplatePropertyKind {
147 Operation(Operation),
148 OperationId(OperationId),
149});
150
151impl CoreTemplatePropertyVar<'static> for OperationTemplatePropertyKind {
152 fn wrap_template(template: Box<dyn Template>) -> Self {
153 Self::Core(CoreTemplatePropertyKind::wrap_template(template))
154 }
155
156 fn wrap_list_template(template: Box<dyn ListTemplate>) -> Self {
157 Self::Core(CoreTemplatePropertyKind::wrap_list_template(template))
158 }
159
160 fn type_name(&self) -> &'static str {
161 match self {
162 Self::Core(property) => property.type_name(),
163 Self::Operation(_) => "Operation",
164 Self::OperationId(_) => "OperationId",
165 }
166 }
167
168 fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'static, bool>> {
169 match self {
170 Self::Core(property) => property.try_into_boolean(),
171 Self::Operation(_) => None,
172 Self::OperationId(_) => None,
173 }
174 }
175
176 fn try_into_integer(self) -> Option<BoxedTemplateProperty<'static, i64>> {
177 match self {
178 Self::Core(property) => property.try_into_integer(),
179 _ => None,
180 }
181 }
182
183 fn try_into_plain_text(self) -> Option<BoxedTemplateProperty<'static, String>> {
184 match self {
185 Self::Core(property) => property.try_into_plain_text(),
186 _ => {
187 let template = self.try_into_template()?;
188 Some(PlainTextFormattedProperty::new(template).into_dyn())
189 }
190 }
191 }
192
193 fn try_into_template(self) -> Option<Box<dyn Template>> {
194 match self {
195 Self::Core(property) => property.try_into_template(),
196 Self::Operation(_) => None,
197 Self::OperationId(property) => Some(property.into_template()),
198 }
199 }
200
201 fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'static, bool>> {
202 match (self, other) {
203 (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_eq(rhs),
204 (Self::Core(_), _) => None,
205 (Self::Operation(_), _) => None,
206 (Self::OperationId(_), _) => None,
207 }
208 }
209
210 fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'static, Ordering>> {
211 match (self, other) {
212 (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_cmp(rhs),
213 (Self::Core(_), _) => None,
214 (Self::Operation(_), _) => None,
215 (Self::OperationId(_), _) => None,
216 }
217 }
218}
219
220pub type OperationTemplateBuildMethodFnMap<T> =
222 TemplateBuildMethodFnMap<'static, OperationTemplateLanguage, T>;
223
224pub struct OperationTemplateBuildFnTable {
226 pub core: CoreTemplateBuildFnTable<'static, OperationTemplateLanguage>,
227 pub operation_methods: OperationTemplateBuildMethodFnMap<Operation>,
228 pub operation_id_methods: OperationTemplateBuildMethodFnMap<OperationId>,
229}
230
231impl OperationTemplateBuildFnTable {
232 fn builtin() -> Self {
234 OperationTemplateBuildFnTable {
235 core: CoreTemplateBuildFnTable::builtin(),
236 operation_methods: builtin_operation_methods(),
237 operation_id_methods: builtin_operation_id_methods(),
238 }
239 }
240
241 pub fn empty() -> Self {
242 OperationTemplateBuildFnTable {
243 core: CoreTemplateBuildFnTable::empty(),
244 operation_methods: HashMap::new(),
245 operation_id_methods: HashMap::new(),
246 }
247 }
248
249 fn merge(&mut self, other: OperationTemplateBuildFnTable) {
250 let OperationTemplateBuildFnTable {
251 core,
252 operation_methods,
253 operation_id_methods,
254 } = other;
255
256 self.core.merge(core);
257 merge_fn_map(&mut self.operation_methods, operation_methods);
258 merge_fn_map(&mut self.operation_id_methods, operation_id_methods);
259 }
260}
261
262fn builtin_operation_methods() -> OperationTemplateBuildMethodFnMap<Operation> {
263 let mut map = OperationTemplateBuildMethodFnMap::<Operation>::new();
266 map.insert(
267 "current_operation",
268 |language, _diagnostics, _build_ctx, self_property, function| {
269 function.expect_no_arguments()?;
270 let current_op_id = language.current_op_id.clone();
271 let out_property = self_property.map(move |op| Some(op.id()) == current_op_id.as_ref());
272 Ok(out_property.into_dyn_wrapped())
273 },
274 );
275 map.insert(
276 "description",
277 |_language, _diagnostics, _build_ctx, self_property, function| {
278 function.expect_no_arguments()?;
279 let out_property = self_property.map(|op| op.metadata().description.clone());
280 Ok(out_property.into_dyn_wrapped())
281 },
282 );
283 map.insert(
284 "id",
285 |_language, _diagnostics, _build_ctx, self_property, function| {
286 function.expect_no_arguments()?;
287 let out_property = self_property.map(|op| op.id().clone());
288 Ok(out_property.into_dyn_wrapped())
289 },
290 );
291 map.insert(
292 "tags",
293 |_language, _diagnostics, _build_ctx, self_property, function| {
294 function.expect_no_arguments()?;
295 let out_property = self_property.map(|op| {
296 op.metadata()
298 .tags
299 .iter()
300 .map(|(key, value)| format!("{key}: {value}"))
301 .join("\n")
302 });
303 Ok(out_property.into_dyn_wrapped())
304 },
305 );
306 map.insert(
307 "snapshot",
308 |_language, _diagnostics, _build_ctx, self_property, function| {
309 function.expect_no_arguments()?;
310 let out_property = self_property.map(|op| op.metadata().is_snapshot);
311 Ok(out_property.into_dyn_wrapped())
312 },
313 );
314 map.insert(
315 "time",
316 |_language, _diagnostics, _build_ctx, self_property, function| {
317 function.expect_no_arguments()?;
318 let out_property = self_property.map(|op| TimestampRange {
319 start: op.metadata().start_time,
320 end: op.metadata().end_time,
321 });
322 Ok(out_property.into_dyn_wrapped())
323 },
324 );
325 map.insert(
326 "user",
327 |_language, _diagnostics, _build_ctx, self_property, function| {
328 function.expect_no_arguments()?;
329 let out_property = self_property.map(|op| {
330 format!("{}@{}", op.metadata().username, op.metadata().hostname)
332 });
333 Ok(out_property.into_dyn_wrapped())
334 },
335 );
336 map.insert(
337 "root",
338 |language, _diagnostics, _build_ctx, self_property, function| {
339 function.expect_no_arguments()?;
340 let root_op_id = language.repo_loader.op_store().root_operation_id().clone();
341 let out_property = self_property.map(move |op| op.id() == &root_op_id);
342 Ok(out_property.into_dyn_wrapped())
343 },
344 );
345 map
346}
347
348impl Template for OperationId {
349 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
350 write!(formatter, "{}", self.hex())
351 }
352}
353
354fn builtin_operation_id_methods() -> OperationTemplateBuildMethodFnMap<OperationId> {
355 let mut map = OperationTemplateBuildMethodFnMap::<OperationId>::new();
358 map.insert(
359 "short",
360 |language, diagnostics, build_ctx, self_property, function| {
361 let ([], [len_node]) = function.expect_arguments()?;
362 let len_property = len_node
363 .map(|node| {
364 template_builder::expect_usize_expression(
365 language,
366 diagnostics,
367 build_ctx,
368 node,
369 )
370 })
371 .transpose()?;
372 let out_property = (self_property, len_property).map(|(id, len)| {
373 let mut hex = id.hex();
374 hex.truncate(len.unwrap_or(12));
375 hex
376 });
377 Ok(out_property.into_dyn_wrapped())
378 },
379 );
380 map
381}