on master help sign in
Add support for dates for increment/decrement
commitc1f6167e37909517676c30b0a80203739e8492a5 authorJason Rodney Hansen <jasonrodneyhansen@gmail.com> dateMon 15 Nov 2021, 19:51 (5 years ago) parent11a2f9a
5 files changed+241−8
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+20−8
Cargo.lock+1
@@ -369,6 +369,7 @@ name = "helix-core"
369369version = "0.5.0"
370370dependencies = [
371371 "arc-swap",
372+ "chrono",
372373 "etcetera",
373374 "helix-syntax",
374375 "log",
helix-core/Cargo.toml+2
@@ -36,5 +36,7 @@ similar = "2.1"
3636
3737etcetera = "0.3"
3838
39+chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
40+
3941[dev-dependencies]
4042quickcheck = { version = "1", default-features = false }
helix-core/src/date.rsnew file+217
@@ -0,0 +1,217 @@
1+use chrono::{Duration, NaiveDate};
2+
3+use std::borrow::Cow;
4+
5+use ropey::RopeSlice;
6+
7+use crate::{
8+ textobject::{textobject_word, TextObject},
9+ Range, Tendril,
10+};
11+
12+// Only support formats that aren't region specific.
13+static FORMATS: &[&str] = &["%Y-%m-%d", "%Y/%m/%d"];
14+
15+// We don't want to parse ambiguous dates like 10/11/12 or 7/8/10.
16+// They must be YYYY-mm-dd or YYYY/mm/dd.
17+// So 2021-01-05 works, but 2021-1-5 doesn't.
18+const DATE_LENGTH: usize = 10;
19+
20+#[derive(Debug, PartialEq, Eq)]
21+pub struct DateIncrementor {
22+ pub date: NaiveDate,
23+ pub range: Range,
24+ pub format: &'static str,
25+}
26+
27+impl DateIncrementor {
28+ pub fn from_range(text: RopeSlice, range: Range) -> Option<DateIncrementor> {
29+ // Don't increment if the cursor is one right of the date text.
30+ if text.char(range.from()).is_whitespace() {
31+ return None;
32+ }
33+
34+ let range = textobject_word(text, range, TextObject::Inside, 1, true);
35+ let text: Cow<str> = text.slice(range.from()..range.to()).into();
36+
37+ let first = text.chars().next()?;
38+ let last = text.chars().next_back()?;
39+
40+ // Allow date strings in quotes.
41+ let (range, text) = if first == last && (first == '"' || first == '\'') {
42+ (
43+ Range::new(range.from() + 1, range.to() - 1),
44+ Cow::from(&text[1..text.len() - 1]),
45+ )
46+ } else {
47+ (range, text)
48+ };
49+
50+ if text.len() != DATE_LENGTH {
51+ return None;
52+ }
53+
54+ FORMATS.iter().find_map(|format| {
55+ NaiveDate::parse_from_str(&text, format)
56+ .ok()
57+ .map(|date| DateIncrementor {
58+ date,
59+ range,
60+ format,
61+ })
62+ })
63+ }
64+
65+ pub fn incremented_text(&self, amount: i64) -> Tendril {
66+ let incremented_date = self.date + Duration::days(amount);
67+ incremented_date.format(self.format).to_string().into()
68+ }
69+}
70+
71+#[cfg(test)]
72+mod test {
73+ use super::*;
74+ use crate::Rope;
75+
76+ #[test]
77+ fn test_date_dashes() {
78+ let rope = Rope::from_str("2021-11-15");
79+ let range = Range::point(0);
80+ assert_eq!(
81+ DateIncrementor::from_range(rope.slice(..), range),
82+ Some(DateIncrementor {
83+ date: NaiveDate::from_ymd(2021, 11, 15),
84+ range: Range::new(0, 10),
85+ format: "%Y-%m-%d",
86+ })
87+ );
88+ }
89+
90+ #[test]
91+ fn test_date_slashes() {
92+ let rope = Rope::from_str("2021/11/15");
93+ let range = Range::point(0);
94+ assert_eq!(
95+ DateIncrementor::from_range(rope.slice(..), range),
96+ Some(DateIncrementor {
97+ date: NaiveDate::from_ymd(2021, 11, 15),
98+ range: Range::new(0, 10),
99+ format: "%Y/%m/%d",
100+ })
101+ );
102+ }
103+
104+ #[test]
105+ fn test_date_surrounded_by_spaces() {
106+ let rope = Rope::from_str(" 2021-11-15 ");
107+ let range = Range::point(10);
108+ assert_eq!(
109+ DateIncrementor::from_range(rope.slice(..), range),
110+ Some(DateIncrementor {
111+ date: NaiveDate::from_ymd(2021, 11, 15),
112+ range: Range::new(3, 13),
113+ format: "%Y-%m-%d",
114+ })
115+ );
116+ }
117+
118+ #[test]
119+ fn test_date_in_single_quotes() {
120+ let rope = Rope::from_str("date = '2021-11-15'");
121+ let range = Range::point(10);
122+ assert_eq!(
123+ DateIncrementor::from_range(rope.slice(..), range),
124+ Some(DateIncrementor {
125+ date: NaiveDate::from_ymd(2021, 11, 15),
126+ range: Range::new(8, 18),
127+ format: "%Y-%m-%d",
128+ })
129+ );
130+ }
131+
132+ #[test]
133+ fn test_date_in_double_quotes() {
134+ let rope = Rope::from_str("date = \"2021-11-15\"");
135+ let range = Range::point(10);
136+ assert_eq!(
137+ DateIncrementor::from_range(rope.slice(..), range),
138+ Some(DateIncrementor {
139+ date: NaiveDate::from_ymd(2021, 11, 15),
140+ range: Range::new(8, 18),
141+ format: "%Y-%m-%d",
142+ })
143+ );
144+ }
145+
146+ #[test]
147+ fn test_date_cursor_one_right_of_date() {
148+ let rope = Rope::from_str("2021-11-15 ");
149+ let range = Range::point(10);
150+ assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None);
151+ }
152+
153+ #[test]
154+ fn test_date_cursor_one_left_of_number() {
155+ let rope = Rope::from_str(" 2021-11-15");
156+ let range = Range::point(0);
157+ assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None);
158+ }
159+
160+ #[test]
161+ fn test_invalid_dates() {
162+ let tests = [
163+ "0000-00-00",
164+ "1980-2-21",
165+ "1980-12-1",
166+ "12345",
167+ "2020-02-30",
168+ "1999-12-32",
169+ "19-12-32",
170+ "1-2-3",
171+ "0000/00/00",
172+ "1980/2/21",
173+ "1980/12/1",
174+ "12345",
175+ "2020/02/30",
176+ "1999/12/32",
177+ "19/12/32",
178+ "1/2/3",
179+ ];
180+
181+ for invalid in tests {
182+ let rope = Rope::from_str(invalid);
183+ let range = Range::point(0);
184+
185+ assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None);
186+ }
187+ }
188+
189+ #[test]
190+ fn test_increment_dates() {
191+ let tests = [
192+ ("1980-12-21", 1, "1980-12-22"),
193+ ("1980-12-21", -1, "1980-12-20"),
194+ ("1980-12-21", 100, "1981-03-31"),
195+ ("1980-12-21", -100, "1980-09-12"),
196+ ("1980-12-21", 1000, "1983-09-17"),
197+ ("1980-12-21", -1000, "1978-03-27"),
198+ ("1980/12/21", 1, "1980/12/22"),
199+ ("1980/12/21", -1, "1980/12/20"),
200+ ("1980/12/21", 100, "1981/03/31"),
201+ ("1980/12/21", -100, "1980/09/12"),
202+ ("1980/12/21", 1000, "1983/09/17"),
203+ ("1980/12/21", -1000, "1978/03/27"),
204+ ];
205+
206+ for (original, amount, expected) in tests {
207+ let rope = Rope::from_str(original);
208+ let range = Range::point(0);
209+ assert_eq!(
210+ DateIncrementor::from_range(rope.slice(..), range)
211+ .unwrap()
212+ .incremented_text(amount),
213+ expected.into()
214+ );
215+ }
216+ }
217+}
helix-core/src/lib.rs+1
@@ -1,6 +1,7 @@
11pub mod auto_pairs;
22pub mod chars;
33pub mod comment;
4+pub mod date;
45pub mod diagnostic;
56pub mod diff;
67pub mod graphemes;
helix-term/src/commands.rs+20−8
@@ -1,5 +1,7 @@
11use helix_core::{
2− comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
2+ comment, coords_at_pos,
3+ date::DateIncrementor,
4+ find_first_non_whitespace_char, find_root, graphemes,
35 history::UndoKind,
46 indent,
57 indent::IndentStyle,
@@ -5802,13 +5804,23 @@ fn increment_impl(cx: &mut Context, amount: i64) {
58025804 let text = doc.text();
58035805
58045806 let changes = selection.ranges().iter().filter_map(|range| {
5805− let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?;
5806− let new_text = incrementor.incremented_text(amount);
5807− Some((
5808− incrementor.range.from(),
5809− incrementor.range.to(),
5810− Some(new_text),
5811− ))
5807+ if let Some(incrementor) = DateIncrementor::from_range(text.slice(..), *range) {
5808+ let new_text = incrementor.incremented_text(amount);
5809+ Some((
5810+ incrementor.range.from(),
5811+ incrementor.range.to(),
5812+ Some(new_text),
5813+ ))
5814+ } else if let Some(incrementor) = NumberIncrementor::from_range(text.slice(..), *range) {
5815+ let new_text = incrementor.incremented_text(amount);
5816+ Some((
5817+ incrementor.range.from(),
5818+ incrementor.range.to(),
5819+ Some(new_text),
5820+ ))
5821+ } else {
5822+ None
5823+ }
58125824 });
58135825
58145826 if changes.clone().count() > 0 {