mirror of
https://github.com/enso-org/enso.git
synced 2024-12-20 11:41:46 +03:00
Text API changes (https://github.com/enso-org/ide/pull/556)
Original commit: e41be1d351
This commit is contained in:
parent
f96ea27f4a
commit
198526afaa
1
gui/src/rust/Cargo.lock
generated
1
gui/src/rust/Cargo.lock
generated
@ -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)",
|
||||
|
@ -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" }
|
||||
|
@ -55,6 +55,14 @@ use serde::Serialize;
|
||||
use shapely::*;
|
||||
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.
|
||||
#[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 {
|
||||
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<S:Into<Shape<Ast>>>(&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.
|
||||
|
@ -81,10 +81,7 @@ impl Handle {
|
||||
/// 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);
|
||||
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 {
|
||||
|
@ -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) {
|
||||
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_len = Size::new(inserted.chars().count());
|
||||
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<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 {
|
||||
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<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)]
|
||||
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<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]
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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});
|
||||
|
@ -184,6 +184,7 @@ impl Module {
|
||||
async fn handle_notification
|
||||
(&self, content:&LanguageServerContent, notification:Notification)
|
||||
-> FallibleResult<ParsedContentSummary> {
|
||||
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<ParsedContentSummary> {
|
||||
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)
|
||||
}
|
||||
|
@ -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 <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 ===
|
||||
|
||||
@ -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> {
|
||||
/// Creates operation which deletes text at given range.
|
||||
pub fn delete(range:Range<Index>) -> Self {
|
||||
|
Loading…
Reference in New Issue
Block a user