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::PlainTextFormattedProperty;
42use crate::templater::Template;
43use crate::templater::TemplateFormatter;
44use crate::templater::TemplatePropertyExt as _;
45use crate::templater::TimestampRange;
46
47pub trait OperationTemplateLanguageExtension {
48 fn build_fn_table(&self) -> OperationTemplateBuildFnTable;
49
50 fn build_cache_extensions(&self, extensions: &mut ExtensionsMap);
51}
52
53pub struct OperationTemplateLanguage {
54 repo_loader: RepoLoader,
55 current_op_id: Option<OperationId>,
56 build_fn_table: OperationTemplateBuildFnTable,
57 cache_extensions: ExtensionsMap,
58}
59
60impl OperationTemplateLanguage {
61 pub fn new(
64 repo_loader: &RepoLoader,
65 current_op_id: Option<&OperationId>,
66 extensions: &[impl AsRef<dyn OperationTemplateLanguageExtension>],
67 ) -> Self {
68 let mut build_fn_table = OperationTemplateBuildFnTable::builtin();
69 let mut cache_extensions = ExtensionsMap::empty();
70
71 for extension in extensions {
72 build_fn_table.merge(extension.as_ref().build_fn_table());
73 extension
74 .as_ref()
75 .build_cache_extensions(&mut cache_extensions);
76 }
77
78 OperationTemplateLanguage {
79 repo_loader: repo_loader.clone(),
81 current_op_id: current_op_id.cloned(),
82 build_fn_table,
83 cache_extensions,
84 }
85 }
86}
87
88impl TemplateLanguage<'static> for OperationTemplateLanguage {
89 type Property = OperationTemplatePropertyKind;
90
91 fn settings(&self) -> &UserSettings {
92 self.repo_loader.settings()
93 }
94
95 fn build_function(
96 &self,
97 diagnostics: &mut TemplateDiagnostics,
98 build_ctx: &BuildContext<Self::Property>,
99 function: &FunctionCallNode,
100 ) -> TemplateParseResult<Self::Property> {
101 let table = &self.build_fn_table.core;
102 table.build_function(self, diagnostics, build_ctx, function)
103 }
104
105 fn build_method(
106 &self,
107 diagnostics: &mut TemplateDiagnostics,
108 build_ctx: &BuildContext<Self::Property>,
109 property: Self::Property,
110 function: &FunctionCallNode,
111 ) -> TemplateParseResult<Self::Property> {
112 let type_name = property.type_name();
113 match property {
114 OperationTemplatePropertyKind::Core(property) => {
115 let table = &self.build_fn_table.core;
116 table.build_method(self, diagnostics, build_ctx, property, function)
117 }
118 OperationTemplatePropertyKind::Operation(property) => {
119 let table = &self.build_fn_table.operation_methods;
120 let build = template_parser::lookup_method(type_name, table, function)?;
121 build(self, diagnostics, build_ctx, property, function)
122 }
123 OperationTemplatePropertyKind::OperationId(property) => {
124 let table = &self.build_fn_table.operation_id_methods;
125 let build = template_parser::lookup_method(type_name, table, function)?;
126 build(self, diagnostics, build_ctx, property, function)
127 }
128 }
129 }
130}
131
132impl OperationTemplateLanguage {
133 pub fn cache_extension<T: Any>(&self) -> Option<&T> {
134 self.cache_extensions.get::<T>()
135 }
136}
137
138pub enum OperationTemplatePropertyKind {
139 Core(CoreTemplatePropertyKind<'static>),
140 Operation(BoxedTemplateProperty<'static, Operation>),
141 OperationId(BoxedTemplateProperty<'static, OperationId>),
142}
143
144impl OperationTemplatePropertyKind {
145 template_builder::impl_wrap_property_fns!('static, OperationTemplatePropertyKind, {
146 pub wrap_operation(Operation) => Operation,
147 pub wrap_operation_id(OperationId) => OperationId,
148 });
149}
150
151impl CoreTemplatePropertyVar<'static> for OperationTemplatePropertyKind {
152 template_builder::impl_core_wrap_property_fns!('static, OperationTemplatePropertyKind::Core);
153
154 fn type_name(&self) -> &'static str {
155 match self {
156 OperationTemplatePropertyKind::Core(property) => property.type_name(),
157 OperationTemplatePropertyKind::Operation(_) => "Operation",
158 OperationTemplatePropertyKind::OperationId(_) => "OperationId",
159 }
160 }
161
162 fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'static, bool>> {
163 match self {
164 OperationTemplatePropertyKind::Core(property) => property.try_into_boolean(),
165 OperationTemplatePropertyKind::Operation(_) => None,
166 OperationTemplatePropertyKind::OperationId(_) => None,
167 }
168 }
169
170 fn try_into_integer(self) -> Option<BoxedTemplateProperty<'static, i64>> {
171 match self {
172 OperationTemplatePropertyKind::Core(property) => property.try_into_integer(),
173 _ => None,
174 }
175 }
176
177 fn try_into_plain_text(self) -> Option<BoxedTemplateProperty<'static, String>> {
178 match self {
179 OperationTemplatePropertyKind::Core(property) => property.try_into_plain_text(),
180 _ => {
181 let template = self.try_into_template()?;
182 Some(PlainTextFormattedProperty::new(template).into_dyn())
183 }
184 }
185 }
186
187 fn try_into_template(self) -> Option<Box<dyn Template>> {
188 match self {
189 OperationTemplatePropertyKind::Core(property) => property.try_into_template(),
190 OperationTemplatePropertyKind::Operation(_) => None,
191 OperationTemplatePropertyKind::OperationId(property) => Some(property.into_template()),
192 }
193 }
194
195 fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'static, bool>> {
196 match (self, other) {
197 (
198 OperationTemplatePropertyKind::Core(lhs),
199 OperationTemplatePropertyKind::Core(rhs),
200 ) => lhs.try_into_eq(rhs),
201 (OperationTemplatePropertyKind::Core(_), _) => None,
202 (OperationTemplatePropertyKind::Operation(_), _) => None,
203 (OperationTemplatePropertyKind::OperationId(_), _) => None,
204 }
205 }
206
207 fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'static, Ordering>> {
208 match (self, other) {
209 (
210 OperationTemplatePropertyKind::Core(lhs),
211 OperationTemplatePropertyKind::Core(rhs),
212 ) => lhs.try_into_cmp(rhs),
213 (OperationTemplatePropertyKind::Core(_), _) => None,
214 (OperationTemplatePropertyKind::Operation(_), _) => None,
215 (OperationTemplatePropertyKind::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 type P = OperationTemplatePropertyKind;
264 let mut map = OperationTemplateBuildMethodFnMap::<Operation>::new();
267 map.insert(
268 "current_operation",
269 |language, _diagnostics, _build_ctx, self_property, function| {
270 function.expect_no_arguments()?;
271 let current_op_id = language.current_op_id.clone();
272 let out_property = self_property.map(move |op| Some(op.id()) == current_op_id.as_ref());
273 Ok(P::wrap_boolean(out_property.into_dyn()))
274 },
275 );
276 map.insert(
277 "description",
278 |_language, _diagnostics, _build_ctx, self_property, function| {
279 function.expect_no_arguments()?;
280 let out_property = self_property.map(|op| op.metadata().description.clone());
281 Ok(P::wrap_string(out_property.into_dyn()))
282 },
283 );
284 map.insert(
285 "id",
286 |_language, _diagnostics, _build_ctx, self_property, function| {
287 function.expect_no_arguments()?;
288 let out_property = self_property.map(|op| op.id().clone());
289 Ok(P::wrap_operation_id(out_property.into_dyn()))
290 },
291 );
292 map.insert(
293 "tags",
294 |_language, _diagnostics, _build_ctx, self_property, function| {
295 function.expect_no_arguments()?;
296 let out_property = self_property.map(|op| {
297 op.metadata()
299 .tags
300 .iter()
301 .map(|(key, value)| format!("{key}: {value}"))
302 .join("\n")
303 });
304 Ok(P::wrap_string(out_property.into_dyn()))
305 },
306 );
307 map.insert(
308 "snapshot",
309 |_language, _diagnostics, _build_ctx, self_property, function| {
310 function.expect_no_arguments()?;
311 let out_property = self_property.map(|op| op.metadata().is_snapshot);
312 Ok(P::wrap_boolean(out_property.into_dyn()))
313 },
314 );
315 map.insert(
316 "time",
317 |_language, _diagnostics, _build_ctx, self_property, function| {
318 function.expect_no_arguments()?;
319 let out_property = self_property.map(|op| TimestampRange {
320 start: op.metadata().start_time,
321 end: op.metadata().end_time,
322 });
323 Ok(P::wrap_timestamp_range(out_property.into_dyn()))
324 },
325 );
326 map.insert(
327 "user",
328 |_language, _diagnostics, _build_ctx, self_property, function| {
329 function.expect_no_arguments()?;
330 let out_property = self_property.map(|op| {
331 format!("{}@{}", op.metadata().username, op.metadata().hostname)
333 });
334 Ok(P::wrap_string(out_property.into_dyn()))
335 },
336 );
337 map.insert(
338 "root",
339 |language, _diagnostics, _build_ctx, self_property, function| {
340 function.expect_no_arguments()?;
341 let root_op_id = language.repo_loader.op_store().root_operation_id().clone();
342 let out_property = self_property.map(move |op| op.id() == &root_op_id);
343 Ok(P::wrap_boolean(out_property.into_dyn()))
344 },
345 );
346 map
347}
348
349impl Template for OperationId {
350 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> {
351 write!(formatter, "{}", self.hex())
352 }
353}
354
355fn builtin_operation_id_methods() -> OperationTemplateBuildMethodFnMap<OperationId> {
356 type P = OperationTemplatePropertyKind;
357 let mut map = OperationTemplateBuildMethodFnMap::<OperationId>::new();
360 map.insert(
361 "short",
362 |language, diagnostics, build_ctx, self_property, function| {
363 let ([], [len_node]) = function.expect_arguments()?;
364 let len_property = len_node
365 .map(|node| {
366 template_builder::expect_usize_expression(
367 language,
368 diagnostics,
369 build_ctx,
370 node,
371 )
372 })
373 .transpose()?;
374 let out_property = (self_property, len_property).map(|(id, len)| {
375 let mut hex = id.hex();
376 hex.truncate(len.unwrap_or(12));
377 hex
378 });
379 Ok(P::wrap_string(out_property.into_dyn()))
380 },
381 );
382 map
383}