2023-11-17 05:45:31 +01:00
|
|
|
//! # TextChange
|
2024-09-05 01:45:48 +02:00
|
|
|
//! A high-level representation of a change within a given buffer.
|
2023-11-17 05:45:31 +01:00
|
|
|
|
2024-09-05 01:45:48 +02:00
|
|
|
/// An editor-friendly representation of a text change in a given buffer.
|
2024-09-19 21:32:46 +02:00
|
|
|
///
|
2024-09-05 01:45:48 +02:00
|
|
|
/// It's expressed with a range of characters and a string of content that should replace them,
|
|
|
|
/// allowing representation of any combination of deletions, insertions or replacements.
|
2023-11-17 05:53:38 +01:00
|
|
|
///
|
2024-09-05 01:45:48 +02:00
|
|
|
/// Bulky and large operations will result in a single [`TextChange`] effectively sending the whole
|
|
|
|
/// new buffer, but smaller changes are efficient and easy to create or apply.
|
2023-11-17 05:53:38 +01:00
|
|
|
///
|
2024-09-05 01:45:48 +02:00
|
|
|
/// [`TextChange`] contains an optional `hash` field. This is used for error correction: if
|
|
|
|
/// provided, it should match the hash of the buffer content **after** applying this change.
|
|
|
|
/// Note that the `hash` field will not necessarily be provided every time.
|
2023-11-17 05:53:38 +01:00
|
|
|
///
|
2024-09-05 01:45:48 +02:00
|
|
|
/// ### Examples
|
|
|
|
/// To insert 'a' after 4th character we should send a.
|
|
|
|
/// `TextChange { start: 4, end: 4, content: "a".into(), hash: None }`
|
2023-11-17 05:53:38 +01:00
|
|
|
///
|
2024-09-05 01:45:48 +02:00
|
|
|
/// To delete a the fourth character we should send a.
|
|
|
|
/// `TextChange { start: 3, end: 4, content: "".into(), hash: None }`
|
2023-11-17 05:53:38 +01:00
|
|
|
///
|
2024-09-25 05:46:48 +02:00
|
|
|
/// ```
|
2024-09-21 11:52:46 +02:00
|
|
|
/// let change = codemp::api::TextChange {
|
|
|
|
/// start: 6, end: 11,
|
|
|
|
/// content: "mom".to_string(), hash: None
|
|
|
|
/// };
|
|
|
|
/// let before = "hello world!";
|
|
|
|
/// let after = change.apply(before);
|
|
|
|
/// assert_eq!(after, "hello mom!");
|
|
|
|
/// ```
|
2023-11-17 05:45:31 +01:00
|
|
|
#[derive(Clone, Debug, Default)]
|
2024-08-07 23:06:33 +02:00
|
|
|
#[cfg_attr(feature = "js", napi_derive::napi(object))]
|
2024-09-19 21:32:46 +02:00
|
|
|
#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyo3::pyclass(get_all))]
|
2024-09-11 15:45:35 +02:00
|
|
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
2023-11-17 05:45:31 +01:00
|
|
|
pub struct TextChange {
|
2024-09-05 01:45:48 +02:00
|
|
|
/// Range start of text change, as char indexes in buffer previous state.
|
2024-08-07 23:06:33 +02:00
|
|
|
pub start: u32,
|
2024-09-05 01:45:48 +02:00
|
|
|
/// Range end of text change, as char indexes in buffer previous state.
|
2024-09-16 18:23:19 +02:00
|
|
|
#[cfg_attr(feature = "serialize", serde(alias = "finish"))] // Lua uses `end` as keyword
|
2024-08-07 23:06:33 +02:00
|
|
|
pub end: u32,
|
2024-09-05 01:45:48 +02:00
|
|
|
/// New content of text inside span.
|
2023-11-17 05:45:31 +01:00
|
|
|
pub content: String,
|
2024-09-05 01:45:48 +02:00
|
|
|
/// Optional content hash after applying this change.
|
2024-08-14 00:24:32 +02:00
|
|
|
pub hash: Option<i64>,
|
2023-11-17 05:45:31 +01:00
|
|
|
}
|
|
|
|
|
2024-08-13 00:36:09 +02:00
|
|
|
impl TextChange {
|
2024-09-05 01:45:48 +02:00
|
|
|
/// Returns the [`std::ops::Range`] representing this change's span.
|
2024-08-13 00:36:09 +02:00
|
|
|
pub fn span(&self) -> std::ops::Range<usize> {
|
|
|
|
self.start as usize..self.end as usize
|
|
|
|
}
|
2024-08-16 12:58:43 +02:00
|
|
|
}
|
2024-08-13 00:36:09 +02:00
|
|
|
|
2024-09-19 21:32:46 +02:00
|
|
|
#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyo3::pymethods)]
|
2024-08-16 12:58:43 +02:00
|
|
|
impl TextChange {
|
2024-09-05 01:45:48 +02:00
|
|
|
/// Returns true if this [`TextChange`] deletes existing text.
|
|
|
|
///
|
|
|
|
/// Note that this is is **not** mutually exclusive with [TextChange::is_insert].
|
2024-08-13 00:36:09 +02:00
|
|
|
pub fn is_delete(&self) -> bool {
|
|
|
|
self.start < self.end
|
|
|
|
}
|
|
|
|
|
2024-09-05 01:45:48 +02:00
|
|
|
/// Returns true if this [`TextChange`] adds new text.
|
|
|
|
///
|
|
|
|
/// Note that this is is **not** mutually exclusive with [TextChange::is_delete].
|
2024-08-13 00:36:09 +02:00
|
|
|
pub fn is_insert(&self) -> bool {
|
|
|
|
!self.content.is_empty()
|
|
|
|
}
|
|
|
|
|
2024-09-05 01:45:48 +02:00
|
|
|
/// Returns true if this [`TextChange`] is effectively as no-op.
|
2024-08-13 00:36:09 +02:00
|
|
|
pub fn is_empty(&self) -> bool {
|
|
|
|
!self.is_delete() && !self.is_insert()
|
|
|
|
}
|
2024-08-16 12:58:43 +02:00
|
|
|
|
2024-09-05 01:45:48 +02:00
|
|
|
/// Applies this text change to given text, returning a new string.
|
2023-11-30 00:38:24 +01:00
|
|
|
pub fn apply(&self, txt: &str) -> String {
|
2024-08-16 12:58:43 +02:00
|
|
|
let pre_index = std::cmp::min(self.start as usize, txt.len());
|
2023-11-30 03:02:13 +01:00
|
|
|
let pre = txt.get(..pre_index).unwrap_or("").to_string();
|
2024-08-16 12:58:43 +02:00
|
|
|
let post = txt.get(self.end as usize..).unwrap_or("").to_string();
|
2023-11-30 00:38:24 +01:00
|
|
|
format!("{}{}{}", pre, self.content, post)
|
|
|
|
}
|
2023-11-17 05:45:31 +01:00
|
|
|
}
|
2023-11-30 00:37:57 +01:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2023-11-30 00:38:24 +01:00
|
|
|
#[test]
|
|
|
|
fn textchange_apply_works_for_insertions() {
|
2024-08-05 22:44:46 +02:00
|
|
|
let change = super::TextChange {
|
2024-08-08 23:58:45 +02:00
|
|
|
start: 5,
|
|
|
|
end: 5,
|
2024-08-05 22:44:46 +02:00
|
|
|
content: " cruel".to_string(),
|
2024-08-16 12:58:43 +02:00
|
|
|
hash: None,
|
2024-08-05 22:44:46 +02:00
|
|
|
};
|
2023-11-30 00:38:24 +01:00
|
|
|
let result = change.apply("hello world!");
|
|
|
|
assert_eq!(result, "hello cruel world!");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn textchange_apply_works_for_deletions() {
|
2024-08-05 22:44:46 +02:00
|
|
|
let change = super::TextChange {
|
2024-08-08 23:58:45 +02:00
|
|
|
start: 5,
|
|
|
|
end: 11,
|
2024-08-05 22:44:46 +02:00
|
|
|
content: "".to_string(),
|
2024-08-16 12:58:43 +02:00
|
|
|
hash: None,
|
2024-08-05 22:44:46 +02:00
|
|
|
};
|
2023-11-30 00:38:24 +01:00
|
|
|
let result = change.apply("hello cruel world!");
|
|
|
|
assert_eq!(result, "hello world!");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn textchange_apply_works_for_replacements() {
|
2024-08-05 22:44:46 +02:00
|
|
|
let change = super::TextChange {
|
2024-08-08 23:58:45 +02:00
|
|
|
start: 5,
|
|
|
|
end: 11,
|
2024-08-05 22:44:46 +02:00
|
|
|
content: " not very pleasant".to_string(),
|
2024-08-16 12:58:43 +02:00
|
|
|
hash: None,
|
2024-08-05 22:44:46 +02:00
|
|
|
};
|
2023-11-30 00:38:24 +01:00
|
|
|
let result = change.apply("hello cruel world!");
|
|
|
|
assert_eq!(result, "hello not very pleasant world!");
|
|
|
|
}
|
2023-11-30 03:01:59 +01:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn textchange_apply_never_panics() {
|
2024-08-05 22:44:46 +02:00
|
|
|
let change = super::TextChange {
|
2024-08-08 23:58:45 +02:00
|
|
|
start: 100,
|
|
|
|
end: 110,
|
2024-08-05 22:44:46 +02:00
|
|
|
content: "a very long string \n which totally matters".to_string(),
|
2024-08-16 12:58:43 +02:00
|
|
|
hash: None,
|
2024-08-05 22:44:46 +02:00
|
|
|
};
|
2023-11-30 03:01:59 +01:00
|
|
|
let result = change.apply("a short text");
|
2024-08-05 22:44:46 +02:00
|
|
|
assert_eq!(
|
|
|
|
result,
|
|
|
|
"a short texta very long string \n which totally matters"
|
|
|
|
);
|
2023-11-30 03:01:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn empty_textchange_doesnt_alter_buffer() {
|
2024-08-05 22:44:46 +02:00
|
|
|
let change = super::TextChange {
|
2024-08-08 23:58:45 +02:00
|
|
|
start: 42,
|
|
|
|
end: 42,
|
2024-08-05 22:44:46 +02:00
|
|
|
content: "".to_string(),
|
2024-08-16 12:58:43 +02:00
|
|
|
hash: None,
|
2024-08-05 22:44:46 +02:00
|
|
|
};
|
2023-11-30 03:01:59 +01:00
|
|
|
let result = change.apply("some important text");
|
|
|
|
assert_eq!(result, "some important text");
|
|
|
|
}
|
2023-11-30 00:37:57 +01:00
|
|
|
}
|