1
use crate::Position;
2
use std::collections::VecDeque;
3
use std::time::{Duration, Instant};
4
use unicode_segmentation::UnicodeSegmentation;
5

            
6
const TIME_AFTER_WHICH_OPERATION_COMMITS: u64 = 1; // in seconds
7
const HISTORY_SIZE: usize = 8;
8

            
9
2
#[derive(Debug, PartialEq, Clone, Copy)]
10
pub enum OperationType {
11
    Insert,
12
    Delete,
13
}
14

            
15
impl OperationType {
16
5
    fn reversed(self) -> Self {
17
5
        match self {
18
2
            OperationType::Insert => OperationType::Delete,
19
3
            OperationType::Delete => OperationType::Insert,
20
        }
21
5
    }
22
}
23
/// An Operation describe a text edition at a specific start position.
24
///
25
/// An Operation can be of 2 types: either Insert or Delete, testifying of the fact
26
/// that we either inserted or deleted the provided text content, starting at a given
27
/// x/y position.
28
4
#[derive(Debug, PartialEq)]
29
pub struct Operation {
30
6
    pub content: String,
31
4
    pub start_position: Position,
32
6
    pub op_type: OperationType,
33
}
34

            
35
impl Operation {
36
    /// Append the argument string to the Operation content
37
31
    fn mut_push(&mut self, text: &str) {
38
31
        self.content.push_str(text);
39
31
    }
40

            
41
    /// Return the position the cursor would be at the end of the Operation.
42
    ///
43
    /// Examples:
44
    /// - an insert of "rust" starting at {0, 0} would return an end position of {3, 0}
45
    /// - an insert of "rust\nrocks" starting at {0, 0} would return an end position of {4, 1}
46
    /// - a deletion of "tsur" starting at {3, 0} would return an end position of {0, 0}
47
    /// - a deletion of "skcor\ntsur" starting at {1, 4} would return an end position of {0, 0}
48
    ///
49
    /// Note: this one took a long time to get right but seems to work. Check unit tests in doubt!
50
    #[must_use]
51
23
    pub fn end_position(&self, document_rows_length: &[usize]) -> Position {
52
23
        let mut x: usize = self.start_position.x;
53
23
        let mut y: usize = self.start_position.y;
54
46
        match self.op_type {
55
            OperationType::Insert => {
56
146
                for grapheme in self.content.graphemes(true) {
57
266
                    if grapheme == "\n" {
58
13
                        x = 0;
59
13
                        y += 1;
60
                    } else {
61
120
                        x += 1;
62
                    }
63
                }
64
13
                Position {
65
13
                    x: x.saturating_sub(1),
66
13
                    y,
67
13
                }
68
            }
69
            OperationType::Delete => {
70
114
                for grapheme in self.content.graphemes(true) {
71
312
                    if grapheme == "\n" {
72
7
                        y = y.saturating_sub(1);
73
7
                        x = *document_rows_length.get(y).unwrap_or(&0);
74
                    } else {
75
97
                        x = x.saturating_sub(1);
76
                    }
77
                }
78
10
                Position { x, y }
79
            }
80
        }
81
23
    }
82

            
83
    #[must_use]
84
5
    pub fn reversed(&self, document_rows_length: &[usize]) -> Self {
85
5
        Self {
86
5
            content: self.content.graphemes(true).rev().collect(),
87
5
            op_type: self.op_type.reversed(),
88
5
            start_position: self.end_position(document_rows_length),
89
        }
90
5
    }
91
}
92

            
93
/// History is a bounded double-ended ``Vec`` of ``Operations``. Every-time a new change is
94
/// registered, it is added to the history. If the time elapsed since the last change is
95
/// greater than ``TIME_AFTER_WHICH_OPERATION_COMMITS``, a new operation is added to the
96
/// ``operations`` ``VecDeque``. If not, the content of the back (last) operation is
97
/// mutated in place.
98
#[derive(Debug)]
99
pub struct History {
100
    pub operations: VecDeque<Operation>,
101
    pub last_edit_time: Instant,
102
}
103

            
104
impl Default for History {
105
50
    fn default() -> Self {
106
50
        Self {
107
50
            operations: VecDeque::with_capacity(HISTORY_SIZE),
108
50
            last_edit_time: Instant::now(),
109
        }
110
50
    }
111
}
112

            
113
impl History {
114
50
    fn set_last_edit_time_to_now(&mut self) {
115
50
        self.last_edit_time = Instant::now();
116
50
    }
117

            
118
19
    fn push(&mut self, text: &str, position: Position, operation_type: OperationType) {
119
        // maintain the history to its max size, to bound memory usage
120
19
        if self.operations.len() == HISTORY_SIZE {
121
            self.operations.pop_front();
122
        }
123
19
        self.operations.push_back(Operation {
124
19
            content: text.to_string(),
125
            start_position: position,
126
            op_type: operation_type,
127
        });
128
19
        self.set_last_edit_time_to_now();
129
19
    }
130
    /// Push (by either creating a new Operation or mutating the last one) an
131
    /// Insert operation in history based on the provided inserted text and position.
132
11
    fn push_insert(&mut self, text: &str, position: Position) {
133
11
        self.push(text, position, OperationType::Insert);
134
11
    }
135

            
136
    /// Push (by either creating a new Operation or mutating the last one) a
137
    /// Delete operation in history based on the provided deleted text and position.
138
8
    fn push_delete(&mut self, text: &str, position: Position) {
139
8
        self.push(text, position, OperationType::Delete);
140
8
    }
141

            
142
    /// Register that an insertion of provided text occured at the provided position.
143
    ///
144
    /// Either register that as a whole new ``Operation``, or mutate the back ``Operation``
145
    /// depending whether the elapsed time since the last operation is greater than
146
    /// ``TIME_AFTER_WHICH_OPERATION_COMMITS``.
147
    /// However, if the back operation was a Delee, push a new Insert ``Operation``
148
    /// in the history.
149
40
    pub fn register_insertion(&mut self, text: &str, position: Position) {
150
120
        if Instant::elapsed(&self.last_edit_time)
151
40
            >= Duration::new(TIME_AFTER_WHICH_OPERATION_COMMITS, 0)
152
40
            || self.operations.is_empty()
153
        {
154
9
            self.push_insert(text, position);
155
31
        } else if let Some(op) = self.operations.back_mut() {
156
31
            match op.op_type {
157
29
                OperationType::Insert => {
158
29
                    op.mut_push(text);
159
29
                    self.set_last_edit_time_to_now();
160
                }
161
2
                OperationType::Delete => self.push_insert(text, position),
162
            }
163
        }
164
40
    }
165

            
166
    /// Register that a deletion of provided text occured at the provided position.
167
    ///
168
    /// Either register that as a whole new ``Operation``, or mutate the back ``Operation``
169
    /// depending whether the elapsed time since the last operation is greater than
170
    /// ``TIME_AFTER_WHICH_OPERATION_COMMITS``.
171
    ///  However, if the back operation was an Insert, push a new Delete ``Operation``
172
    /// in the history.
173
10
    pub fn register_deletion(&mut self, text: &str, position: Position) {
174
30
        if Instant::elapsed(&self.last_edit_time)
175
10
            >= Duration::new(TIME_AFTER_WHICH_OPERATION_COMMITS, 0)
176
10
            || self.operations.is_empty()
177
        {
178
4
            self.push_delete(text, position);
179
6
        } else if let Some(op) = self.operations.back_mut() {
180
6
            match op.op_type {
181
4
                OperationType::Insert => self.push_delete(text, position),
182
                OperationType::Delete => {
183
2
                    op.mut_push(text);
184
2
                    self.set_last_edit_time_to_now();
185
                }
186
            }
187
        }
188
10
    }
189

            
190
    /// If any Operation is in the history, pop it and returm its reversed Operation.
191
    #[must_use]
192
3
    pub fn last_operation_reversed(&mut self, document_rows_length: &[usize]) -> Option<Operation> {
193
3
        self.operations
194
            .pop_back()
195
6
            .map(|op| op.reversed(document_rows_length))
196
3
    }
197
}
198

            
199
#[cfg(test)]
200
#[path = "./history_test.rs"]
201
mod history_test;