1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
use crate::buffer::operation::Operation;
use crate::buffer::{Buffer, Cursor, GapBuffer, Position};
use std::cell::RefCell;
use std::clone::Clone;
use std::convert::Into;
use std::rc::Rc;
/// A reversible buffer insert operation.
///
/// Inserts the provided content at the specified position. Tracks both, and reverses
/// the operation by calculating the content's start and end positions (range), relative
/// to its inserted location, and removing said range from the underlying buffer.
///
/// If the buffer is configured with a `change_callback`, it will be called with
/// the position of this operation when it is run or reversed.
#[derive(Clone)]
pub struct Replace {
old_content: String,
new_content: String,
}
impl Operation for Replace {
fn run(&mut self, buffer: &mut Buffer) {
replace_content(self.new_content.clone(), buffer);
}
fn reverse(&mut self, buffer: &mut Buffer) {
replace_content(self.old_content.clone(), buffer);
}
fn clone_operation(&self) -> Box<dyn Operation> {
Box::new(self.clone())
}
}
impl Replace {
/// Creates a new empty insert operation.
pub fn new(old_content: String, new_content: String) -> Replace {
Replace {
old_content,
new_content,
}
}
}
impl Buffer {
/// Replaces the buffer's contents with the provided data. This method will
/// make best efforts to retain the full cursor position, then cursor line,
/// and will ultimately fall back to resetting the cursor to its initial
/// (0,0) position if these fail. The buffer's ID, syntax definition, and
/// change callback are always persisted.
///
/// <div class="warning">
/// As this is a reversible operation, both the before and after buffer
/// contents are kept in-memory, which for large buffers may be relatively
/// expensive. To help avoid needless replacements, this method will
/// ignore requests that don't actually change content. Despite this, use
/// this operation judiciously; it is designed for wholesale replacements
/// (e.g. external formatting tools) that cannot be broken down into
/// selective delete/insert operations.
/// </div>
///
/// # Examples
///
/// ```
/// use scribe::buffer::{Buffer, Position};
///
/// let mut buffer = Buffer::new();
/// buffer.insert("scribe\nlibrary\n");
/// buffer.cursor.move_to(Position { line: 1, offset: 1 });
/// buffer.replace("new\ncontent");
///
/// assert_eq!(buffer.data(), "new\ncontent");
/// assert_eq!(*buffer.cursor, Position{ line: 1, offset: 1 });
/// ```
pub fn replace<T: Into<String> + AsRef<str>>(&mut self, content: T) {
let old_content = self.data();
// Ignore replacements that don't change content.
if content.as_ref() == old_content {
return;
}
// Build and run an insert operation.
let mut op = Replace::new(self.data(), content.into());
op.run(self);
// Store the operation in the history object so that it can be undone.
match self.operation_group {
Some(ref mut group) => group.add(Box::new(op)),
None => self.history.add(Box::new(op)),
};
}
}
fn replace_content(content: String, buffer: &mut Buffer) {
// Create a new gap buffer and associated cursor with the new content.
let data = Rc::new(RefCell::new(GapBuffer::new(content)));
let mut cursor = Cursor::new(data.clone(), Position { line: 0, offset: 0 });
// Try to retain cursor position or line of the current gap buffer.
if !cursor.move_to(*buffer.cursor) {
cursor.move_to(Position {
line: buffer.cursor.line,
offset: 0,
});
}
// Do the replacement.
buffer.data = data;
buffer.cursor = cursor;
// Run the change callback, if present.
if let Some(ref callback) = buffer.change_callback {
callback(Position::new())
}
}
#[cfg(test)]
mod tests {
use crate::buffer::position::Position;
use crate::buffer::Buffer;
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
#[test]
fn replace_retains_full_position_when_possible() {
let mut buffer = Buffer::new();
buffer.insert("amp editor");
// Move to a position that will exist after replacing content.
buffer.cursor.move_to(Position { line: 0, offset: 3 });
// Replace the buffer content.
buffer.replace("scribe");
// Verify that the position is retained.
assert_eq!(*buffer.cursor, Position { line: 0, offset: 3 });
}
#[test]
fn replace_retains_position_line_when_possible() {
let mut buffer = Buffer::new();
// Move to a position whose line (but not offset)
// is available in the replaced content.
buffer.insert("amp\neditor");
buffer.cursor.move_to(Position { line: 1, offset: 1 });
// Replace the buffer content.
buffer.replace("scribe\n");
// Verify that the position is set to the start of the same line.
assert_eq!(*buffer.cursor, Position { line: 1, offset: 0 });
}
#[test]
fn replace_discards_position_when_impossible() {
let mut buffer = Buffer::new();
// Move to a position entirely unavailable in the replaced content.
buffer.insert("\namp\neditor");
buffer.cursor.move_to(Position { line: 2, offset: 1 });
// Replace the buffer content.
buffer.replace("scribe\n");
// Verify that the position is discarded.
assert_eq!(*buffer.cursor, Position::new());
}
#[test]
fn replace_calls_change_callback_with_zero_position() {
let mut buffer = Buffer::new();
buffer.insert("amp\neditor");
// Create a non-zero position that we'll share with the callback.
let tracked_position = Rc::new(RefCell::new(Position { line: 1, offset: 1 }));
let callback_position = tracked_position.clone();
// Set up the callback so that it updates the shared position.
buffer.change_callback = Some(Box::new(move |change_position| {
*callback_position.borrow_mut() = change_position
}));
// Replace the buffer content.
buffer.replace("scribe");
// Verify that the callback received the correct position.
assert_eq!(*tracked_position.borrow(), Position::new());
}
#[test]
fn replace_flags_buffer_as_modified() {
let file_path = Path::new("tests/sample/file");
let mut buffer = Buffer::from_file(file_path).unwrap();
// Replace the buffer content.
buffer.replace("scribe");
// Verify that the buffer is seen as modified.
assert!(buffer.modified());
}
#[test]
fn replace_is_reversible() {
let file_path = Path::new("tests/sample/file");
let mut buffer = Buffer::from_file(file_path).unwrap();
// Replace the buffer content and then undo it.
buffer.replace("scribe");
buffer.undo();
// Verify that the original content is restored.
assert_eq!(buffer.data(), "it works!\n");
}
#[test]
fn replace_does_nothing_if_replacement_matches_buffer_contents() {
let file_path = Path::new("tests/sample/file");
let mut buffer = Buffer::from_file(file_path).unwrap();
// Try to replace buffer content with matching content.
buffer.replace("it works!\n");
assert!(!buffer.modified());
assert!(buffer.history.previous().is_none());
}
}