Implemented notification about text&graph changes brokering (https://github.com/enso-org/ide/pull/231)

Add possibility to subscribe for notifications about changes of one of module representations. Today, the notifications are sent after applying code changes (there are no other operations implemented yet).

Original commit: 68b63f2891
This commit is contained in:
Adam Obuchowicz 2020-03-13 17:52:41 +01:00 committed by GitHub
parent 4fd137bfc3
commit f26d88593c
20 changed files with 617 additions and 385 deletions

8
gui/Cargo.lock generated
View File

@ -567,7 +567,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"basegl-system-web 0.1.0", "basegl-system-web 0.1.0",
"enso-prelude 0.1.0", "enso-prelude 0.1.0",
"keyboard-types 0.4.3 (git+https://github.com/farmaazon/keyboard-types)", "keyboard-types 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rust-dense-bitset 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rust-dense-bitset 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -1218,8 +1218,8 @@ dependencies = [
[[package]] [[package]]
name = "keyboard-types" name = "keyboard-types"
version = "0.4.3" version = "0.5.0"
source = "git+https://github.com/farmaazon/keyboard-types#c983ff5cba38a8b712a59dbd1a7406c4b1a3c17d" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
@ -3152,7 +3152,7 @@ dependencies = [
"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" "checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
"checksum js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "7889c7c36282151f6bf465be4700359318aef36baa951462382eae49e9577cf9" "checksum js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "7889c7c36282151f6bf465be4700359318aef36baa951462382eae49e9577cf9"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum keyboard-types 0.4.3 (git+https://github.com/farmaazon/keyboard-types)" = "<none>" "checksum keyboard-types 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a989afac88279b0482f402d234b5fbd405bf1ad051308595b58de4e6de22346b"
"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
"checksum libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" "checksum libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018"

View File

@ -14,6 +14,7 @@ need the following setup:
rustup toolchain install nightly-2019-11-04 # Install the nightly channel. rustup toolchain install nightly-2019-11-04 # Install the nightly channel.
rustup default nightly # Set it as the default one. rustup default nightly # Set it as the default one.
rustup component add clippy # Install the linter. rustup component add clippy # Install the linter.
cargo install cargo-watch # To enable ./run watch utility
``` ```
- **Node and Node Package Manager LTS** - **Node and Node Package Manager LTS**
@ -22,7 +23,7 @@ need the following setup:
changes are known to cause serious issues, thus **we provide support for the latest LTS version only. changes are known to cause serious issues, thus **we provide support for the latest LTS version only.
Please do not report build issues if you use other versions.** In case you run run MacOS or Linux Please do not report build issues if you use other versions.** In case you run run MacOS or Linux
the easiest way to setup the proper version is by installing the the easiest way to setup the proper version is by installing the
[Node Version Manager](https://github.com/nvm-sh/nvm) and running `nvm use --lts`. [Node Version Manager](https://github.com/nvm-sh/nvm) and running `nvm install --lts && nvm use --lts`.
<br/> <br/>
<br/> <br/>

View File

@ -3,20 +3,19 @@
pub mod content; pub mod content;
pub mod cursor; pub mod cursor;
pub mod frp; pub mod frp;
pub mod location;
pub mod render; pub mod render;
pub mod word_occurrence; pub mod word_occurrence;
use crate::prelude::*; use crate::prelude::*;
use crate::display; use crate::display;
use crate::display::shape::text::text_field::content::location::TextLocationChange;
use crate::display::shape::text::text_field::content::TextFieldContent; use crate::display::shape::text::text_field::content::TextFieldContent;
use crate::display::shape::text::text_field::cursor::Cursors; use crate::display::shape::text::text_field::cursor::Cursors;
use crate::display::shape::text::text_field::cursor::Cursor; use crate::display::shape::text::text_field::cursor::Cursor;
use crate::display::shape::text::text_field::cursor::CursorId; use crate::display::shape::text::text_field::cursor::CursorId;
use crate::display::shape::text::text_field::cursor::Step; use crate::display::shape::text::text_field::cursor::Step;
use crate::display::shape::text::text_field::cursor::CursorNavigation; use crate::display::shape::text::text_field::cursor::CursorNavigation;
use crate::display::shape::text::text_field::location::TextLocationChange;
use crate::display::shape::text::text_field::frp::TextFieldFrp; use crate::display::shape::text::text_field::frp::TextFieldFrp;
use crate::display::shape::text::text_field::word_occurrence::WordOccurrences; use crate::display::shape::text::text_field::word_occurrence::WordOccurrences;
use crate::display::shape::text::glyph::font::FontHandle; use crate::display::shape::text::glyph::font::FontHandle;
@ -26,7 +25,6 @@ use crate::display::shape::text::text_field::render::assignment::GlyphLinesAssig
use crate::display::world::World; use crate::display::world::World;
use data::text::TextChange; use data::text::TextChange;
use data::text::TextChangedNotification;
use data::text::TextLocation; use data::text::TextLocation;
use nalgebra::Vector2; use nalgebra::Vector2;
use nalgebra::Vector3; use nalgebra::Vector3;
@ -85,7 +83,7 @@ shared! { TextField
frp : Option<TextFieldFrp>, frp : Option<TextFieldFrp>,
word_occurrences : Option<WordOccurrences>, word_occurrences : Option<WordOccurrences>,
#[derivative(Debug="ignore")] #[derivative(Debug="ignore")]
text_change_callback : Option<Box<dyn FnMut(&TextChangedNotification)>> text_change_callback : Option<Box<dyn FnMut(&TextChange)>>
} }
impl { impl {
@ -173,16 +171,6 @@ shared! { TextField
self.rendered.update_cursor_sprites(&self.cursors, &mut self.content); self.rendered.update_cursor_sprites(&self.cursors, &mut self.content);
} }
/// Make change in text content.
///
/// As an opposite to `edit` function, here we don't care about cursors, nor call any
/// "text changed" callback, just do the change described in `TextChange` structure.
pub fn apply_change(&mut self, change:TextChange) {
self.content.apply_change(change);
self.assignment_update().update_after_text_edit();
self.rendered.update_glyphs(&mut self.content);
}
/// Obtains the whole text content as a single String. /// Obtains the whole text content as a single String.
pub fn get_content(&self) -> String { pub fn get_content(&self) -> String {
let mut line_strings = self.content.lines().iter().map(|l| l.to_string()); let mut line_strings = self.content.lines().iter().map(|l| l.to_string());
@ -242,7 +230,7 @@ shared! { TextField
/// ///
/// This callback will be called once per `write` function call and all functions using it. /// This callback will be called once per `write` function call and all functions using it.
/// That's include all edits being an effect of keyboard or mouse event. /// That's include all edits being an effect of keyboard or mouse event.
pub fn set_text_edit_callback<Callback:FnMut(&TextChangedNotification) + 'static> pub fn set_text_edit_callback<Callback:FnMut(&TextChange) + 'static>
(&mut self, callback:Callback) { (&mut self, callback:Callback) {
self.text_change_callback = Some(Box::new(callback)) self.text_change_callback = Some(Box::new(callback))
} }
@ -381,18 +369,20 @@ impl TextFieldData {
} }
} }
/// Applies change for one cursor, updating its position, and returns struct which should be
/// passed to `text_change_callback`.
fn apply_one_cursor_change fn apply_one_cursor_change
(&mut self, location_change:&mut TextLocationChange, cursor_id:CursorId, to_insert:&str) (&mut self, location_change:&mut TextLocationChange, cursor_id:CursorId, to_insert:&str)
-> TextChangedNotification { -> TextChange {
let CursorId(id) = cursor_id; let CursorId(id) = cursor_id;
let cursor = &mut self.cursors.cursors[id]; let cursor = &mut self.cursors.cursors[id];
let replaced = location_change.apply_to_range(cursor.selection_range()); let replaced = location_change.apply_to_range(cursor.selection_range());
let replaced_chars = self.content.convert_location_range_to_char_index(&replaced); let replaced_chars = self.content.convert_location_range_to_char_index(&replaced);
let change = TextChange::replace(replaced,to_insert); let change = content::Change::replace(replaced,to_insert);
location_change.add_change(&change); location_change.add_change(&change);
*cursor = Cursor::new(change.inserted_text_range().end); *cursor = Cursor::new(change.inserted_text_range().end);
self.content.apply_change(change.clone()); self.content.apply_change(change);
TextChangedNotification {change,replaced_chars} TextChange::replace(replaced_chars, to_insert.to_string())
} }
} }

View File

@ -1,5 +1,6 @@
//! Module with all structures describing the content of the TextField. //! Module with all structures describing the content of the TextField.
pub mod line; pub mod line;
pub mod location;
use crate::prelude::*; use crate::prelude::*;
@ -8,8 +9,8 @@ use crate::display::shape::text::text_field::content::line::Line;
use crate::display::shape::text::text_field::content::line::LineFullInfo; use crate::display::shape::text::text_field::content::line::LineFullInfo;
use crate::display::shape::text::text_field::TextFieldProperties; use crate::display::shape::text::text_field::TextFieldProperties;
use data::text::ChangeType; use data::text;
use data::text::TextChange; use data::text::TextChangeTemplate;
use data::text::TextLocation; use data::text::TextLocation;
use data::text::split_to_lines; use data::text::split_to_lines;
use nalgebra::Vector2; use nalgebra::Vector2;
@ -78,6 +79,75 @@ impl DirtyLines {
} }
// ==============
// === Change ===
// ==============
/// A change type
#[derive(Copy,Clone,Debug)]
pub enum ChangeType {
/// A change where we replace fragment of one line with text without new lines.
SingleLine,
/// A multi-line change is a change which is not a single line change (see docs for SingleLine).
MultiLine
}
/// A type representing change applied on TextFieldContent.
#[derive(Debug,Shrinkwrap)]
#[shrinkwrap(mutable)]
pub struct Change(pub TextChangeTemplate<TextLocation,Vec<Vec<char>>>);
impl Change {
/// Creates operation which inserts text at given position.
pub fn insert(at:TextLocation, text:&str) -> Self {
Self(TextChangeTemplate::insert(at,Self::mk_lines_as_char_vector(text)))
}
/// Creates operation which deletes text at given range.
pub fn delete(range:Range<TextLocation>) -> Self {
Self(TextChangeTemplate::delete(range))
}
/// Creates operation which replaces text at given range with given string.
pub fn replace(replaced:Range<TextLocation>, text:&str) -> Self {
Self(TextChangeTemplate::replace(replaced,Self::mk_lines_as_char_vector(text)))
}
/// Converts change representation to String.
pub fn inserted_string(&self) -> String {
self.inserted.iter().map(|line| line.iter().collect::<String>()).join("\n")
}
/// A type of this change. See `ChangeType` doc for details.
pub fn change_type(&self) -> ChangeType {
let is_one_line_modified = self.replaced.start.line == self.replaced.end.line;
let is_mostly_one_line_inserted = self.inserted.len() <= 1;
if is_one_line_modified && is_mostly_one_line_inserted {
ChangeType::SingleLine
} else {
ChangeType::MultiLine
}
}
/// Returns text location range where the inserted text will appear after making this change.
pub fn inserted_text_range(&self) -> Range<TextLocation> {
let start = self.replaced.start;
let end_line = start.line + self.inserted.len().saturating_sub(1);
let last_line_len = self.inserted.last().map_or(0, |l| l.len());
let end_column = if start.line == end_line {
start.column + last_line_len
} else {
last_line_len
};
start..TextLocation{line:end_line, column:end_column}
}
fn mk_lines_as_char_vector(text:&str) -> Vec<Vec<char>> {
split_to_lines(text).map(|s| s.chars().collect_vec()).collect()
}
}
// ============================ // ============================
// === TextFieldContent === // === TextFieldContent ===
@ -199,7 +269,7 @@ impl TextFieldContent {
/// Converts location in this text represented by `row:column` pair to absolute char's position /// Converts location in this text represented by `row:column` pair to absolute char's position
/// from document begin. Panics if location is invalid for current content. /// from document begin. Panics if location is invalid for current content.
pub fn convert_location_to_char_index(&mut self, location:TextLocation) -> usize { pub fn convert_location_to_char_index(&mut self, location:TextLocation) -> text::Index {
if self.line_offsets.is_empty() { if self.line_offsets.is_empty() {
self.line_offsets.push(0); self.line_offsets.push(0);
} }
@ -211,13 +281,13 @@ impl TextFieldContent {
self.line_offsets.push(current_char_index); self.line_offsets.push(current_char_index);
} }
} }
self.line_offsets[location.line] + location.column text::Index::new(self.line_offsets[location.line] + location.column)
} }
/// Converts range of locations in this text represented by `row:column` pair to absolute /// Converts range of locations in this text represented by `row:column` pair to absolute
/// char's position from document begin. /// char's position from document begin.
pub fn convert_location_range_to_char_index(&mut self, range:&Range<TextLocation>) pub fn convert_location_range_to_char_index(&mut self, range:&Range<TextLocation>)
-> Range<usize> { -> Range<text::Index> {
let start = self.convert_location_to_char_index(range.start); let start = self.convert_location_to_char_index(range.start);
let end = self.convert_location_to_char_index(range.end); let end = self.convert_location_to_char_index(range.end);
start..end start..end
@ -229,7 +299,7 @@ impl TextFieldContent {
impl TextFieldContent { impl TextFieldContent {
/// Apply change to content. /// Apply change to content.
pub fn apply_change(&mut self, change:TextChange) { pub fn apply_change(&mut self, change:Change) {
let first_modified_line = change.replaced.start.line; let first_modified_line = change.replaced.start.line;
match change.change_type() { match change.change_type() {
ChangeType::SingleLine => self.make_simple_change(change), ChangeType::SingleLine => self.make_simple_change(change),
@ -241,27 +311,28 @@ impl TextFieldContent {
} }
/// Apply many changes to content. /// Apply many changes to content.
pub fn apply_changes<Changes:IntoIterator<Item=TextChange>>(&mut self, changes:Changes) { pub fn apply_changes<Changes:IntoIterator<Item=Change>>(&mut self, changes:Changes) {
let change_key = |chg:&TextChange | chg.replaced.start; let change_key = |chg:&Change | chg.replaced.start;
let changes_vec = changes.into_iter().sorted_by_key(change_key); let changes_vec = changes.into_iter().sorted_by_key(change_key);
changes_vec.rev().for_each(|change| self.apply_change(change)); changes_vec.rev().for_each(|change| self.apply_change(change));
} }
fn make_simple_change(&mut self, change:TextChange) { fn make_simple_change(&mut self, change:Change) {
let line_index = change.replaced.start.line; let line_index = change.replaced.start.line;
let new_content = change.lines.first().unwrap(); let empty_line = default();
let new_content = change.inserted.first().unwrap_or(&empty_line);
let range = change.replaced.start.column..change.replaced.end.column; let range = change.replaced.start.column..change.replaced.end.column;
self.lines[line_index].modify().splice(range,new_content.iter().cloned()); self.lines[line_index].modify().splice(range,new_content.iter().cloned());
self.dirty_lines.add_single_line(line_index); self.dirty_lines.add_single_line(line_index);
} }
fn make_multiline_change(&mut self, mut change:TextChange) { fn make_multiline_change(&mut self, mut change:Change) {
self.mix_content_into_change(&mut change); self.mix_content_into_change(&mut change);
let start_line = change.replaced.start.line; let start_line = change.replaced.start.line;
let end_line = change.replaced.end.line; let end_line = change.replaced.end.line;
let replaced_lines_count = end_line - start_line + 1; let replaced_lines_count = end_line - start_line + 1;
let inserted_lines_count = change.lines.len(); let inserted_lines_count = change.inserted.len();
let inserted_lines = change.lines.drain(0..change.lines.len()).map(Line::new_raw); let inserted_lines = change.inserted.drain(0..inserted_lines_count).map(Line::new_raw);
self.lines.splice(start_line..=end_line,inserted_lines); self.lines.splice(start_line..=end_line,inserted_lines);
if replaced_lines_count != inserted_lines_count { if replaced_lines_count != inserted_lines_count {
self.dirty_lines.add_lines_range_from(start_line..); self.dirty_lines.add_lines_range_from(start_line..);
@ -275,25 +346,28 @@ impl TextFieldContent {
/// This is for convenience of making multiline content changes. After mixing existing content /// This is for convenience of making multiline content changes. After mixing existing content
/// into change we can just operate on whole lines (replace the whole lines of current content /// into change we can just operate on whole lines (replace the whole lines of current content
/// with the whole lines-to-insert in change description). /// with the whole lines-to-insert in change description).
fn mix_content_into_change(&mut self, change:&mut TextChange) { fn mix_content_into_change(&mut self, change:&mut Change) {
if change.inserted.is_empty() {
change.inserted.push(default())
}
self.mix_first_edited_line_into_change(change); self.mix_first_edited_line_into_change(change);
self.mix_last_edited_line_into_change(change); self.mix_last_edited_line_into_change(change);
} }
fn mix_first_edited_line_into_change(&self, change:&mut TextChange) { fn mix_first_edited_line_into_change(&self, change:&mut Change) {
let first_line = change.replaced.start.line; let first_line = change.replaced.start.line;
let replace_from = change.replaced.start.column; let replace_from = change.replaced.start.column;
let first_edited = &self.lines[first_line].chars(); let first_edited = &self.lines[first_line].chars();
let prefix = &first_edited[..replace_from]; let prefix = &first_edited[..replace_from];
change.lines.first_mut().unwrap().splice(0..0,prefix.iter().cloned()); change.inserted.first_mut().unwrap().splice(0..0, prefix.iter().cloned());
} }
fn mix_last_edited_line_into_change(&mut self, change:&mut TextChange) { fn mix_last_edited_line_into_change(&mut self, change:&mut Change) {
let last_line = change.replaced.end.line; let last_line = change.replaced.end.line;
let replace_to = change.replaced.end.column; let replace_to = change.replaced.end.column;
let last_edited = &self.lines[last_line].chars(); let last_edited = &self.lines[last_line].chars();
let suffix = &last_edited[replace_to..]; let suffix = &last_edited[replace_to..];
change.lines.last_mut().unwrap().extend_from_slice(suffix); change.inserted.last_mut().unwrap().extend_from_slice(suffix);
} }
} }
@ -368,9 +442,9 @@ pub(crate) mod test {
let delete_from = TextLocation {line:1, column:0}; let delete_from = TextLocation {line:1, column:0};
let delete_to = TextLocation {line:1, column:4}; let delete_to = TextLocation {line:1, column:4};
let deleted_range = delete_from..delete_to; let deleted_range = delete_from..delete_to;
let insert = TextChange::insert(TextLocation {line:1, column:1}, "ab"); let insert = Change::insert(TextLocation {line:1, column:1}, "ab");
let delete = TextChange::delete(deleted_range.clone()); let delete = Change::delete(deleted_range.clone());
let replace = TextChange::replace(deleted_range, "text"); let replace = Change::replace(deleted_range, "text");
let mut content = TextFieldContent::new(text,&mock_properties()); let mut content = TextFieldContent::new(text,&mock_properties());
@ -399,9 +473,9 @@ pub(crate) mod test {
let begin_loc = TextLocation {line:0, column:0}; let begin_loc = TextLocation {line:0, column:0};
let middle_loc = TextLocation {line:1, column:2}; let middle_loc = TextLocation {line:1, column:2};
let end_loc = TextLocation {line:2, column:6}; let end_loc = TextLocation {line:2, column:6};
let insert_at_begin = TextChange::insert(begin_loc ,inserted); let insert_at_begin = Change::insert(begin_loc, inserted);
let insert_in_middle = TextChange::insert(middle_loc,inserted); let insert_in_middle = Change::insert(middle_loc, inserted);
let insert_at_end = TextChange::insert(end_loc ,inserted); let insert_at_end = Change::insert(end_loc, inserted);
let mut content = TextFieldContent::new(text,&mock_properties()); let mut content = TextFieldContent::new(text,&mock_properties());
@ -436,7 +510,7 @@ pub(crate) mod test {
let delete_from = TextLocation {line:0, column:2}; let delete_from = TextLocation {line:0, column:2};
let delete_to = TextLocation {line:2, column:3}; let delete_to = TextLocation {line:2, column:3};
let deleted_range = delete_from..delete_to; let deleted_range = delete_from..delete_to;
let delete = TextChange::delete(deleted_range); let delete = Change::delete(deleted_range);
let mut content = TextFieldContent::new(text,&mock_properties()); let mut content = TextFieldContent::new(text,&mock_properties());
content.apply_change(delete); content.apply_change(delete);
@ -470,8 +544,8 @@ pub(crate) mod test {
let two_lines = "Two\nlines"; let two_lines = "Two\nlines";
let replaced_range = TextLocation{line:1,column:2}..TextLocation{line:2,column:2}; let replaced_range = TextLocation{line:1,column:2}..TextLocation{line:2,column:2};
let one_line_change = TextChange::replace(replaced_range.clone(),one_line); let one_line_change = Change::replace(replaced_range.clone(), one_line);
let two_lines_change = TextChange::replace(replaced_range.clone(),two_lines); let two_lines_change = Change::replace(replaced_range.clone(), two_lines);
let one_line_expected = replaced_range.start..TextLocation{line:1, column:10}; let one_line_expected = replaced_range.start..TextLocation{line:1, column:10};
let two_lines_expected = replaced_range.start..TextLocation{line:2, column:5 }; let two_lines_expected = replaced_range.start..TextLocation{line:2, column:5 };
@ -485,20 +559,20 @@ pub(crate) mod test {
let text = "First\nSecond\nThird"; let text = "First\nSecond\nThird";
let mut content = TextFieldContent::new(text,&mock_properties()); let mut content = TextFieldContent::new(text,&mock_properties());
assert_eq!(0 , content.convert_location_to_char_index(TextLocation{line:0, column:0})); assert_eq!(0 ,content.convert_location_to_char_index(TextLocation{line:0, column:0}).value);
assert_eq!(2 , content.convert_location_to_char_index(TextLocation{line:0, column:2})); assert_eq!(2 ,content.convert_location_to_char_index(TextLocation{line:0, column:2}).value);
assert_eq!(5 , content.convert_location_to_char_index(TextLocation{line:0, column:5})); assert_eq!(5 ,content.convert_location_to_char_index(TextLocation{line:0, column:5}).value);
assert_eq!(6 , content.convert_location_to_char_index(TextLocation{line:1, column:0})); assert_eq!(6 ,content.convert_location_to_char_index(TextLocation{line:1, column:0}).value);
assert_eq!(12, content.convert_location_to_char_index(TextLocation{line:1, column:6})); assert_eq!(12,content.convert_location_to_char_index(TextLocation{line:1, column:6}).value);
assert_eq!(13, content.convert_location_to_char_index(TextLocation{line:2, column:0})); assert_eq!(13,content.convert_location_to_char_index(TextLocation{line:2, column:0}).value);
assert_eq!(18, content.convert_location_to_char_index(TextLocation{line:2, column:5})); assert_eq!(18,content.convert_location_to_char_index(TextLocation{line:2, column:5}).value);
let removed_range = TextLocation{line:1, column:1}..TextLocation{line:1, column:3}; let removed_range = TextLocation{line:1, column:1}..TextLocation{line:1, column:3};
let change = TextChange::delete(removed_range); let change = Change::delete(removed_range);
content.apply_change(change); content.apply_change(change);
assert_eq!(10, content.convert_location_to_char_index(TextLocation{line:1, column:4})); assert_eq!(10,content.convert_location_to_char_index(TextLocation{line:1, column:4}).value);
assert_eq!(16, content.convert_location_to_char_index(TextLocation{line:2, column:5})); assert_eq!(16,content.convert_location_to_char_index(TextLocation{line:2, column:5}).value);
} }
fn get_lines_as_strings(content:&TextFieldContent) -> Vec<String> { fn get_lines_as_strings(content:&TextFieldContent) -> Vec<String> {

View File

@ -1,7 +1,7 @@
//! Module with content regarding location in text (e.g. location of cursors or changes etc.) //! Module with content regarding location in text (e.g. location of cursors or changes etc.)
use crate::display::shape::text::text_field::content::Change;
use data::text::TextLocation; use data::text::TextLocation;
use data::text::TextChange;
use std::ops::Range; use std::ops::Range;
@ -44,7 +44,7 @@ impl TextLocationChange {
} }
/// Add a new text change into consideration. /// Add a new text change into consideration.
pub fn add_change(&mut self, change:&TextChange) { pub fn add_change(&mut self, change:&Change) {
let removed = &change.replaced; let removed = &change.replaced;
let inserted = change.inserted_text_range(); let inserted = change.inserted_text_range();
let lines_removed = (removed.end.line - removed.start.line) as isize; let lines_removed = (removed.end.line - removed.start.line) as isize;
@ -99,42 +99,42 @@ mod test {
// initial change // initial change
let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 2}; let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 2};
location_change.add_change(&TextChange::replace(replaced,one_line)); location_change.add_change(&Change::replace(replaced,one_line));
assert_eq!(0, location_change.line_offset); assert_eq!(0, location_change.line_offset);
assert_eq!(2, location_change.last_changed_line); assert_eq!(2, location_change.last_changed_line);
assert_eq!(8, location_change.column_offset); assert_eq!(8, location_change.column_offset);
// single line in the same line as previous // single line in the same line as previous
let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 13}; let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 13};
location_change.add_change(&TextChange::replace(replaced,one_line)); location_change.add_change(&Change::replace(replaced,one_line));
assert_eq!(0, location_change.line_offset); assert_eq!(0, location_change.line_offset);
assert_eq!(2, location_change.last_changed_line); assert_eq!(2, location_change.last_changed_line);
assert_eq!(5, location_change.column_offset); assert_eq!(5, location_change.column_offset);
// single -> multiple lines in the same line as previous // single -> multiple lines in the same line as previous
let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 8}; let replaced = TextLocation{line:2, column:2}..TextLocation{line:2, column: 8};
location_change.add_change(&TextChange::replace(replaced,two_lines)); location_change.add_change(&Change::replace(replaced,two_lines));
assert_eq!(1, location_change.line_offset); assert_eq!(1, location_change.line_offset);
assert_eq!(3, location_change.last_changed_line); assert_eq!(3, location_change.last_changed_line);
assert_eq!(2, location_change.column_offset); assert_eq!(2, location_change.column_offset);
// multiple -> multiple lines // multiple -> multiple lines
let replaced = TextLocation{line:3, column:2}..TextLocation{line:7, column: 3}; let replaced = TextLocation{line:3, column:2}..TextLocation{line:7, column: 3};
location_change.add_change(&TextChange::replace(replaced,two_lines)); location_change.add_change(&Change::replace(replaced,two_lines));
assert_eq!(-2, location_change.line_offset); assert_eq!(-2, location_change.line_offset);
assert_eq!(4 , location_change.last_changed_line); assert_eq!(4 , location_change.last_changed_line);
assert_eq!(2 , location_change.column_offset); assert_eq!(2 , location_change.column_offset);
// multiple -> single lines in the same line as previous // multiple -> single lines in the same line as previous
let replaced = TextLocation{line:4, column:2}..TextLocation{line:5, column: 11}; let replaced = TextLocation{line:4, column:2}..TextLocation{line:5, column: 11};
location_change.add_change(&TextChange::replace(replaced,one_line)); location_change.add_change(&Change::replace(replaced,one_line));
assert_eq!(-3, location_change.line_offset); assert_eq!(-3, location_change.line_offset);
assert_eq!(4 , location_change.last_changed_line); assert_eq!(4 , location_change.last_changed_line);
assert_eq!(-1, location_change.column_offset); assert_eq!(-1, location_change.column_offset);
// single line in other line than previous // single line in other line than previous
let replaced = TextLocation{line:5, column:2}..TextLocation{line:5, column: 12}; let replaced = TextLocation{line:5, column:2}..TextLocation{line:5, column: 12};
location_change.add_change(&TextChange::replace(replaced,one_line)); location_change.add_change(&Change::replace(replaced,one_line));
assert_eq!(-3, location_change.line_offset); assert_eq!(-3, location_change.line_offset);
assert_eq!(5 , location_change.last_changed_line); assert_eq!(5 , location_change.last_changed_line);
assert_eq!(-2, location_change.column_offset); assert_eq!(-2, location_change.column_offset);

View File

@ -71,6 +71,7 @@ impl SubAssign for Size {
} }
} }
// === Span === // === Span ===
/// Strongly typed span into container with index and size. /// Strongly typed span into container with index and size.
@ -144,6 +145,7 @@ impl Sub for Index {
} }
} }
// === TextLocation === // === TextLocation ===
/// A position of character in a multiline text. /// A position of character in a multiline text.
@ -179,105 +181,56 @@ impl TextLocation {
// === Change === // === Change ===
// ============== // ==============
/// A change type /// A template for structure describing a text operation in one place.
#[derive(Copy,Clone,Debug)]
pub enum ChangeType {
/// A change where we replace fragment of one line with text without new lines.
SingleLine,
/// A multi-line change is a change which is not a single line change (see docs for SingleLine).
MultiLine
}
/// A structure describing a text operation in one place.
#[derive(Clone,Debug)]
pub struct TextChange {
/// Text fragment to be replaced. If we don't mean to remove any text, this should be an empty
/// range with start set at position there `lines` will be inserted (see `TextChange::insert`
/// definition).
pub replaced : Range<TextLocation>,
/// Lines to insert instead of replaced fragment.
pub lines : Vec<Vec<char>>,
}
impl TextChange {
/// Creates operation which inserts text at given position.
pub fn insert(at:TextLocation, text:&str) -> Self {
TextChange {
replaced : at..at,
lines : Self::mk_lines_as_char_vector(text)
}
}
/// Creates operation which deletes text at given range.
pub fn delete(range:Range<TextLocation>) -> Self {
TextChange {
replaced : range,
lines : vec![vec![]],
}
}
/// Creates operation which replaces text at given range with given string.
pub fn replace(replaced:Range<TextLocation>, text:&str) -> Self {
TextChange {replaced,
lines : Self::mk_lines_as_char_vector(text)
}
}
/// A type of this change. See `ChangeType` doc for details.
pub fn change_type(&self) -> ChangeType {
if self.lines.is_empty() {
panic!("Invalid change");
}
let is_one_line_modified = self.replaced.start.line == self.replaced.end.line;
let is_one_line_inserted = self.lines.len() == 1;
if is_one_line_modified && is_one_line_inserted {
ChangeType::SingleLine
} else {
ChangeType::MultiLine
}
}
/// Converts change representation to String.
pub fn inserted_string(&self) -> String {
self.lines.iter().map(|line| line.iter().collect::<String>()).join("\n")
}
/// Returns text location range where the inserted text will appear after making this change.
pub fn inserted_text_range(&self) -> Range<TextLocation> {
let start = self.replaced.start;
let end_line = start.line + self.lines.len().saturating_sub(1);
let last_line_len = self.lines.last().map_or(0, |l| l.len());
let end_column = if start.line == end_line {
start.column + last_line_len
} else {
last_line_len
};
start..TextLocation{line:end_line, column:end_column}
}
fn mk_lines_as_char_vector(text:&str) -> Vec<Vec<char>> {
split_to_lines(text).map(|s| s.chars().collect_vec()).collect()
}
}
// ===========================
// === Change Notification ===
// ===========================
/// A notification about text change.
/// ///
/// In essence, it's `TextChange` with some additional useful information. /// This is a generalized template, because we use different representation for both index
#[derive(Clone,Debug,Shrinkwrap)] /// (e.g. `Index` or `TextLocation`) and inserted content (it may be just String, but also e.g.
pub struct TextChangedNotification { /// Vec<char>, or Vec<Vec<char>> split by newlines).
/// A change which has occurred. #[derive(Clone,Debug)]
#[shrinkwrap(main_field)] pub struct TextChangeTemplate<Index,Content> {
pub change : TextChange, /// Text fragment to be replaced. If we don't mean to remove any text, this should be an empty
/// The replaced range as char positions from document begin, instead of row:column pairs. /// range with start set at position there `lines` will be inserted
pub replaced_chars : Range<usize>, /// (see `TextChangeTemplate::insert` definition).
pub replaced : Range<Index>,
/// Text which replaces fragment described in `replaced` field.
pub inserted: Content,
} }
/// The simplest change representation.
pub type TextChange = TextChangeTemplate<Index,String>;
// === Constructors ===
impl<Index:Copy,Content> TextChangeTemplate<Index,Content> {
/// Creates operation which inserts text at given position.
pub fn insert(at:Index, text:Content) -> Self {
TextChangeTemplate {
replaced : at..at,
inserted: text,
}
}
}
impl<Index,Content> TextChangeTemplate<Index,Content> {
/// Creates operation which replaces text at given range with given string.
pub fn replace(replaced:Range<Index>, text:Content) -> Self {
let inserted = text;
TextChangeTemplate {replaced,inserted}
}
}
impl<Index,Content:Default> TextChangeTemplate<Index,Content> {
/// Creates operation which deletes text at given range.
pub fn delete(range:Range<Index>) -> Self {
TextChangeTemplate {
replaced : range,
inserted : default(),
}
}
}
// ================= // =================
// === Utilities === // === Utilities ===

View File

@ -10,7 +10,5 @@ edition = "2018"
enso-prelude = { version = "0.1.0" , path = "../prelude" } enso-prelude = { version = "0.1.0" , path = "../prelude" }
basegl-system-web = { version = "0.1.0" , path = "../system/web" } basegl-system-web = { version = "0.1.0" , path = "../system/web" }
percent-encoding = { version = "2.1.0" } percent-encoding = { version = "2.1.0" }
rust-dense-bitset = "0.1.1" rust-dense-bitset = { version = "0.1.1" }
#TODO [ao] replace with official version once this will be merged and released: keyboard-types = { version = "0.5.0" }
#https://github.com/pyfisch/keyboard-types/pull/4
keyboard-types = { git = "https://github.com/farmaazon/keyboard-types" }

View File

@ -67,7 +67,7 @@ impl IdGenerator {
} }
/// Create a new IdGenerator counting from 0. /// Create a new IdGenerator counting from 0.
fn new() -> IdGenerator { pub fn new() -> IdGenerator {
IdGenerator::new_from(0) IdGenerator::new_from(0)
} }
@ -77,6 +77,11 @@ impl IdGenerator {
} }
} }
impl Default for IdGenerator {
fn default() -> Self {
Self::new()
}
}
// ============= // =============

View File

@ -17,6 +17,7 @@
pub mod text; pub mod text;
pub mod module; pub mod module;
pub mod notification;
pub mod project; pub mod project;
/// General-purpose `Result` supporting any `Error`-compatible failures. /// General-purpose `Result` supporting any `Error`-compatible failures.

View File

@ -8,16 +8,19 @@
use crate::prelude::*; use crate::prelude::*;
use crate::controller::FallibleResult; use crate::controller::FallibleResult;
use crate::controller::notification;
use crate::double_representation::text::apply_code_change_to_id_map; use crate::double_representation::text::apply_code_change_to_id_map;
use crate::executor::global::spawn;
use ast;
use ast::Ast; use ast::Ast;
use ast::HasRepr; use ast::HasRepr;
use ast::IdMap; use ast::IdMap;
use data::text::Index;
use data::text::Size;
use data::text::Span; use data::text::Span;
use data::text::TextChangedNotification; use data::text::TextChange;
use file_manager_client as fmc; use file_manager_client as fmc;
use flo_stream::MessagePublisher;
use flo_stream::Subscriber;
use json_rpc::error::RpcError; use json_rpc::error::RpcError;
use parser::api::IsParser; use parser::api::IsParser;
use parser::Parser; use parser::Parser;
@ -76,8 +79,13 @@ shared! { Handle
id_map: IdMap, id_map: IdMap,
/// The File Manager Client handle. /// The File Manager Client handle.
file_manager: fmc::Handle, file_manager: fmc::Handle,
/// The Parser handle /// The Parser handle.
parser: Parser, parser: Parser,
/// Publisher of "text changed" notifications
text_notifications : notification::Publisher<notification::Text>,
/// Publisher of "graph changed" notifications
graph_notifications : notification::Publisher<notification::Graph>,
/// The logger handle.
logger: Logger, logger: Logger,
} }
@ -88,17 +96,18 @@ shared! { Handle
} }
/// Updates AST after code change. /// Updates AST after code change.
pub fn apply_code_change(&mut self,change:&TextChangedNotification) -> FallibleResult<()> { pub fn apply_code_change(&mut self,change:&TextChange) -> FallibleResult<()> {
let mut code = self.code(); let mut code = self.code();
let replaced_range = change.replaced_chars.clone(); let replaced_size = change.replaced.end - change.replaced.start;
let inserted_string = change.inserted_string(); let replaced_span = Span::new(change.replaced.start,replaced_size);
let replaced_size = Size::new(replaced_range.end - replaced_range.start); let replaced_indices = change.replaced.start.value..change.replaced.end.value;
let replaced_span = Span::new(Index::new(replaced_range.start),replaced_size);
code.replace_range(replaced_range,&inserted_string); code.replace_range(replaced_indices,&change.inserted);
apply_code_change_to_id_map(&mut self.id_map,&replaced_span,&inserted_string); apply_code_change_to_id_map(&mut self.id_map,&replaced_span,&change.inserted);
self.ast = self.parser.parse(code, self.id_map.clone())?; let ast = self.parser.parse(code, self.id_map.clone())?;
self.update_ast(ast);
self.logger.trace(|| format!("Applied change; Ast is now {:?}", self.ast)); self.logger.trace(|| format!("Applied change; Ast is now {:?}", self.ast));
Ok(()) Ok(())
} }
@ -119,6 +128,16 @@ shared! { Handle
} }
Ok(()) Ok(())
} }
/// Get subscriber receiving notifications about changes in module's text representation.
pub fn subscribe_text_notifications(&mut self) -> Subscriber<notification::Text> {
self.text_notifications.subscribe()
}
/// Get subscriber receiving notifications about changes in module's graph representation.
pub fn subscribe_graph_notifications(&mut self) -> Subscriber<notification::Graph> {
self.graph_notifications.subscribe()
}
} }
} }
@ -126,20 +145,38 @@ impl Handle {
/// Create a module controller for given location. /// Create a module controller for given location.
/// ///
/// It may wait for module content, because the module must initialize its state. /// It may wait for module content, because the module must initialize its state.
pub async fn new(location:Location, mut file_manager:fmc::Handle, mut parser:Parser) pub async fn new(location:Location, file_manager:fmc::Handle, parser:Parser)
-> FallibleResult<Self> { -> FallibleResult<Self> {
let logger = Logger::new(format!("Module Controller {}", location)); let logger = Logger::new(format!("Module Controller {}", location));
let ast = Ast::new(ast::Module{lines:default()},None);
let id_map = default();
let text_notifications = default();
let graph_notifications = default();
let data = Controller {location,ast,file_manager,parser,id_map,logger,text_notifications,
graph_notifications};
let handle = Handle::new_from_data(data);
handle.load_file().await?;
Ok(handle)
}
/// Load or reload module content from file.
pub async fn load_file(&self) -> FallibleResult<()> {
let (logger,path,mut fm,mut parser) = self.with_borrowed(|data| {
( data.logger.clone()
, data.location.to_path()
, data.file_manager.clone_ref()
, data.parser.clone_ref()
)
});
logger.info(|| "Loading module file"); logger.info(|| "Loading module file");
let path = location.to_path(); let content = fm.read(path).await?;
file_manager.touch(path.clone()).await?;
let content = file_manager.read(path).await?;
logger.info(|| "Parsing code"); logger.info(|| "Parsing code");
let ast = parser.parse(content,default())?; let ast = parser.parse(content,default())?;
logger.info(|| "Code parsed"); logger.info(|| "Code parsed");
logger.trace(|| format!("The parsed ast is {:?}", ast)); logger.trace(|| format!("The parsed ast is {:?}", ast));
let id_map = default(); self.with_borrowed(|data| data.update_ast(ast));
let data = Controller {location,ast,file_manager,parser,id_map,logger}; Ok(())
Ok(Handle::new_from_data(data))
} }
/// Save the module to file. /// Save the module to file.
@ -152,35 +189,49 @@ impl Handle {
} }
#[cfg(test)] #[cfg(test)]
fn new_mock pub fn new_mock
(location:Location, code:&str, id_map:IdMap, file_manager:fmc::Handle, mut parser:Parser) (location:Location, code:&str, id_map:IdMap, file_manager:fmc::Handle, mut parser:Parser)
-> FallibleResult<Self> { -> FallibleResult<Self> {
let logger = Logger::new("Mocked Module Controller"); let logger = Logger::new("Mocked Module Controller");
let ast = parser.parse(code.to_string(),id_map.clone())?; let ast = parser.parse(code.to_string(),id_map.clone())?;
let data = Controller {location,ast,file_manager,parser,id_map,logger}; let text_notifications = default();
let graph_notifications = default();
let data = Controller {location,ast,file_manager,parser,id_map,logger,
text_notifications,graph_notifications};
Ok(Handle::new_from_data(data)) Ok(Handle::new_from_data(data))
} }
} }
impl Controller {
/// Update current ast in module controller and emit notification about overall invalidation.
fn update_ast(&mut self,ast:Ast) {
self.ast = ast;
let text_change = notification::Text::Invalidate;
let graph_change = notification::Graph::Invalidate;
let code_notify = self.text_notifications.publish(text_change);
let graph_notify = self.graph_notifications.publish(graph_change);
spawn(async move { futures::join!(code_notify,graph_notify); });
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::controller::notification;
use crate::executor::test_utils::TestWithLocalPoolExecutor;
use ast; use ast;
use ast::BlockLine; use ast::BlockLine;
use data::text::Index; use data::text::Index;
use data::text::Span; use data::text::Span;
use data::text::Size; use data::text::Size;
use data::text::TextChange; use file_manager_client::Path;
use data::text::TextLocation;
use json_rpc::test_util::transport::mock::MockTransport; use json_rpc::test_util::transport::mock::MockTransport;
use parser::Parser; use parser::Parser;
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen_test::wasm_bindgen_test; use wasm_bindgen_test::wasm_bindgen_test;
use file_manager_client::Path;
#[test] #[test]
fn get_location_from_path() { fn get_location_from_path() {
@ -194,6 +245,7 @@ mod test {
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn update_ast_after_text_change() { fn update_ast_after_text_change() {
TestWithLocalPoolExecutor::set_up().run_test(async {
let transport = MockTransport::new(); let transport = MockTransport::new();
let file_manager = file_manager_client::Handle::new(transport); let file_manager = file_manager_client::Handle::new(transport);
let parser = Parser::new().unwrap(); let parser = Parser::new().unwrap();
@ -211,11 +263,11 @@ mod test {
let controller = Handle::new_mock(location,code,id_map,file_manager,parser).unwrap(); let controller = Handle::new_mock(location,code,id_map,file_manager,parser).unwrap();
let mut text_notifications = controller.subscribe_text_notifications();
let mut graph_notifications = controller.subscribe_graph_notifications();
// Change code from "2+2" to "22+2" // Change code from "2+2" to "22+2"
let change = TextChangedNotification { let change = TextChange::insert(Index::new(1),"2".to_string());
change : TextChange::insert(TextLocation{line:0,column:1}, "2"),
replaced_chars: 1..1
};
controller.apply_code_change(&change).unwrap(); controller.apply_code_change(&change).unwrap();
let expected_ast = Ast::new(ast::Module { let expected_ast = Ast::new(ast::Module {
lines: vec![BlockLine { lines: vec![BlockLine {
@ -230,5 +282,10 @@ mod test {
}] }]
}, None); }, None);
assert_eq!(expected_ast, controller.with_borrowed(|data| data.ast.clone())); assert_eq!(expected_ast, controller.with_borrowed(|data| data.ast.clone()));
// Check emitted notifications
assert_eq!(Some(notification::Text::Invalidate ), text_notifications.next().await );
assert_eq!(Some(notification::Graph::Invalidate), graph_notifications.next().await);
});
} }
} }

View File

@ -0,0 +1,61 @@
//! Here are the common structures used by Controllers notifications (sent between controllers and
//! from controller to view).
use crate::prelude::*;
// ===============
// === Commons ===
// ===============
/// A buffer size for notification publisher.
///
/// If Publisher buffer will be full, the thread sending next notification will be blocked until
/// all subscribers read message from buffer. We don't expect much traffic on file notifications,
/// therefore there is no need for setting big buffers.
const NOTIFICATION_BUFFER_SIZE : usize = 36;
/// A notification publisher which implements Debug and Default.
#[derive(Shrinkwrap)]
#[shrinkwrap(mutable)]
pub struct Publisher<Message>(pub flo_stream::Publisher<Message>);
impl<Message:Clone> Default for Publisher<Message> {
fn default() -> Self {
Self(flo_stream::Publisher::new(NOTIFICATION_BUFFER_SIZE))
}
}
impl<Message:'static> Debug for Publisher<Message> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "notification::Publisher<{:?}>", std::any::TypeId::of::<Message>())
}
}
// =====================================
// === Double Representation Changes ===
// =====================================
// === Text ===
/// A notification about changes of text representation or plain text file content.
#[derive(Copy,Clone,Debug,Eq,PartialEq)]
pub enum Text {
/// The content should be fully reloaded.
Invalidate,
}
// === Graph ===
/// A notification about changes of graph representation of a module.
#[derive(Copy,Clone,Debug,Eq,PartialEq)]
pub enum Graph {
/// The content should be fully reloaded.
Invalidate,
}

View File

@ -151,30 +151,25 @@ impl Handle {
mod test { mod test {
use super::*; use super::*;
use crate::executor::global::spawn; use crate::executor::test_utils::TestWithLocalPoolExecutor;
use crate::executor::global::set_spawner; use crate::transport::test_utils::TestWithMockedTransport;
use file_manager_client::Path; use file_manager_client::Path;
use json_rpc::test_util::transport::mock::MockTransport; use json_rpc::test_util::transport::mock::MockTransport;
use futures::executor::LocalPool;
use wasm_bindgen_test::wasm_bindgen_test; use wasm_bindgen_test::wasm_bindgen_test;
use wasm_bindgen_test::wasm_bindgen_test_configure; use wasm_bindgen_test::wasm_bindgen_test_configure;
wasm_bindgen_test_configure!(run_in_browser); wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn obtain_module_controller() { fn obtain_module_controller() {
let mut executor = LocalPool::new(); let transport = MockTransport::new();
let finished = Rc::new(RefCell::new(false)); let mut test = TestWithMockedTransport::set_up(&transport);
let finished_clone = finished.clone_ref(); test.run_test(async move {
let mut transport = MockTransport::new(); let project_ctrl = controller::project::Handle::new_running(transport);
let transport_clone = transport.clone_ref();
set_spawner(executor.spawner());
spawn(async move {
let project_ctrl = controller::project::Handle::new_running(transport_clone);
let location = controller::module::Location("TestLocation".to_string()); let location = controller::module::Location("TestLocation".to_string());
let another_loc = controller::module::Location("TestLocation2".to_string()); let another_loc = controller::module::Location("TestLocation2".to_string());
@ -185,48 +180,16 @@ mod test {
assert_eq!(location , module_ctrl .location()); assert_eq!(location , module_ctrl .location());
assert_eq!(another_loc, another_module_ctrl.location()); assert_eq!(another_loc, another_module_ctrl.location());
assert!(module_ctrl.identity_equals(&same_module_ctrl)); assert!(module_ctrl.identity_equals(&same_module_ctrl));
*finished_clone.borrow_mut() = true;
}); });
// Load module (touch + read content)
executor.run_until_stalled(); test.when_stalled_send_response("2 + 2");
transport.mock_peer_message_text(r#"{ test.when_stalled_send_response("3+3");
"jsonrpc" : "2.0",
"id" : 0,
"result" : null
}"#);
executor.run_until_stalled();
transport.mock_peer_message_text(r#"{
"jsonrpc" : "2.0",
"id" : 1,
"result" :"2 + 2"
}"#);
// Load Another Module (touch + read content)
executor.run_until_stalled();
transport.mock_peer_message_text(r#"{
"jsonrpc" : "2.0",
"id" : 2,
"result" : null
}"#);
executor.run_until_stalled();
transport.mock_peer_message_text(r#"{
"jsonrpc" : "2.0",
"id" : 3,
"result" :"3+3"
}"#);
// Check test reach its end
executor.run_until_stalled();
assert!(*finished.borrow());
} }
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn obtain_plain_text_controller() { fn obtain_plain_text_controller() {
let mut executor = LocalPool::new();
let finished = Rc::new(RefCell::new(false));
let finished_clone = finished.clone_ref();
let transport = MockTransport::new(); let transport = MockTransport::new();
set_spawner(executor.spawner()); TestWithLocalPoolExecutor::set_up().run_test(async move {
spawn(async move {
let project_ctrl = controller::project::Handle::new_running(transport); let project_ctrl = controller::project::Handle::new_running(transport);
let file_manager_handle = project_ctrl.file_manager(); let file_manager_handle = project_ctrl.file_manager();
let path = Path("TestPath".to_string()); let path = Path("TestPath".to_string());
@ -241,42 +204,20 @@ mod test {
assert_eq!(path , text_ctrl .file_path() ); assert_eq!(path , text_ctrl .file_path() );
assert_eq!(another_path, another_txt_ctrl.file_path() ); assert_eq!(another_path, another_txt_ctrl.file_path() );
assert!(text_ctrl.identity_equals(&same_text_ctrl)); assert!(text_ctrl.identity_equals(&same_text_ctrl));
*finished_clone.borrow_mut() = true;
}); });
executor.run_until_stalled();
assert!(*finished.borrow());
} }
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn obtain_text_controller_for_module() { fn obtain_text_controller_for_module() {
let mut executor = LocalPool::new(); let transport = MockTransport::new();
let finished = Rc::new(RefCell::new(false)); let mut test = TestWithMockedTransport::set_up(&transport);
let finished_clone = finished.clone_ref(); test.run_test(async move {
let mut transport = MockTransport::new(); let project_ctrl = controller::project::Handle::new_running(transport);
let transport_clone = transport.clone_ref();
set_spawner(executor.spawner());
spawn(async move {
let project_ctrl = controller::project::Handle::new_running(transport_clone);
let path = controller::module::Location("test".to_string()).to_path(); let path = controller::module::Location("test".to_string()).to_path();
let text_ctrl = project_ctrl.get_text_controller(path.clone()).await.unwrap(); let text_ctrl = project_ctrl.get_text_controller(path.clone()).await.unwrap();
let content = text_ctrl.read_content().await.unwrap(); let content = text_ctrl.read_content().await.unwrap();
assert_eq!("2 + 2", content.as_str()); assert_eq!("2 + 2", content.as_str());
*finished_clone.borrow_mut() = true;
}); });
executor.run_until_stalled(); test.when_stalled_send_response("2 + 2");
transport.mock_peer_message_text(r#"{
"jsonrpc" : "2.0",
"id" : 0,
"result" : null
}"#);
executor.run_until_stalled();
transport.mock_peer_message_text(r#"{
"jsonrpc" : "2.0",
"id" : 1,
"result" :"2 + 2"
}"#);
executor.run_until_stalled();
assert!(*finished.borrow());
} }
} }

View File

@ -6,36 +6,16 @@
use crate::prelude::*; use crate::prelude::*;
use crate::controller::FallibleResult; use crate::controller::FallibleResult;
use crate::controller::notification;
use crate::executor::global::spawn;
use data::text::TextChangedNotification; use data::text::TextChange;
use failure::_core::fmt::Formatter;
use failure::_core::fmt::Error;
use file_manager_client as fmc; use file_manager_client as fmc;
use flo_stream::MessagePublisher; use flo_stream::MessagePublisher;
use flo_stream::Publisher;
use flo_stream::Subscriber; use flo_stream::Subscriber;
use json_rpc::error::RpcError; use json_rpc::error::RpcError;
use shapely::shared; use shapely::shared;
use utils::channel::process_stream_with_handle;
// ====================
// === Notification ===
// ====================
/// A buffer size for notification publisher.
///
/// If Publisher buffer will be full, the thread sending next notification will be blocked until
/// all subscribers read message from buffer. We don't expect much traffic on file notifications,
/// therefore there is no need for setting big buffers.
const NOTIFICATION_BUFFER_SIZE : usize = 36;
/// A notification from TextController.
#[derive(Clone,Debug)]
pub enum Notification {
/// File contents needs to be set to the following due to synchronization with external state.
SetNewContent(String),
}
// ======================= // =======================
@ -56,16 +36,17 @@ enum FileHandle {
shared! { Handle shared! { Handle
/// Data stored by the text controller. /// Data stored by the text controller.
#[derive(Debug)]
pub struct Controller { pub struct Controller {
file: FileHandle, file: FileHandle,
/// Sink where we put events to be consumed by the view. /// Sink where we put events to be consumed by the view.
notification_publisher: Publisher<Notification>, notifications: notification::Publisher<notification::Text>,
} }
impl { impl {
/// Get subscriber receiving controller's notifications. /// Get subscriber receiving controller's notifications.
pub fn subscribe(&mut self) -> Subscriber<Notification> { pub fn subscribe(&mut self) -> Subscriber<notification::Text> {
self.notification_publisher.subscribe() self.notifications.subscribe()
} }
/// Get clone of file path handled by this controller. /// Get clone of file path handled by this controller.
@ -85,7 +66,13 @@ impl Handle {
} }
/// Create controller managing Luna module file. /// Create controller managing Luna module file.
pub fn new_for_module(controller:controller::module::Handle) -> Self { pub fn new_for_module(controller:controller::module::Handle) -> Self {
Self::new(FileHandle::Module {controller}) let text_notifications = controller.subscribe_text_notifications();
let handle = Self::new(FileHandle::Module {controller});
let weak = handle.downgrade();
spawn(process_stream_with_handle(text_notifications,weak,|notification,this| {
this.with_borrowed(move |data| data.notifications.publish(notification))
}));
handle
} }
/// Read file's content. /// Read file's content.
@ -119,7 +106,7 @@ impl Handle {
/// This function should be called by view on every user interaction changing the text content /// This function should be called by view on every user interaction changing the text content
/// of file. It will e.g. update the Module Controller state and notify other views about /// of file. It will e.g. update the Module Controller state and notify other views about
/// update in case of module files. /// update in case of module files.
pub fn apply_text_change(&self, change:&TextChangedNotification) -> FallibleResult<()> { pub fn apply_text_change(&self, change:&TextChange) -> FallibleResult<()> {
if let FileHandle::Module {controller} = self.file_handle() { if let FileHandle::Module {controller} = self.file_handle() {
controller.apply_code_change(change) controller.apply_code_change(change)
} else { } else {
@ -136,7 +123,7 @@ impl Handle {
fn new(file_handle:FileHandle) -> Self { fn new(file_handle:FileHandle) -> Self {
let state = Controller { let state = Controller {
file : file_handle, file : file_handle,
notification_publisher : Publisher::new(NOTIFICATION_BUFFER_SIZE), notifications : default(),
}; };
Self {rc:Rc::new(RefCell::new(state))} Self {rc:Rc::new(RefCell::new(state))}
} }
@ -147,21 +134,6 @@ impl Handle {
} }
// === Debug implementations ===
impl Debug for Controller {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
write!(f,"Text Controller on {:?} }}",self.file)
}
}
impl Debug for Handle {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
self.rc.borrow().fmt(f)
}
}
// === Test Utilities === // === Test Utilities ===
#[cfg(test)] #[cfg(test)]
@ -177,3 +149,34 @@ impl Handle {
}) })
} }
} }
#[cfg(test)]
mod test {
use super::*;
use crate::executor::test_utils::TestWithLocalPoolExecutor;
use data::text::Index;
use json_rpc::test_util::transport::mock::MockTransport;
use parser::Parser;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test]
fn passing_notifications_from_modue() {
let mut test = TestWithLocalPoolExecutor::set_up();
test.run_test(async move {
let fm = file_manager_client::Handle::new(MockTransport::new());
let loc = controller::module::Location("test".to_string());
let parser = Parser::new().unwrap();
let module_res = controller::module::Handle::new_mock(loc,"2+2",default(),fm,parser);
let module = module_res.unwrap();
let controller = Handle::new_for_module(module.clone_ref());
let mut sub = controller.subscribe();
module.apply_code_change(&TextChange::insert(Index::new(1),"2".to_string())).unwrap();
assert_eq!(Some(notification::Text::Invalidate), sub.next().await);
})
}
}

View File

@ -3,3 +3,6 @@
pub mod web; pub mod web;
pub mod global; pub mod global;
#[cfg(test)]
pub mod test_utils;

View File

@ -0,0 +1,49 @@
use crate::prelude::*;
use futures::executor;
use crate::executor::global::set_spawner;
use crate::executor::global::spawn;
#[derive(Debug)]
pub struct TestWithLocalPoolExecutor {
executor : executor::LocalPool,
is_finished : Rc<Cell<bool>>,
}
impl TestWithLocalPoolExecutor {
pub fn set_up() -> Self {
let executor = executor::LocalPool::new();
let is_finished = Rc::new(Cell::new(false));
set_spawner(executor.spawner());
Self {executor,is_finished}
}
pub fn run_test<Test>(&mut self, test:Test)
where Test : Future<Output=()> + 'static {
let is_finished_clone = self.is_finished.clone_ref();
spawn(async move {
test.await;
is_finished_clone.set(true);
});
}
pub fn when_stalled<Callback>(&mut self, callback:Callback)
where Callback : FnOnce() {
self.executor.run_until_stalled();
if !self.is_finished.get() {
callback();
}
}
}
impl Drop for TestWithLocalPoolExecutor {
fn drop(&mut self) {
// We should be able to finish test.
self.executor.run_until_stalled();
assert!(self.is_finished.get());
}
}

View File

@ -1,3 +1,6 @@
//! Transport implementations used by the IDE. //! Transport implementations used by the IDE.
pub mod web; pub mod web;
#[cfg(test)]
pub mod test_utils;

View File

@ -0,0 +1,38 @@
use crate::prelude::*;
use crate::executor::test_utils::TestWithLocalPoolExecutor;
use json_rpc::test_util::transport::mock::MockTransport;
use serde::Serialize;
#[derive(Debug)]
pub struct TestWithMockedTransport {
with_executor_fixture : TestWithLocalPoolExecutor,
transport : MockTransport,
next_response_id : json_rpc::handler::IdGenerator,
}
impl TestWithMockedTransport {
pub fn set_up(transport:&MockTransport) -> Self {
Self {
with_executor_fixture : TestWithLocalPoolExecutor::set_up(),
transport : transport.clone_ref(),
next_response_id : json_rpc::handler::IdGenerator::new(),
}
}
pub fn run_test<TestBody>(&mut self, test:TestBody)
where TestBody : Future<Output=()> + 'static {
self.with_executor_fixture.run_test(test);
}
pub fn when_stalled_send_response(&mut self, result:impl Serialize) {
let mut transport = self.transport.clone_ref();
let id = self.next_response_id.generate();
let result = json_rpc::messages::Message::new_success(id,result);
self.with_executor_fixture.when_stalled(move ||
transport.mock_peer_message(result)
);
}
}

View File

@ -66,6 +66,10 @@ impl ProjectView {
pub async fn new(logger:&Logger, controller:controller::project::Handle) pub async fn new(logger:&Logger, controller:controller::project::Handle)
-> FallibleResult<Self> { -> FallibleResult<Self> {
let path = Path::new(INITIAL_FILE_PATH); let path = Path::new(INITIAL_FILE_PATH);
// This touch is to ensure, that our hardcoded module exists (so we don't require
// additional user/tester action to run IDE. It will be removed once we will support opening
// any module file.
controller.file_manager().touch(path.clone()).await?;
let text_controller = controller.get_text_controller(path).await?; let text_controller = controller.get_text_controller(path).await?;
let world = WorldData::new(&web::get_html_element_by_id("root").unwrap()); let world = WorldData::new(&web::get_html_element_by_id("root").unwrap());
let logger = logger.sub("ProjectView"); let logger = logger.sub("ProjectView");

View File

@ -10,11 +10,12 @@ use basegl::display::shape::text::glyph::font::FontRegistry;
use basegl::display::shape::text::text_field::TextField; use basegl::display::shape::text::text_field::TextField;
use basegl::display::shape::text::text_field::TextFieldProperties; use basegl::display::shape::text::text_field::TextFieldProperties;
use basegl::display::world::*; use basegl::display::world::*;
use data::text::TextChange;
use enso_frp::io::KeyboardActions; use enso_frp::io::KeyboardActions;
use enso_frp::io::KeyMask; use enso_frp::io::KeyMask;
use nalgebra::Vector2; use nalgebra::Vector2;
use nalgebra::zero; use nalgebra::zero;
use utils::channel::process_stream_with_handle;
// ================== // ==================
@ -79,19 +80,6 @@ impl TextEditor {
let text_size = 16.0; let text_size = 16.0;
let properties = TextFieldProperties {font,text_size,base_color,size}; let properties = TextFieldProperties {font,text_size,base_color,size};
let text_field = TextField::new(&world,properties); let text_field = TextField::new(&world,properties);
let text_field_weak = text_field.downgrade();
let controller_clone = controller.clone_ref();
let logger_ref = logger.clone();
executor::global::spawn(async move {
if let Ok(content) = controller_clone.read_content().await {
if let Some(text_field) = text_field_weak.upgrade() {
text_field.set_content(&content);
logger_ref.info("File loaded");
}
}
});
world.add_child(&text_field); world.add_child(&text_field);
let data = TextEditorData {controller,text_field,padding,position,size,logger}; let data = TextEditorData {controller,text_field,padding,position,size,logger};
@ -101,23 +89,14 @@ impl TextEditor {
fn initialize(self, keyboard_actions:&mut KeyboardActions) -> Self { fn initialize(self, keyboard_actions:&mut KeyboardActions) -> Self {
let save_keys = KeyMask::new_control_character('s'); let save_keys = KeyMask::new_control_character('s');
let text_editor = Rc::downgrade(&self.rc); let text_editor = Rc::downgrade(&self.rc);
keyboard_actions.set_action(save_keys,move |_| { keyboard_actions.set_action(save_keys,enclose!((text_editor) move |_| {
if let Some(text_editor) = text_editor.upgrade() { if let Some(text_editor) = text_editor.upgrade() {
text_editor.borrow().save(); text_editor.borrow().save();
} }
}); }));
self.with_borrowed(move |data| { self.setup_notifications();
let logger = data.logger.clone(); executor::global::spawn(self.reload_content());
let controller_clone = data.controller.clone_ref();
data.text_field.set_text_edit_callback(move |change| {
let result = controller_clone.apply_text_change(change);
if result.is_err() {
logger.error(|| "Error while notifying controllers about text change");
logger.error(|| format!("{:?}", result));
}
});
});
self.update(); self.update();
self self
} }
@ -128,6 +107,54 @@ impl TextEditor {
self.update(); self.update();
} }
/// Setup handlers for notification going from text field and from controller.
fn setup_notifications(&self) {
let weak = self.downgrade();
let text_field = self.with_borrowed(|data| data.text_field.clone_ref());
text_field.set_text_edit_callback(enclose!((weak) move |change| {
if let Some(this) = weak.upgrade() {
this.handle_text_field_notification(change);
}
}));
let notifications_sub = self.with_borrowed(|data| data.controller.subscribe());
executor::global::spawn(process_stream_with_handle(notifications_sub,weak,|notif,this| {
this.handle_controller_notification(notif)
}));
}
fn handle_controller_notification(&self, notification:controller::notification::Text)
-> impl Future<Output=()> {
match notification {
controller::notification::Text::Invalidate => self.reload_content()
}
}
fn handle_text_field_notification(&self, change:&TextChange) {
let (logger,controller) = self.with_borrowed(|data|
(data.logger.clone(),data.controller.clone_ref()));
let result = controller.apply_text_change(change);
if result.is_err() {
logger.error(|| "Error while notifying controllers about text change");
logger.error(|| format!("{:?}", result));
}
}
/// Reload the TextEditor content with data obtained from controller
fn reload_content(&self) -> impl Future<Output=()> {
let (logger,controller) = self.with_borrowed(|data|
(data.logger.clone(),data.controller.clone_ref()));
let weak = self.downgrade();
async move {
if let Ok(content) = controller.read_content().await {
if let Some(this) = weak.upgrade() {
this.with_borrowed(|data| data.text_field.set_content(&content));
logger.info("File loaded");
}
}
}
}
/// Updates the underlying display object, should be called after setting size or position. /// Updates the underlying display object, should be called after setting size or position.
fn update(&self) { fn update(&self) {
let data = self.rc.borrow_mut(); let data = self.rc.borrow_mut();

View File

@ -1,6 +1,9 @@
//! General-purpose code for dealing with mpsc channels. //! General-purpose code for dealing with futures channels, sinks and streams.
use crate::prelude::*;
use futures::channel::mpsc::UnboundedSender; use futures::channel::mpsc::UnboundedSender;
use futures::StreamExt;
/// Emit message using `UnboundedSender`. Does not care if there are listeners. /// Emit message using `UnboundedSender`. Does not care if there are listeners.
pub fn emit<T>(sender:&UnboundedSender<T>, message:T) { pub fn emit<T>(sender:&UnboundedSender<T>, message:T) {
@ -19,3 +22,24 @@ pub fn emit<T>(sender:&UnboundedSender<T>, message:T) {
} }
} }
} }
/// Process stream elements while handle exists.
///
/// This function will call `function` for each element received from `stream`, also providing
/// the strong version of `weak` as argument, until the stream ends or handle will no longer exists
/// (the `view` method of `WeakElement` returns `None`).
pub async fn process_stream_with_handle<Stream,Weak,Function,Future>
(mut stream:Stream, weak:Weak, mut function:Function)
where Stream : StreamExt + Unpin,
Weak : WeakElement,
Function : FnMut(Stream::Item,Weak::Strong) -> Future,
Future : std::future::Future<Output=()> {
loop {
let item_opt = stream.next().await;
let handle_opt = weak.view();
match (item_opt,handle_opt) {
(Some(item),Some(handle)) => function(item,handle).await,
_ => break,
}
}
}