Original commit: e41be1d351
This commit is contained in:
Michał Wawrzyniec Urbańczyk 2020-06-13 03:53:27 +02:00 committed by GitHub
parent f96ea27f4a
commit 198526afaa
8 changed files with 465 additions and 60 deletions

View File

@ -1262,6 +1262,7 @@ dependencies = [
"graph-editor 0.1.0", "graph-editor 0.1.0",
"js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)", "js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)",
"json-rpc 0.1.0", "json-rpc 0.1.0",
"logger 0.1.0",
"nalgebra 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)", "nalgebra 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)",
"parser 0.1.0", "parser 0.1.0",
"regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -16,6 +16,7 @@ data = { version = "0.1.0" , path = "../lib/data"
enso-frp = { version = "0.1.0" , path = "../lib/frp" } enso-frp = { version = "0.1.0" , path = "../lib/frp" }
enso-prelude = { version = "0.1.0" , path = "../lib/prelude" } enso-prelude = { version = "0.1.0" , path = "../lib/prelude" }
graph-editor = { version = "0.1.0" , path = "../lib/graph-editor" } 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" } shapely = { version = "0.1.0" , path = "../lib/shapely/impl" }
ast = { version = "0.1.0" , path = "ast/impl" } ast = { version = "0.1.0" , path = "ast/impl" }

View File

@ -55,6 +55,14 @@ use serde::Serialize;
use shapely::*; use shapely::*;
use uuid::Uuid; use uuid::Uuid;
/// A sequence of AST nodes, typically the "token soup".
pub type Stream<T> = Vec<T>;
// =============
// === IdMap ===
// =============
/// A mapping between text position and immutable ID. /// A mapping between text position and immutable ID.
#[derive(Clone,Debug,Default,Deserialize,Eq,PartialEq,Serialize)] #[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<T> = Vec<T>;
@ -208,9 +214,9 @@ impl<'t> IntoIterator for &'t Ast {
} }
} }
impl ToString for Ast { impl Display for Ast {
fn to_string(&self) -> String { fn fmt(&self, f:&mut fmt::Formatter<'_>) -> fmt::Result {
self.repr() write!(f,"{}",self.repr())
} }
} }
@ -279,6 +285,21 @@ impl Ast {
pub fn with_shape<S:Into<Shape<Ast>>>(&self, shape:S) -> Ast { pub fn with_shape<S:Into<Shape<Ast>>>(&self, shape:S) -> Ast {
Ast::new(shape.into(),self.id) 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. /// Fills `id` with `None` by default.

View File

@ -80,11 +80,8 @@ impl Handle {
/// May return Error when new code causes parsing errors, or when parsed code does not produce /// May return Error when new code causes parsing errors, or when parsed code does not produce
/// Module ast. /// Module ast.
pub fn apply_code_change(&self,change:TextChange) -> FallibleResult<()> { pub fn apply_code_change(&self,change:TextChange) -> FallibleResult<()> {
let mut id_map = self.model.ast().id_map(); let mut id_map = self.model.ast().id_map();
let replaced_size = change.replaced.end - change.replaced.start; apply_code_change_to_id_map(&mut id_map,&change,&self.model.ast().repr());
let replaced_span = Span::new(change.replaced.start,replaced_size);
apply_code_change_to_id_map(&mut id_map,&replaced_span,&change.inserted);
self.model.apply_code_change(change,&self.parser,id_map) 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(); let controller = Handle::new_mock(location,module,id_map,ls,parser).unwrap();
// Change code from "2+2" to "22+2" // 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(); controller.apply_code_change(change).unwrap();
let expected_ast = Ast::new_no_id(ast::Module { let expected_ast = Ast::new_no_id(ast::Module {
lines: vec![BlockLine { lines: vec![BlockLine {

View File

@ -1,5 +1,7 @@
//! A module with functions used to support working with text representation of the language. //! A module with functions used to support working with text representation of the language.
use crate::prelude::*;
use ast::IdMap; use ast::IdMap;
use data::text::Size; use data::text::Size;
use data::text::Span; use data::text::Span;
@ -11,71 +13,319 @@ use data::text::Span;
// ================ // ================
/// Update IdMap to reflect the recent code change. /// Update IdMap to reflect the recent code change.
/// pub fn apply_code_change_to_id_map(id_map:&mut IdMap, change:&data::text::TextChange, code:&str) {
// TODO[ao]: It's a really minimalistic algorithm, just extends/shrinks span without caring about // TODO [mwu]
// code structure. // The initial provisional algorithm received some changes to better behave in our typical
pub fn apply_code_change_to_id_map(id_map:&mut IdMap, removed:&Span, inserted:&str) { // editor use-cases, i.e. to keep node ids when editing its expression. However, this came
let vector = &mut id_map.vec; // at price of not properly keeping other sub-ids on parts of the node line.
let inserted_len = Size::new(inserted.chars().count()); // 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)); vector.drain_filter(|(span,_)| removed.contains_span(&span));
for (span, _) in vector {
if span.index >= removed.end() { // If the edited section ends up being the trailing part of AST node, how many bytes should be
span.index += inserted_len; // trimmed from the id. Precalculated, as is constant in the loop below.
span.index -= removed.size; 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<Span,ast::Id> = 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 { } else if span.index >= removed.index {
let removed_chars = removed.end() - span.index; // AST node starts inside the edited region. It doesn't end strictly inside it.
span.index = removed.index + inserted_len; debug!(logger,"Node overlapping with the end of the edited region.");
span.size -= removed_chars; 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 { } else if span.end() >= removed.index {
let removed_chars = (span.end() - removed.index).min(removed.size); debug!(logger,"Node overlapping with the beginning of the edited region.");
span.size -= removed_chars; // AST node ends in the edited region.
span.size += inserted_len; 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<Item=char>) -> 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use ast::IdMap; use ast::HasIdMap;
use data::text::Index; use data::text::Index;
use data::text::Size; use data::text::TextChange;
use data::text::Span;
use uuid::Uuid; 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<str>) -> 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<Item:AsRef<str>>) -> 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<Uuid> {
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] #[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() { fn applying_code_changes_to_id_map() {
let uuid1 = Uuid::new_v4(); let parser = Parser::new_or_panic();
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)
]);
apply_code_change_to_id_map(&mut id_map, &Span::new(Index::new(6),Size::new(4)), "a test"); // All the cases describe edit to a middle line in three line main definition.
let expected = IdMap::new(vec! let cases = [
[ (Span::new(Index::new(0) , Size::new(3)), uuid1) "a = «⎀f»foo",
, (Span::new(Index::new(5) , Size::new(7)), uuid2) "a = «f»foo",
, (Span::new(Index::new(12), Size::new(1)), uuid4) "a = «⎀ »foo",
, (Span::new(Index::new(15), Size::new(2)), uuid5) "a = « »foo",
]); "a = «⎀f» foo",
assert_eq!(expected, id_map); "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"); // Same as above but not in an assignment form
let expected = IdMap::new(vec! "«⎀f»foo",
[ (Span::new(Index::new(0) , Size::new(3)), uuid1) "«f»foo",
, (Span::new(Index::new(5) , Size::new(8)), uuid2) // Commented out tests below would fail because of leading whitespace breaking the
, (Span::new(Index::new(14), Size::new(2)), uuid5) // block structure.
]); // "«⎀ »foo",
assert_eq!(expected, id_map); // "« »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);
}
} }
} }

View File

@ -394,10 +394,8 @@ impl Module {
pub fn apply_code_change pub fn apply_code_change
(&self, change:TextChange, parser:&Parser, new_id_map:ast::IdMap) -> FallibleResult<()> { (&self, change:TextChange, parser:&Parser, new_id_map:ast::IdMap) -> FallibleResult<()> {
let mut code = self.ast().repr(); 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); let replaced_location = TextLocation::convert_range(&code,&change.replaced);
change.apply(&mut code);
code.replace_range(replaced_indices,&change.inserted);
let new_ast = parser.parse(code,new_id_map)?.try_into()?; let new_ast = parser.parse(code,new_id_map)?.try_into()?;
self.content.borrow_mut().ast = new_ast; self.content.borrow_mut().ast = new_ast;
self.notify(Notification::CodeChanged {change,replaced_location}); self.notify(Notification::CodeChanged {change,replaced_location});

View File

@ -184,6 +184,7 @@ impl Module {
async fn handle_notification async fn handle_notification
(&self, content:&LanguageServerContent, notification:Notification) (&self, content:&LanguageServerContent, notification:Notification)
-> FallibleResult<ParsedContentSummary> { -> FallibleResult<ParsedContentSummary> {
debug!(self.logger,"Handling notification: {content:?}.");
match content { match content {
LanguageServerContent::Desynchronized(summary) => self.full_invalidation(summary).await, LanguageServerContent::Desynchronized(summary) => self.full_invalidation(summary).await,
LanguageServerContent::Synchronized(summary) => match notification { LanguageServerContent::Synchronized(summary) => match notification {
@ -214,6 +215,7 @@ impl Module {
/// of Language Server state. /// of Language Server state.
async fn full_invalidation async fn full_invalidation
(&self, ls_content:&ContentSummary) -> FallibleResult<ParsedContentSummary> { (&self, ls_content:&ContentSummary) -> FallibleResult<ParsedContentSummary> {
debug!(self.logger,"Handling full invalidation: {ls_content:?}.");
let range = TextLocation::at_document_begin()..ls_content.end_of_file; let range = TextLocation::at_document_begin()..ls_content.end_of_file;
self.notify_language_server(ls_content,|content| vec![TextEdit { self.notify_language_server(ls_content,|content| vec![TextEdit {
range : range.into(), range : range.into(),
@ -236,6 +238,7 @@ impl Module {
old_version : ls_content.digest.clone(), old_version : ls_content.digest.clone(),
new_version : summary.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?; self.language_server.client.apply_text_file_edit(&edit).await?;
Ok(summary) Ok(summary)
} }

View File

@ -11,6 +11,7 @@ use serde::Serialize;
use serde::Deserialize; use serde::Deserialize;
/// ====================================== /// ======================================
/// === Text Coordinates And Distances === /// === Text Coordinates And Distances ===
/// ====================================== /// ======================================
@ -66,6 +67,16 @@ impl Size {
pub fn new(value:usize) -> Self { pub fn new(value:usize) -> Self {
Size {value} 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 { 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 === // === Span ===
@ -147,6 +170,50 @@ impl Span {
let end = self.end().value; let end = self.end().value;
start .. end 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 <Range<usize>> for Span { |range| impls! { From + &From <Range<usize>> for Span { |range|
@ -163,6 +230,34 @@ impl PartialEq<Range<usize>> 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<Span> for str {
type Output = str;
fn index(&self, index:Span) -> &Self::Output {
&self[index.range()]
}
}
impl std::ops::Index<Span> for String {
type Output = str;
fn index(&self, index:Span) -> &Self::Output {
&self.as_str()[index]
}
}
impl From<Range<Index>> for Span {
fn from(range:Range<Index>) -> Self {
Span::from_indices(range.start,range.end)
}
}
// === Operators for Index and Size === // === Operators for Index and Size ===
@ -320,6 +415,45 @@ impl<Index,Content> TextChangeTemplate<Index,Content> {
} }
} }
impl<Index:Sub+Clone,Content> TextChangeTemplate<Index,Content> {
/// Calculate the size of the replaced text.
pub fn replaced_size(&self) -> Index::Output {
self.replaced.end.clone() - self.replaced.start.clone()
}
}
impl<Content> TextChangeTemplate<Index,Content> {
/// 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<str> {
//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<str> {
let mut target = target.to_string();
self.apply(&mut target);
target
}
}
impl<Index,Content:Default> TextChangeTemplate<Index,Content> { impl<Index,Content:Default> TextChangeTemplate<Index,Content> {
/// Creates operation which deletes text at given range. /// Creates operation which deletes text at given range.
pub fn delete(range:Range<Index>) -> Self { pub fn delete(range:Range<Index>) -> Self {