2023-11-17 05:45:31 +01:00
|
|
|
//! # TextChange
|
|
|
|
//!
|
2023-11-17 05:53:38 +01:00
|
|
|
//! an editor-friendly representation of a text change in a buffer
|
|
|
|
//! to easily interface with codemp from various editors
|
2023-11-17 05:45:31 +01:00
|
|
|
|
2024-08-05 19:15:30 +02:00
|
|
|
/// an atomic and orderable operation
|
|
|
|
///
|
|
|
|
/// this under the hood thinly wraps our CRDT operation
|
|
|
|
#[derive(Debug, Clone)]
|
2024-08-13 00:36:09 +02:00
|
|
|
pub struct Op(pub(crate) diamond_types::list::operation::Operation);
|
|
|
|
// Do we need this in the api? why not just have a TextChange, which already covers as operation.
|
2024-01-25 02:13:45 +01:00
|
|
|
|
2023-11-17 05:45:31 +01:00
|
|
|
/// an editor-friendly representation of a text change in a buffer
|
2023-11-17 05:53:38 +01:00
|
|
|
///
|
|
|
|
/// this represent a range in the previous state of the string and a new content which should be
|
|
|
|
/// replaced to it, allowing to represent any combination of deletions, insertions or replacements
|
|
|
|
///
|
|
|
|
/// bulk and widespread operations will result in a TextChange effectively sending the whole new
|
2024-08-05 22:44:46 +02:00
|
|
|
/// buffer, but small changes are efficient and easy to create or apply
|
2023-11-17 05:53:38 +01:00
|
|
|
///
|
|
|
|
/// ### examples
|
|
|
|
/// to insert 'a' after 4th character we should send a
|
|
|
|
/// `TextChange { span: 4..4, content: "a".into() }`
|
|
|
|
///
|
|
|
|
/// to delete a the fourth character we should send a
|
|
|
|
/// `TextChange { span: 3..4, content: "".into() }`
|
|
|
|
///
|
2024-08-07 23:06:33 +02:00
|
|
|
|
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-08-08 23:58:45 +02:00
|
|
|
#[cfg_attr(feature = "python", pyo3::pyclass)]
|
|
|
|
#[cfg_attr(feature = "python", pyo3(get_all))]
|
2023-11-17 05:45:31 +01:00
|
|
|
pub struct TextChange {
|
2024-08-07 23:06:33 +02:00
|
|
|
/// range start of text change, as char indexes in buffer previous state
|
|
|
|
pub start: u32,
|
|
|
|
/// range end of text change, as char indexes in buffer previous state
|
|
|
|
pub end: u32,
|
2023-11-17 05:45:31 +01:00
|
|
|
/// new content of text inside span
|
|
|
|
pub content: String,
|
|
|
|
}
|
|
|
|
|
2024-08-13 00:36:09 +02:00
|
|
|
impl TextChange {
|
|
|
|
pub fn span(&self) -> std::ops::Range<usize> {
|
|
|
|
self.start as usize..self.end as usize
|
|
|
|
}
|
|
|
|
|
|
|
|
/// returns true if this TextChange deletes existing text
|
|
|
|
pub fn is_delete(&self) -> bool {
|
|
|
|
self.start < self.end
|
|
|
|
}
|
|
|
|
|
|
|
|
/// returns true if this TextChange adds new text
|
|
|
|
pub fn is_insert(&self) -> bool {
|
|
|
|
!self.content.is_empty()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// returns true if this TextChange is effectively as no-op
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
|
|
!self.is_delete() && !self.is_insert()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
2023-11-17 05:45:31 +01:00
|
|
|
impl TextChange {
|
|
|
|
/// create a new TextChange from the difference of given strings
|
|
|
|
pub fn from_diff(before: &str, after: &str) -> TextChange {
|
|
|
|
let diff = similar::TextDiff::from_chars(before, after);
|
|
|
|
let mut start = 0;
|
|
|
|
let mut end = 0;
|
|
|
|
let mut from_beginning = true;
|
|
|
|
for op in diff.ops() {
|
|
|
|
match op {
|
2023-11-30 00:37:57 +01:00
|
|
|
similar::DiffOp::Equal { len, .. } => {
|
2023-11-17 05:45:31 +01:00
|
|
|
if from_beginning {
|
2023-11-30 00:37:57 +01:00
|
|
|
start += len
|
2023-11-17 05:45:31 +01:00
|
|
|
} else {
|
2023-11-30 00:37:57 +01:00
|
|
|
end += len
|
2023-11-17 05:45:31 +01:00
|
|
|
}
|
2024-08-05 22:44:46 +02:00
|
|
|
}
|
2023-11-17 05:45:31 +01:00
|
|
|
_ => {
|
|
|
|
end = 0;
|
|
|
|
from_beginning = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let end_before = before.len() - end;
|
|
|
|
let end_after = after.len() - end;
|
|
|
|
|
|
|
|
TextChange {
|
2024-08-07 23:06:33 +02:00
|
|
|
start: start as u32,
|
|
|
|
end: end_before as u32,
|
2023-11-17 05:45:31 +01:00
|
|
|
content: after[start..end_after].to_string(),
|
|
|
|
}
|
|
|
|
}
|
2023-11-24 11:04:42 +01:00
|
|
|
|
2024-08-07 23:06:33 +02:00
|
|
|
pub fn span(&self) -> std::ops::Range<usize> {
|
2024-08-08 23:58:45 +02:00
|
|
|
self.start as usize..self.end as usize
|
2024-08-07 23:06:33 +02:00
|
|
|
}
|
|
|
|
|
2024-08-05 19:15:30 +02:00
|
|
|
/// consume the [TextChange], transforming it into a Vec of [Op]
|
2024-08-08 23:58:45 +02:00
|
|
|
pub fn transform(&self, woot: &Woot) -> WootResult<Vec<Op>> {
|
2024-02-09 00:35:08 +01:00
|
|
|
let mut out = Vec::new();
|
2024-08-05 22:44:46 +02:00
|
|
|
if self.is_empty() {
|
|
|
|
return Ok(out);
|
|
|
|
} // no-op
|
2024-02-09 00:35:08 +01:00
|
|
|
let view = woot.view();
|
2024-08-07 23:06:33 +02:00
|
|
|
let Some(span) = view.get(self.span()) else {
|
2024-02-09 00:35:08 +01:00
|
|
|
return Err(crate::woot::WootError::OutOfBounds);
|
|
|
|
};
|
|
|
|
let diff = similar::TextDiff::from_chars(span, &self.content);
|
|
|
|
for (i, diff) in diff.iter_all_changes().enumerate() {
|
|
|
|
match diff.tag() {
|
2024-08-05 22:44:46 +02:00
|
|
|
similar::ChangeTag::Equal => {}
|
2024-08-07 23:06:33 +02:00
|
|
|
similar::ChangeTag::Delete => match woot.delete_one(self.span().start + i) {
|
2024-02-09 00:35:08 +01:00
|
|
|
Err(e) => tracing::error!("could not create deletion: {}", e),
|
2024-08-05 19:15:30 +02:00
|
|
|
Ok(op) => out.push(Op(op)),
|
2024-02-09 00:35:08 +01:00
|
|
|
},
|
|
|
|
similar::ChangeTag::Insert => {
|
2024-08-07 23:06:33 +02:00
|
|
|
match woot.insert(self.span().start + i, diff.value()) {
|
2024-08-05 22:44:46 +02:00
|
|
|
Ok(ops) => {
|
|
|
|
for op in ops {
|
|
|
|
out.push(Op(op))
|
|
|
|
}
|
|
|
|
}
|
2024-02-09 00:35:08 +01:00
|
|
|
Err(e) => tracing::error!("could not create insertion: {}", e),
|
|
|
|
}
|
2024-08-05 22:44:46 +02:00
|
|
|
}
|
2024-02-09 00:35:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(out)
|
|
|
|
}
|
|
|
|
|
2023-11-30 00:38:24 +01:00
|
|
|
/// returns true if this TextChange deletes existing text
|
|
|
|
pub fn is_deletion(&self) -> bool {
|
2024-08-07 23:06:33 +02:00
|
|
|
!self.span().is_empty()
|
2023-11-30 00:38:24 +01:00
|
|
|
}
|
|
|
|
|
2023-11-30 03:41:53 +01:00
|
|
|
/// returns true if this TextChange adds new text
|
2023-11-30 00:38:24 +01:00
|
|
|
pub fn is_addition(&self) -> bool {
|
|
|
|
!self.content.is_empty()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// returns true if this TextChange is effectively as no-op
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
|
|
!self.is_deletion() && !self.is_addition()
|
|
|
|
}
|
|
|
|
|
2023-11-30 03:41:53 +01: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-07 23:06:33 +02:00
|
|
|
let pre_index = std::cmp::min(self.span().start, txt.len());
|
2023-11-30 03:02:13 +01:00
|
|
|
let pre = txt.get(..pre_index).unwrap_or("").to_string();
|
2024-08-07 23:06:33 +02:00
|
|
|
let post = txt.get(self.span().end..).unwrap_or("").to_string();
|
2023-11-30 00:38:24 +01:00
|
|
|
format!("{}{}{}", pre, self.content, post)
|
|
|
|
}
|
|
|
|
|
2023-11-24 11:04:42 +01:00
|
|
|
/// convert from byte index to row and column
|
|
|
|
/// txt must be the whole content of the buffer, in order to count lines
|
2024-03-09 19:59:36 +01:00
|
|
|
pub fn index_to_rowcol(txt: &str, index: usize) -> codemp_proto::cursor::RowCol {
|
2023-11-30 03:02:13 +01:00
|
|
|
// FIXME might panic, use .get()
|
2023-11-24 11:04:42 +01:00
|
|
|
let row = txt[..index].matches('\n').count() as i32;
|
|
|
|
let col = txt[..index].split('\n').last().unwrap_or("").len() as i32;
|
2024-03-09 19:59:36 +01:00
|
|
|
codemp_proto::cursor::RowCol { row, col }
|
2023-11-24 11:04:42 +01:00
|
|
|
}
|
2023-11-17 05:45:31 +01:00
|
|
|
}
|
2023-11-30 00:37:57 +01:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
#[test]
|
|
|
|
fn textchange_diff_works_for_deletions() {
|
|
|
|
let change = super::TextChange::from_diff(
|
|
|
|
"sphinx of black quartz, judge my vow",
|
2024-08-05 22:44:46 +02:00
|
|
|
"sphinx of quartz, judge my vow",
|
2023-11-30 00:37:57 +01:00
|
|
|
);
|
2024-08-07 23:06:33 +02:00
|
|
|
assert_eq!(change.span(), 10..16);
|
2023-11-30 00:37:57 +01:00
|
|
|
assert_eq!(change.content, "");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn textchange_diff_works_for_insertions() {
|
|
|
|
let change = super::TextChange::from_diff(
|
|
|
|
"sphinx of quartz, judge my vow",
|
2024-08-05 22:44:46 +02:00
|
|
|
"sphinx of black quartz, judge my vow",
|
2023-11-30 00:37:57 +01:00
|
|
|
);
|
2024-08-07 23:06:33 +02:00
|
|
|
assert_eq!(change.span(), 10..10);
|
2023-11-30 00:37:57 +01:00
|
|
|
assert_eq!(change.content, "black ");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn textchange_diff_works_for_changes() {
|
|
|
|
let change = super::TextChange::from_diff(
|
|
|
|
"sphinx of black quartz, judge my vow",
|
2024-08-05 22:44:46 +02:00
|
|
|
"sphinx who watches the desert, judge my vow",
|
2023-11-30 00:37:57 +01:00
|
|
|
);
|
2024-08-07 23:06:33 +02:00
|
|
|
assert_eq!(change.span(), 7..22);
|
2023-11-30 00:37:57 +01:00
|
|
|
assert_eq!(change.content, "who watches the desert");
|
|
|
|
}
|
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(),
|
|
|
|
};
|
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(),
|
|
|
|
};
|
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(),
|
|
|
|
};
|
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(),
|
|
|
|
};
|
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_diff_produces_empty_textchange() {
|
|
|
|
let change = super::TextChange::from_diff("same \n\n text", "same \n\n text");
|
|
|
|
assert!(change.is_empty());
|
|
|
|
}
|
2024-08-05 22:44:46 +02:00
|
|
|
|
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(),
|
|
|
|
};
|
2023-11-30 03:01:59 +01:00
|
|
|
let result = change.apply("some important text");
|
|
|
|
assert_eq!(result, "some important text");
|
|
|
|
}
|
2024-08-13 00:36:09 +02:00
|
|
|
}*/
|
|
|
|
|
|
|
|
// TODO: properly implement this for diamond types directly
|
|
|
|
impl From<Op> for TextChange {
|
|
|
|
fn from(value: Op) -> Self {
|
|
|
|
Self {
|
|
|
|
start: value.0.start() as u32,
|
|
|
|
end: value.0.end() as u32,
|
|
|
|
content: value.0.content_as_str().unwrap_or_default().to_string(),
|
|
|
|
}
|
|
|
|
}
|
2023-11-30 00:37:57 +01:00
|
|
|
}
|