diff --git a/src/commands/operation.rs b/src/commands/operation.rs index 30753e7c1..9781ca564 100644 --- a/src/commands/operation.rs +++ b/src/commands/operation.rs @@ -7,10 +7,8 @@ use jujutsu_lib::operation::Operation; use crate::cli_util::{user_error, CommandError, CommandHelper}; use crate::formatter::Formatter; use crate::graphlog::{get_graphlog, Edge}; -use crate::templater::Template; -use crate::time_util::{ - format_absolute_timestamp, format_duration, format_timestamp_relative_to_now, -}; +use crate::templater::{Template, TimestampRange}; +use crate::time_util::format_timestamp_relative_to_now; use crate::ui::Ui; /// Commands for working with the operation log @@ -71,22 +69,19 @@ fn cmd_op_log( metadata.hostname )?; formatter.write_str(" ")?; + let time_range = TimestampRange { + start: metadata.start_time.clone(), + end: metadata.end_time.clone(), + }; if self.relative_timestamps { - let mut f = timeago::Formatter::new(); - f.min_unit(timeago::TimeUnit::Microseconds).ago(""); - let mut duration = format_duration(&metadata.start_time, &metadata.end_time, &f); - if duration == "now" { - duration = "less than a microsecond".to_string() - } - let start = format_timestamp_relative_to_now(&metadata.start_time); - write!(formatter.labeled("time"), "{start}, lasted {duration}")?; - } else { + let start = format_timestamp_relative_to_now(&time_range.start); write!( formatter.labeled("time"), - "{} - {}", - format_absolute_timestamp(&metadata.start_time), - format_absolute_timestamp(&metadata.end_time) + "{start}, lasted {duration}", + duration = time_range.duration() )?; + } else { + time_range.format(&(), formatter)?; } formatter.write_str("\n")?; write!( diff --git a/src/template_parser.rs b/src/template_parser.rs index 7dbee3e4a..ad032e6e4 100644 --- a/src/template_parser.rs +++ b/src/template_parser.rs @@ -27,7 +27,7 @@ use thiserror::Error; use crate::templater::{ ConditionalTemplate, IntoTemplate, LabelTemplate, ListTemplate, Literal, PlainTextFormattedProperty, SeparateTemplate, Template, TemplateFunction, TemplateProperty, - TemplatePropertyFn, + TemplatePropertyFn, TimestampRange, }; use crate::time_util; @@ -610,6 +610,10 @@ pub trait TemplateLanguage<'a> { &self, property: Box + 'a>, ) -> Self::Property; + fn wrap_timestamp_range( + &self, + property: Box + 'a>, + ) -> Self::Property; fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult; fn build_method( @@ -635,6 +639,7 @@ macro_rules! impl_core_wrap_property_fns { wrap_integer(i64) => Integer, wrap_signature(jujutsu_lib::backend::Signature) => Signature, wrap_timestamp(jujutsu_lib::backend::Timestamp) => Timestamp, + wrap_timestamp_range($crate::templater::TimestampRange) => TimestampRange, } ); }; @@ -672,6 +677,7 @@ pub enum CoreTemplatePropertyKind<'a, I> { Integer(Box + 'a>), Signature(Box + 'a>), Timestamp(Box + 'a>), + TimestampRange(Box + 'a>), } impl<'a, I: 'a> IntoTemplateProperty<'a, I> for CoreTemplatePropertyKind<'a, I> { @@ -708,6 +714,7 @@ impl<'a, I: 'a> IntoTemplate<'a, I> for CoreTemplatePropertyKind<'a, I> { CoreTemplatePropertyKind::Integer(property) => property.into_template(), CoreTemplatePropertyKind::Signature(property) => property.into_template(), CoreTemplatePropertyKind::Timestamp(property) => property.into_template(), + CoreTemplatePropertyKind::TimestampRange(property) => property.into_template(), } } } @@ -869,6 +876,9 @@ pub fn build_core_method<'a, L: TemplateLanguage<'a>>( CoreTemplatePropertyKind::Timestamp(property) => { build_timestamp_method(language, property, function) } + CoreTemplatePropertyKind::TimestampRange(property) => { + build_timestamp_range_method(language, property, function) + } } } @@ -977,6 +987,43 @@ fn build_timestamp_method<'a, L: TemplateLanguage<'a>>( Ok(property) } +fn build_timestamp_range_method<'a, L: TemplateLanguage<'a>>( + language: &L, + self_property: impl TemplateProperty + 'a, + function: &FunctionCallNode, +) -> TemplateParseResult { + let property = match function.name { + "start" => { + expect_no_arguments(function)?; + language.wrap_timestamp(chain_properties( + self_property, + TemplatePropertyFn(|time_range: &TimestampRange| time_range.start.clone()), + )) + } + "end" => { + expect_no_arguments(function)?; + language.wrap_timestamp(chain_properties( + self_property, + TemplatePropertyFn(|time_range: &TimestampRange| time_range.end.clone()), + )) + } + "duration" => { + expect_no_arguments(function)?; + language.wrap_string(chain_properties( + self_property, + TemplatePropertyFn(TimestampRange::duration), + )) + } + _ => { + return Err(TemplateParseError::no_such_method( + "TimestampRange", + function, + )) + } + }; + Ok(property) +} + fn build_global_function<'a, L: TemplateLanguage<'a>>( language: &L, function: &FunctionCallNode, diff --git a/src/templater.rs b/src/templater.rs index 0a79a86ab..1ef639678 100644 --- a/src/templater.rs +++ b/src/templater.rs @@ -73,6 +73,40 @@ impl Template<()> for Timestamp { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TimestampRange { + // Could be aliased to Range if needed. + pub start: Timestamp, + pub end: Timestamp, +} + +impl TimestampRange { + // TODO: Introduce duration type, and move formatting to it. + pub fn duration(&self) -> String { + let mut f = timeago::Formatter::new(); + f.min_unit(timeago::TimeUnit::Microseconds).ago(""); + let duration = time_util::format_duration(&self.start, &self.end, &f); + if duration == "now" { + "less than a microsecond".to_owned() + } else { + duration + } + } +} + +impl Template<()> for TimestampRange { + fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { + self.start.format(&(), formatter)?; + write!(formatter, " - ")?; + self.end.format(&(), formatter)?; + Ok(()) + } + + fn has_content(&self, _: &()) -> bool { + true + } +} + impl Template<()> for bool { fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { formatter.write_str(if *self { "true" } else { "false" })