From 198526afaaf32cb246f4db8ac3193700cb0710c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wawrzyniec=20Urba=C5=84czyk?= Date: Sat, 13 Jun 2020 03:53:27 +0200 Subject: [PATCH] Text API changes (https://github.com/enso-org/ide/pull/556) Original commit: https://github.com/enso-org/ide/commit/e41be1d3516fce7a8f658d1096ede53e9fc1ea25 --- gui/src/rust/Cargo.lock | 1 + gui/src/rust/ide/Cargo.toml | 1 + gui/src/rust/ide/ast/impl/src/lib.rs | 31 +- gui/src/rust/ide/src/controller/module.rs | 9 +- .../ide/src/double_representation/text.rs | 342 +++++++++++++++--- gui/src/rust/ide/src/model/module.rs | 4 +- .../rust/ide/src/model/synchronized/module.rs | 3 + gui/src/rust/lib/data/src/text.rs | 134 +++++++ 8 files changed, 465 insertions(+), 60 deletions(-) diff --git a/gui/src/rust/Cargo.lock b/gui/src/rust/Cargo.lock index 2a645559a96..cefe7024349 100644 --- a/gui/src/rust/Cargo.lock +++ b/gui/src/rust/Cargo.lock @@ -1262,6 +1262,7 @@ dependencies = [ "graph-editor 0.1.0", "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", "json-rpc 0.1.0", + "logger 0.1.0", "nalgebra 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)", "parser 0.1.0", "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/gui/src/rust/ide/Cargo.toml b/gui/src/rust/ide/Cargo.toml index a09a2f80ad8..2de818e9d00 100644 --- a/gui/src/rust/ide/Cargo.toml +++ b/gui/src/rust/ide/Cargo.toml @@ -16,6 +16,7 @@ data = { version = "0.1.0" , path = "../lib/data" enso-frp = { version = "0.1.0" , path = "../lib/frp" } enso-prelude = { version = "0.1.0" , path = "../lib/prelude" } graph-editor = { version = "0.1.0" , path = "../lib/graph-editor" } +logger = { version = "0.1.0" , path = "../lib/logger" } shapely = { version = "0.1.0" , path = "../lib/shapely/impl" } ast = { version = "0.1.0" , path = "ast/impl" } diff --git a/gui/src/rust/ide/ast/impl/src/lib.rs b/gui/src/rust/ide/ast/impl/src/lib.rs index d7ba2a12290..2375ec5cf42 100644 --- a/gui/src/rust/ide/ast/impl/src/lib.rs +++ b/gui/src/rust/ide/ast/impl/src/lib.rs @@ -55,6 +55,14 @@ use serde::Serialize; use shapely::*; use uuid::Uuid; +/// A sequence of AST nodes, typically the "token soup". +pub type Stream = Vec; + + + +// ============= +// === IdMap === +// ============= /// A mapping between text position and immutable ID. #[derive(Clone,Debug,Default,Deserialize,Eq,PartialEq,Serialize)] @@ -72,8 +80,6 @@ impl IdMap { } } -/// A sequence of AST nodes, typically the "token soup". -pub type Stream = Vec; @@ -208,9 +214,9 @@ impl<'t> IntoIterator for &'t Ast { } } -impl ToString for Ast { - fn to_string(&self) -> String { - self.repr() +impl Display for Ast { + fn fmt(&self, f:&mut fmt::Formatter<'_>) -> fmt::Result { + write!(f,"{}",self.repr()) } } @@ -279,6 +285,21 @@ impl Ast { pub fn with_shape>>(&self, shape:S) -> Ast { Ast::new(shape.into(),self.id) } + + /// Find a node in the AST with id equal to the given argument. + pub fn find_by_id(&self, id:Id) -> Option<&Ast> { + self.iter_recursive().find(|ast| ast.id == Some(id)) + } + + /// Find a node in the AST with text representation equal to given string. + /// + /// If multiple nodes match, it is unspecified which one of them will be returned. + pub fn find_by_repr(&self, repr:&str) -> Option<&Ast> { + // TODO: [mwu] + // We could do much better with HasTokens and iterative matching. + // Still, no need at this point. + self.iter_recursive().find(|ast| ast.repr() == repr) + } } /// Fills `id` with `None` by default. diff --git a/gui/src/rust/ide/src/controller/module.rs b/gui/src/rust/ide/src/controller/module.rs index 96f040674c1..fc6b795565d 100644 --- a/gui/src/rust/ide/src/controller/module.rs +++ b/gui/src/rust/ide/src/controller/module.rs @@ -80,11 +80,8 @@ impl Handle { /// May return Error when new code causes parsing errors, or when parsed code does not produce /// Module ast. pub fn apply_code_change(&self,change:TextChange) -> FallibleResult<()> { - let mut id_map = self.model.ast().id_map(); - let replaced_size = change.replaced.end - change.replaced.start; - let replaced_span = Span::new(change.replaced.start,replaced_size); - - apply_code_change_to_id_map(&mut id_map,&replaced_span,&change.inserted); + let mut id_map = self.model.ast().id_map(); + apply_code_change_to_id_map(&mut id_map,&change,&self.model.ast().repr()); self.model.apply_code_change(change,&self.parser,id_map) } @@ -197,7 +194,7 @@ mod test { let controller = Handle::new_mock(location,module,id_map,ls,parser).unwrap(); // Change code from "2+2" to "22+2" - let change = TextChange::insert(Index::new(1),"2".to_string()); + let change = TextChange::insert(Index::new(0),"2".to_string()); controller.apply_code_change(change).unwrap(); let expected_ast = Ast::new_no_id(ast::Module { lines: vec![BlockLine { diff --git a/gui/src/rust/ide/src/double_representation/text.rs b/gui/src/rust/ide/src/double_representation/text.rs index e0a9aa1e8e7..5fb9ed132c4 100644 --- a/gui/src/rust/ide/src/double_representation/text.rs +++ b/gui/src/rust/ide/src/double_representation/text.rs @@ -1,5 +1,7 @@ //! A module with functions used to support working with text representation of the language. +use crate::prelude::*; + use ast::IdMap; use data::text::Size; use data::text::Span; @@ -11,71 +13,319 @@ use data::text::Span; // ================ /// Update IdMap to reflect the recent code change. -/// -// TODO[ao]: It's a really minimalistic algorithm, just extends/shrinks span without caring about -// code structure. -pub fn apply_code_change_to_id_map(id_map:&mut IdMap, removed:&Span, inserted:&str) { - let vector = &mut id_map.vec; - let inserted_len = Size::new(inserted.chars().count()); +pub fn apply_code_change_to_id_map(id_map:&mut IdMap, change:&data::text::TextChange, code:&str) { + // TODO [mwu] + // The initial provisional algorithm received some changes to better behave in our typical + // editor use-cases, i.e. to keep node ids when editing its expression. However, this came + // at price of not properly keeping other sub-ids on parts of the node line. + // In future, better and cleaner algorithm will need to be provided. + let removed = change.replaced_span(); + let inserted = change.inserted.as_str(); + let new_code = change.applied(code); + let non_white = |c:char| !c.is_whitespace(); + let logger = logger::disabled::Logger::new("apply_code_change_to_id_map"); + let vector = &mut id_map.vec; + let inserted_size = Size::from(inserted); + + info!(logger,"Old code:\n```\n{code}\n```"); + info!(logger,"New code:\n```\n{new_code}\n```"); + info!(logger,"Updating the ID map with the following text edit: {change:?}."); + + // Remove all entries fully covered by the removed span. vector.drain_filter(|(span,_)| removed.contains_span(&span)); - for (span, _) in vector { - if span.index >= removed.end() { - span.index += inserted_len; - span.index -= removed.size; + + // If the edited section ends up being the trailing part of AST node, how many bytes should be + // trimmed from the id. Precalculated, as is constant in the loop below. + let to_trim_back = { + let last_non_white = inserted.rfind(non_white); + let inserted_len = || inserted.len(); + let length_to_last_non_white = |index| inserted.len() - index - 1; + Size::new(last_non_white.map_or_else(inserted_len,length_to_last_non_white)) + }; + // As above but for the front side. + let to_trim_front = { + let first_non_white = inserted.find(non_white); + let ret = first_non_white.unwrap_or_else(|| inserted.len()); + Size::new(ret) + }; + + let inserted_non_white = inserted.chars().any(non_white); + + // In case of collisions (when, after resizing spans, multiple ids for the same span are + // present), the mappings from this map will be preferred over other ones. + // + // This is needed for edits like: `foo f` => `foo` — the earlier `foo` in `foo f` also has a + // id map entry, however we want it to be consistently shadowed by the id from the whole App + // expression. + let mut preferred : HashMap = default(); + + for (span, id) in vector.iter_mut() { + let mut trim_front = false; + let mut trim_back = false; + let initial_span = *span; + info!(logger,"Processing @{span}: `{&code[*span]}`."); + if span.index > removed.end() { + debug!(logger,"Node after the edited region."); + // AST node starts after edited region — it will be simply shifted. + let code_between = &code[Span::from(removed.end() .. span.index)]; + span.move_left(removed.size); + span.move_right(inserted_size); + + // If there are only spaces between current AST symbol and insertion, extend the symbol. + // This is for cases like line with `foo ` being changed into `foo j`. + debug!(logger,"Between: `{code_between}`."); + if all_spaces(code_between) && inserted_non_white { + debug!(logger,"Will extend the node leftwards."); + span.extend_left(inserted_size); + span.extend_left(Size::from(code_between)); + trim_front = true; + } } else if span.index >= removed.index { - let removed_chars = removed.end() - span.index; - span.index = removed.index + inserted_len; - span.size -= removed_chars; + // AST node starts inside the edited region. It doesn't end strictly inside it. + debug!(logger,"Node overlapping with the end of the edited region."); + let removed_before = span.index - removed.index; + span.move_left(removed_before); + span.shrink_right(removed.size - removed_before); + span.extend_right(inserted_size); + trim_front = true; } else if span.end() >= removed.index { - let removed_chars = (span.end() - removed.index).min(removed.size); - span.size -= removed_chars; - span.size += inserted_len; + debug!(logger,"Node overlapping with the beginning of the edited region."); + // AST node ends in the edited region. + span.set_right(removed.index); + span.extend_right(inserted_size); + trim_back = true; + } else { + debug!(logger,"Node before the edited region."); + // If there are only spaces between current AST symbol and insertion, extend the symbol. + // This is for cases like line with `foo ` being changed into `foo j`. + let between = &code[Span::from(span.end() .. removed.index)]; + if all_spaces(between) && inserted_non_white { + debug!(logger,"Will extend "); + span.size += Size::from(between) + inserted_size; + trim_back = true; + } } + + if trim_front && to_trim_front.non_empty() { + span.shrink_left(to_trim_front); + debug!(logger,"Trimming front {to_trim_front} bytes."); + } + + if trim_back { + if to_trim_back.non_empty() { + span.shrink_right(to_trim_back); + debug!(logger, "Trimming back {to_trim_back} bytes."); + } + let new_repr = &new_code[*span]; + // Trim trailing spaces + let spaces = spaces_size(new_repr.chars().rev()); + if spaces.non_empty() { + debug!(logger,"Additionally trimming {spaces} trailing spaces."); + debug!(logger,"The would-be code: `{new_repr}`."); + span.shrink_right(spaces); + } + } + + // If we edited front or end of an AST node, its extended (or shrunk) span will be + // preferred. + if trim_front || trim_back { + preferred.insert(*span,*id); + } + + info!(logger,"Processing for id {id}: {initial_span} ->\t{span}.\n\ + Code: `{&code[initial_span]}` => `{&new_code[*span]}`"); } + + // If non-preferred entry collides with the preferred one, remove the former. + vector.drain_filter(|(span,id)| { + preferred.get(span).map(|preferred_id| id != preferred_id).unwrap_or(false) + }); } +// =============== +// === Helpers === +// =============== + +/// Returns the byte length of leading space characters sequence. +fn spaces_size(itr:impl Iterator) -> Size { + Size::new(itr.take_while(|c| *c == ' ').fold(0, |acc, c| acc + c.len_utf8())) +} + +/// Checks if the given string slice contains only space charactesr. +fn all_spaces(text:&str) -> bool { + text.chars().all(|c| c == ' ') +} + + + +// ============= +// === Tests === +// ============= + #[cfg(test)] mod test { use super::*; - use ast::IdMap; + use ast::HasIdMap; use data::text::Index; - use data::text::Size; - use data::text::Span; + use data::text::TextChange; use uuid::Uuid; + use parser::Parser; + use enso_prelude::default; + + /// A sample text edit used to test "text api" properties. + /// + /// See `from_markdown` constructor function for helper markdown description. + struct Case { + /// The initial enso program code. + pub code : String, + /// The edit made to the initial code. + pub change : TextChange, + } + + impl Case { + /// Markdown supports currently a single edit in the given code piece. It must be of form + /// `«aa⎀bb»` which reads "replace `aa` with `bb`". + fn from_markdown(marked_code:impl AsRef) -> Case { + let marked_code = marked_code.as_ref(); + let index_of = |c| marked_code.find(c); + + const START : char = '«'; + const INSERTION : char = '⎀'; + const END : char = '»'; + + match (index_of(START),index_of(INSERTION),index_of(END)) { + (Some(start),insertion,Some(end)) => { + assert!(start < end,"Markdown markers discovered in wrong order."); + let erased_finish = insertion.unwrap_or(end); + let code = { + let prefix = &marked_code[..start]; + let erased = &marked_code[start + START.len_utf8() .. erased_finish]; + let suffix = &marked_code[end + END. len_utf8() .. ]; + String::from_iter([prefix,erased,suffix].iter().copied()) + }; + + let inserted_code = insertion.map_or("", |insertion| + &marked_code[insertion + INSERTION.len_utf8()..end] + ); + let removed_span = Range { + start : Index::new(start), + end : Index::new(erased_finish - START.len_utf8()), + }; + let change = TextChange::replace(removed_span,inserted_code.to_string()); + Case {code,change} + } + _ => panic!("Invalid markdown in the marked code: {}.",marked_code), + } + } + + /// Code after applying the change + fn resulting_code(&self) -> String { + self.change.applied(&self.code) + } + + /// Checks if the text operation described by this case keeps the node IDs intact. + /// + /// See `assert_same_node_ids` for details. + fn assert_edit_keeps_main_node_ids(&self, parser:&Parser) { + let ast1 = parser.parse_module(&self.code,default()).unwrap(); + let mut id_map = ast1.id_map(); + + apply_code_change_to_id_map(&mut id_map,&self.change,&self.code); + let code2 = self.resulting_code(); + + let ast2 = parser.parse_module(&code2,id_map.clone()).unwrap(); + assert_same_node_ids(&ast1,&ast2); + } + } + + /// Pretty prints the code of module with a single function named `main`. The lines should + /// contain unindented main function's block lines. + fn to_main(lines:impl IntoIterator>) -> String { + let mut ret = "main = ".to_string(); + for line in lines { + ret.push_str(&format!("\n {}", line.as_ref())) + } + ret + } + + /// Returns the IDs of nodes in the `main` function in their order of line appearance. + fn main_nodes(module:&ast::known::Module) -> Vec { + use double_representation::definition::*; + use double_representation::graph::GraphInfo; + let id = Id::new_plain_name("main"); + let definition = traverse_for_definition(module,&id).unwrap(); + let graph = GraphInfo::from_definition(definition); + let nodes = graph.nodes(); + nodes.into_iter().map(|node| node.id()).collect() + } + + /// Checks that both module AST contain `main` function that has the same sequence of node IDs, + /// as described by the `main_nodes` function. + fn assert_same_node_ids(ast1:&ast::known::Module,ast2:&ast::known::Module) { + let ids1 = main_nodes(ast1); + let ids2 = main_nodes(ast2); + println!("IDs1: {:?}", ids1); + println!("IDs2: {:?}", ids2); + assert_eq!(ids1,ids2); + } #[test] + fn test_case_markdown() { + let case = Case::from_markdown("foo«aa⎀bb»c"); + assert_eq!(case.code, "fooaac"); + assert_eq!(case.change.inserted, "bb"); + assert_eq!(case.change.replaced, Index::new(3)..Index::new(5)); + assert_eq!(case.resulting_code(), "foobbc"); + + let case = Case::from_markdown("foo«aa»c"); + assert_eq!(case.code, "fooaac"); + assert_eq!(case.change.inserted, ""); + assert_eq!(case.change.replaced, Index::new(3)..Index::new(5)); + assert_eq!(case.resulting_code(), "fooc"); + } + + #[wasm_bindgen_test] fn applying_code_changes_to_id_map() { - let uuid1 = Uuid::new_v4(); - let uuid2 = Uuid::new_v4(); - let uuid3 = Uuid::new_v4(); - let uuid4 = Uuid::new_v4(); - let uuid5 = Uuid::new_v4(); - let mut id_map = IdMap::new(vec! - [ (Span::new(Index::new(0) , Size::new(3)), uuid1) - , (Span::new(Index::new(5) , Size::new(2)), uuid2) - , (Span::new(Index::new(7) , Size::new(2)), uuid3) - , (Span::new(Index::new(9) , Size::new(2)), uuid4) - , (Span::new(Index::new(13), Size::new(2)), uuid5) - ]); + let parser = Parser::new_or_panic(); - apply_code_change_to_id_map(&mut id_map, &Span::new(Index::new(6),Size::new(4)), "a test"); - let expected = IdMap::new(vec! - [ (Span::new(Index::new(0) , Size::new(3)), uuid1) - , (Span::new(Index::new(5) , Size::new(7)), uuid2) - , (Span::new(Index::new(12), Size::new(1)), uuid4) - , (Span::new(Index::new(15), Size::new(2)), uuid5) - ]); - assert_eq!(expected, id_map); + // All the cases describe edit to a middle line in three line main definition. + let cases = [ + "a = «⎀f»foo", + "a = «f»foo", + "a = «⎀ »foo", + "a = « »foo", + "a = «⎀f» foo", + "a = foo«⎀ »", + "a = foo«⎀\n»", + "a = foo «⎀\n»", + "a = foo «⎀j»", + "a = foo «j»", + "a = foo«⎀j»", - apply_code_change_to_id_map(&mut id_map, &Span::new(Index::new(12), Size::new(2)), "x"); - let expected = IdMap::new(vec! - [ (Span::new(Index::new(0) , Size::new(3)), uuid1) - , (Span::new(Index::new(5) , Size::new(8)), uuid2) - , (Span::new(Index::new(14), Size::new(2)), uuid5) - ]); - assert_eq!(expected, id_map); + // Same as above but not in an assignment form + "«⎀f»foo", + "«f»foo", + // Commented out tests below would fail because of leading whitespace breaking the + // block structure. + // "«⎀ »foo", + // "« »foo", + // "«⎀f» foo", + "foo«⎀ »", + "foo«⎀\n»", + "foo «⎀\n»", + "foo «⎀j»", + "foo «j»", + "foo«⎀j»", + ]; + + for case in cases.iter() { + let all_nodes = ["previous",case,"next"]; + let main_def = to_main(all_nodes.iter()); + let case = Case::from_markdown(main_def); + case.assert_edit_keeps_main_node_ids(&parser); + } } } diff --git a/gui/src/rust/ide/src/model/module.rs b/gui/src/rust/ide/src/model/module.rs index e7813a6bdc0..eec4c9df49a 100644 --- a/gui/src/rust/ide/src/model/module.rs +++ b/gui/src/rust/ide/src/model/module.rs @@ -394,10 +394,8 @@ impl Module { pub fn apply_code_change (&self, change:TextChange, parser:&Parser, new_id_map:ast::IdMap) -> FallibleResult<()> { let mut code = self.ast().repr(); - let replaced_indices = change.replaced.start.value..change.replaced.end.value; let replaced_location = TextLocation::convert_range(&code,&change.replaced); - - code.replace_range(replaced_indices,&change.inserted); + change.apply(&mut code); let new_ast = parser.parse(code,new_id_map)?.try_into()?; self.content.borrow_mut().ast = new_ast; self.notify(Notification::CodeChanged {change,replaced_location}); diff --git a/gui/src/rust/ide/src/model/synchronized/module.rs b/gui/src/rust/ide/src/model/synchronized/module.rs index 35c2e545c83..ac5f36c7f57 100644 --- a/gui/src/rust/ide/src/model/synchronized/module.rs +++ b/gui/src/rust/ide/src/model/synchronized/module.rs @@ -184,6 +184,7 @@ impl Module { async fn handle_notification (&self, content:&LanguageServerContent, notification:Notification) -> FallibleResult { + debug!(self.logger,"Handling notification: {content:?}."); match content { LanguageServerContent::Desynchronized(summary) => self.full_invalidation(summary).await, LanguageServerContent::Synchronized(summary) => match notification { @@ -214,6 +215,7 @@ impl Module { /// of Language Server state. async fn full_invalidation (&self, ls_content:&ContentSummary) -> FallibleResult { + debug!(self.logger,"Handling full invalidation: {ls_content:?}."); let range = TextLocation::at_document_begin()..ls_content.end_of_file; self.notify_language_server(ls_content,|content| vec![TextEdit { range : range.into(), @@ -236,6 +238,7 @@ impl Module { old_version : ls_content.digest.clone(), new_version : summary.digest.clone() }; + debug!(self.logger,"Notifying LS with edit: {edit:?}."); self.language_server.client.apply_text_file_edit(&edit).await?; Ok(summary) } diff --git a/gui/src/rust/lib/data/src/text.rs b/gui/src/rust/lib/data/src/text.rs index a982780ca0a..e5f80e61584 100644 --- a/gui/src/rust/lib/data/src/text.rs +++ b/gui/src/rust/lib/data/src/text.rs @@ -11,6 +11,7 @@ use serde::Serialize; use serde::Deserialize; + /// ====================================== /// === Text Coordinates And Distances === /// ====================================== @@ -66,6 +67,16 @@ impl Size { pub fn new(value:usize) -> Self { Size {value} } + + /// Checks if this is a non-empty size (more than zero elements). + pub fn non_empty(self) -> bool { + self.value > 0 + } + + /// Checks if this is an empty size (zero elements). + pub fn is_empty(self) -> bool { + self.value == 0 + } } impl Add for Size { @@ -94,6 +105,18 @@ impl SubAssign for Size { } } +impl Display for Size { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f,"{}",self.value) + } +} + +impl From<&str> for Size { + fn from(text:&str) -> Self { + Size::new(text.len()) + } +} + // === Span === @@ -147,6 +170,50 @@ impl Span { let end = self.end().value; start .. end } + + /// Expand the span by moving its left (start) index. + pub fn extend_left(&mut self, size:Size) { + self.index -= size; + self.size += size; + } + + /// Expand the span by moving its right (end) index. + pub fn extend_right(&mut self, size:Size) { + self.size += size; + } + + /// Shrink the span by moving its left (start) index. + pub fn shrink_left(&mut self, size:Size) { + self.index += size; + self.size -= size; + } + + /// Shrink the span by moving its right (end) index. + pub fn shrink_right(&mut self, size:Size) { + self.size -= size; + } + + /// Move the whole span left, maintaining its size. + pub fn move_left(&mut self, size:Size) { + self.index -= size; + } + + /// Move the whole span right, maintaining its size. + pub fn move_right(&mut self, size:Size) { + self.index += size; + } + + /// Move the start index of the span, adjusting the size. + pub fn set_left(&mut self, new_left:Index) { + let end = self.end(); + self.index = new_left; + self.size = end - new_left; + } + + /// Move the end index of the span, adjusting the size. + pub fn set_right(&mut self, new_right:Index) { + self.size = new_right - self.index; + } } impls! { From + &From > for Span { |range| @@ -163,6 +230,34 @@ impl PartialEq> for Span { } } +impl Display for Span { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f,"{}..{}",self.index.value,self.end().value) + } +} + +impl std::ops::Index for str { + type Output = str; + + fn index(&self, index:Span) -> &Self::Output { + &self[index.range()] + } +} + +impl std::ops::Index for String { + type Output = str; + + fn index(&self, index:Span) -> &Self::Output { + &self.as_str()[index] + } +} + +impl From> for Span { + fn from(range:Range) -> Self { + Span::from_indices(range.start,range.end) + } +} + // === Operators for Index and Size === @@ -320,6 +415,45 @@ impl TextChangeTemplate { } } +impl TextChangeTemplate { + /// Calculate the size of the replaced text. + pub fn replaced_size(&self) -> Index::Output { + self.replaced.end.clone() - self.replaced.start.clone() + } +} + +impl TextChangeTemplate { + /// Calculate the size of the replaced text. + pub fn replaced_span(&self) -> Span { + let index = self.replaced.start; + let size = self.replaced_size(); + Span {index,size} + } + + /// Applies the text edit on given `String` value. + /// + /// # Panics + /// + /// Panics if the replaced span is out of the string value bounds. + pub fn apply(&self, target:&mut String) where Content:AsRef { + //debug!(logger, "change: {change:?}, my code: \n```\n{code}\n```"); + let replaced_indices = self.replaced.start.value..self.replaced.end.value; + //debug!(logger, "replacing range {replaced_indices:?} with {change.inserted}"); + target.replace_range(replaced_indices,self.inserted.as_ref()); + } + + /// Applies the text edit on string and returns the result. + /// + /// # Panics + /// + /// Panics if the replaced span is out of the string value bounds. + pub fn applied(&self, target:&str) -> String where Content:AsRef { + let mut target = target.to_string(); + self.apply(&mut target); + target + } +} + impl TextChangeTemplate { /// Creates operation which deletes text at given range. pub fn delete(range:Range) -> Self {