From c1f6167e37909517676c30b0a80203739e8492a5 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Mon, 15 Nov 2021 19:51:10 -0700 Subject: [PATCH] Add support for dates for increment/decrement --- Cargo.lock | 1 + helix-core/Cargo.toml | 2 + helix-core/src/date.rs | 217 +++++++++++++++++++++++++++++++++++++ helix-core/src/lib.rs | 1 + helix-term/src/commands.rs | 28 +++-- 5 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 helix-core/src/date.rs diff --git a/Cargo.lock b/Cargo.lock index 5de6e6103..47a6c01e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,7 @@ name = "helix-core" version = "0.5.0" dependencies = [ "arc-swap", + "chrono", "etcetera", "helix-syntax", "log", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index ea695d34a..0a2a56d9e 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -36,5 +36,7 @@ similar = "2.1" etcetera = "0.3" +chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } + [dev-dependencies] quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/date.rs b/helix-core/src/date.rs new file mode 100644 index 000000000..1332670d1 --- /dev/null +++ b/helix-core/src/date.rs @@ -0,0 +1,217 @@ +use chrono::{Duration, NaiveDate}; + +use std::borrow::Cow; + +use ropey::RopeSlice; + +use crate::{ + textobject::{textobject_word, TextObject}, + Range, Tendril, +}; + +// Only support formats that aren't region specific. +static FORMATS: &[&str] = &["%Y-%m-%d", "%Y/%m/%d"]; + +// We don't want to parse ambiguous dates like 10/11/12 or 7/8/10. +// They must be YYYY-mm-dd or YYYY/mm/dd. +// So 2021-01-05 works, but 2021-1-5 doesn't. +const DATE_LENGTH: usize = 10; + +#[derive(Debug, PartialEq, Eq)] +pub struct DateIncrementor { + pub date: NaiveDate, + pub range: Range, + pub format: &'static str, +} + +impl DateIncrementor { + pub fn from_range(text: RopeSlice, range: Range) -> Option { + // Don't increment if the cursor is one right of the date text. + if text.char(range.from()).is_whitespace() { + return None; + } + + let range = textobject_word(text, range, TextObject::Inside, 1, true); + let text: Cow = text.slice(range.from()..range.to()).into(); + + let first = text.chars().next()?; + let last = text.chars().next_back()?; + + // Allow date strings in quotes. + let (range, text) = if first == last && (first == '"' || first == '\'') { + ( + Range::new(range.from() + 1, range.to() - 1), + Cow::from(&text[1..text.len() - 1]), + ) + } else { + (range, text) + }; + + if text.len() != DATE_LENGTH { + return None; + } + + FORMATS.iter().find_map(|format| { + NaiveDate::parse_from_str(&text, format) + .ok() + .map(|date| DateIncrementor { + date, + range, + format, + }) + }) + } + + pub fn incremented_text(&self, amount: i64) -> Tendril { + let incremented_date = self.date + Duration::days(amount); + incremented_date.format(self.format).to_string().into() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Rope; + + #[test] + fn test_date_dashes() { + let rope = Rope::from_str("2021-11-15"); + let range = Range::point(0); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + format: "%Y-%m-%d", + }) + ); + } + + #[test] + fn test_date_slashes() { + let rope = Rope::from_str("2021/11/15"); + let range = Range::point(0); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + format: "%Y/%m/%d", + }) + ); + } + + #[test] + fn test_date_surrounded_by_spaces() { + let rope = Rope::from_str(" 2021-11-15 "); + let range = Range::point(10); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(3, 13), + format: "%Y-%m-%d", + }) + ); + } + + #[test] + fn test_date_in_single_quotes() { + let rope = Rope::from_str("date = '2021-11-15'"); + let range = Range::point(10); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(8, 18), + format: "%Y-%m-%d", + }) + ); + } + + #[test] + fn test_date_in_double_quotes() { + let rope = Rope::from_str("date = \"2021-11-15\""); + let range = Range::point(10); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(8, 18), + format: "%Y-%m-%d", + }) + ); + } + + #[test] + fn test_date_cursor_one_right_of_date() { + let rope = Rope::from_str("2021-11-15 "); + let range = Range::point(10); + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_date_cursor_one_left_of_number() { + let rope = Rope::from_str(" 2021-11-15"); + let range = Range::point(0); + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_invalid_dates() { + let tests = [ + "0000-00-00", + "1980-2-21", + "1980-12-1", + "12345", + "2020-02-30", + "1999-12-32", + "19-12-32", + "1-2-3", + "0000/00/00", + "1980/2/21", + "1980/12/1", + "12345", + "2020/02/30", + "1999/12/32", + "19/12/32", + "1/2/3", + ]; + + for invalid in tests { + let rope = Rope::from_str(invalid); + let range = Range::point(0); + + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); + } + } + + #[test] + fn test_increment_dates() { + let tests = [ + ("1980-12-21", 1, "1980-12-22"), + ("1980-12-21", -1, "1980-12-20"), + ("1980-12-21", 100, "1981-03-31"), + ("1980-12-21", -100, "1980-09-12"), + ("1980-12-21", 1000, "1983-09-17"), + ("1980-12-21", -1000, "1978-03-27"), + ("1980/12/21", 1, "1980/12/22"), + ("1980/12/21", -1, "1980/12/20"), + ("1980/12/21", 100, "1981/03/31"), + ("1980/12/21", -100, "1980/09/12"), + ("1980/12/21", 1000, "1983/09/17"), + ("1980/12/21", -1000, "1978/03/27"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 8ef41ef33..b16a716f4 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -1,6 +1,7 @@ pub mod auto_pairs; pub mod chars; pub mod comment; +pub mod date; pub mod diagnostic; pub mod diff; pub mod graphemes; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 99d1432c2..639bbd83a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,5 +1,7 @@ use helix_core::{ - comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, + comment, coords_at_pos, + date::DateIncrementor, + find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, indent, indent::IndentStyle, @@ -5802,13 +5804,23 @@ fn increment_impl(cx: &mut Context, amount: i64) { let text = doc.text(); let changes = selection.ranges().iter().filter_map(|range| { - let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?; - let new_text = incrementor.incremented_text(amount); - Some(( - incrementor.range.from(), - incrementor.range.to(), - Some(new_text), - )) + if let Some(incrementor) = DateIncrementor::from_range(text.slice(..), *range) { + let new_text = incrementor.incremented_text(amount); + Some(( + incrementor.range.from(), + incrementor.range.to(), + Some(new_text), + )) + } else if let Some(incrementor) = NumberIncrementor::from_range(text.slice(..), *range) { + let new_text = incrementor.incremented_text(amount); + Some(( + incrementor.range.from(), + incrementor.range.to(), + Some(new_text), + )) + } else { + None + } }); if changes.clone().count() > 0 {