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 = [
"basegl-system-web 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)",
"rust-dense-bitset 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1218,8 +1218,8 @@ dependencies = [
[[package]]
name = "keyboard-types"
version = "0.4.3"
source = "git+https://github.com/farmaazon/keyboard-types#c983ff5cba38a8b712a59dbd1a7406c4b1a3c17d"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"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)",
@ -3152,7 +3152,7 @@ dependencies = [
"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 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 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"

View File

@ -14,6 +14,7 @@ need the following setup:
rustup toolchain install nightly-2019-11-04 # Install the nightly channel.
rustup default nightly # Set it as the default one.
rustup component add clippy # Install the linter.
cargo install cargo-watch # To enable ./run watch utility
```
- **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.
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
[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/>

View File

@ -3,20 +3,19 @@
pub mod content;
pub mod cursor;
pub mod frp;
pub mod location;
pub mod render;
pub mod word_occurrence;
use crate::prelude::*;
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::cursor::Cursors;
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::Step;
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::word_occurrence::WordOccurrences;
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 data::text::TextChange;
use data::text::TextChangedNotification;
use data::text::TextLocation;
use nalgebra::Vector2;
use nalgebra::Vector3;
@ -85,7 +83,7 @@ shared! { TextField
frp : Option<TextFieldFrp>,
word_occurrences : Option<WordOccurrences>,
#[derivative(Debug="ignore")]
text_change_callback : Option<Box<dyn FnMut(&TextChangedNotification)>>
text_change_callback : Option<Box<dyn FnMut(&TextChange)>>
}
impl {
@ -173,16 +171,6 @@ shared! { TextField
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.
pub fn get_content(&self) -> 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.
/// 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) {
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
(&mut self, location_change:&mut TextLocationChange, cursor_id:CursorId, to_insert:&str)
-> TextChangedNotification {
-> TextChange {
let CursorId(id) = cursor_id;
let cursor = &mut self.cursors.cursors[id];
let replaced = location_change.apply_to_range(cursor.selection_range());
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);
*cursor = Cursor::new(change.inserted_text_range().end);
self.content.apply_change(change.clone());
TextChangedNotification {change,replaced_chars}
self.content.apply_change(change);
TextChange::replace(replaced_chars, to_insert.to_string())
}
}

View File

@ -1,5 +1,6 @@
//! Module with all structures describing the content of the TextField.
pub mod line;
pub mod location;
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::TextFieldProperties;
use data::text::ChangeType;
use data::text::TextChange;
use data::text;
use data::text::TextChangeTemplate;
use data::text::TextLocation;
use data::text::split_to_lines;
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 ===
@ -199,7 +269,7 @@ impl TextFieldContent {
/// 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.
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() {
self.line_offsets.push(0);
}
@ -211,13 +281,13 @@ impl TextFieldContent {
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
/// char's position from document begin.
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 end = self.convert_location_to_char_index(range.end);
start..end
@ -229,7 +299,7 @@ impl TextFieldContent {
impl TextFieldContent {
/// 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;
match change.change_type() {
ChangeType::SingleLine => self.make_simple_change(change),
@ -241,27 +311,28 @@ impl TextFieldContent {
}
/// Apply many changes to content.
pub fn apply_changes<Changes:IntoIterator<Item=TextChange>>(&mut self, changes:Changes) {
let change_key = |chg:&TextChange | chg.replaced.start;
pub fn apply_changes<Changes:IntoIterator<Item=Change>>(&mut self, changes:Changes) {
let change_key = |chg:&Change | chg.replaced.start;
let changes_vec = changes.into_iter().sorted_by_key(change_key);
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 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;
self.lines[line_index].modify().splice(range,new_content.iter().cloned());
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);
let start_line = change.replaced.start.line;
let end_line = change.replaced.end.line;
let replaced_lines_count = end_line - start_line + 1;
let inserted_lines_count = change.lines.len();
let inserted_lines = change.lines.drain(0..change.lines.len()).map(Line::new_raw);
let inserted_lines_count = change.inserted.len();
let inserted_lines = change.inserted.drain(0..inserted_lines_count).map(Line::new_raw);
self.lines.splice(start_line..=end_line,inserted_lines);
if replaced_lines_count != inserted_lines_count {
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
/// 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).
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_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 replace_from = change.replaced.start.column;
let first_edited = &self.lines[first_line].chars();
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 replace_to = change.replaced.end.column;
let last_edited = &self.lines[last_line].chars();
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_to = TextLocation {line:1, column:4};
let deleted_range = delete_from..delete_to;
let insert = TextChange::insert(TextLocation {line:1, column:1}, "ab");
let delete = TextChange::delete(deleted_range.clone());
let replace = TextChange::replace(deleted_range, "text");
let insert = Change::insert(TextLocation {line:1, column:1}, "ab");
let delete = Change::delete(deleted_range.clone());
let replace = Change::replace(deleted_range, "text");
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 middle_loc = TextLocation {line:1, column:2};
let end_loc = TextLocation {line:2, column:6};
let insert_at_begin = TextChange::insert(begin_loc ,inserted);
let insert_in_middle = TextChange::insert(middle_loc,inserted);
let insert_at_end = TextChange::insert(end_loc ,inserted);
let insert_at_begin = Change::insert(begin_loc, inserted);
let insert_in_middle = Change::insert(middle_loc, inserted);
let insert_at_end = Change::insert(end_loc, inserted);
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_to = TextLocation {line:2, column:3};
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());
content.apply_change(delete);
@ -470,8 +544,8 @@ pub(crate) mod test {
let two_lines = "Two\nlines";
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 two_lines_change = TextChange::replace(replaced_range.clone(),two_lines);
let one_line_change = Change::replace(replaced_range.clone(), one_line);
let two_lines_change = Change::replace(replaced_range.clone(), two_lines);
let one_line_expected = replaced_range.start..TextLocation{line:1, column:10};
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 mut content = TextFieldContent::new(text,&mock_properties());
assert_eq!(0 , content.convert_location_to_char_index(TextLocation{line:0, column:0}));
assert_eq!(2 , content.convert_location_to_char_index(TextLocation{line:0, column:2}));
assert_eq!(5 , content.convert_location_to_char_index(TextLocation{line:0, column:5}));
assert_eq!(6 , content.convert_location_to_char_index(TextLocation{line:1, column:0}));
assert_eq!(12, content.convert_location_to_char_index(TextLocation{line:1, column:6}));
assert_eq!(13, content.convert_location_to_char_index(TextLocation{line:2, column:0}));
assert_eq!(18, content.convert_location_to_char_index(TextLocation{line:2, column:5}));
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}).value);
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}).value);
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}).value);
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 change = TextChange::delete(removed_range);
let change = Change::delete(removed_range);
content.apply_change(change);
assert_eq!(10, content.convert_location_to_char_index(TextLocation{line:1, column:4}));
assert_eq!(16, content.convert_location_to_char_index(TextLocation{line:2, column:5}));
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}).value);
}
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.)
use crate::display::shape::text::text_field::content::Change;
use data::text::TextLocation;
use data::text::TextChange;
use std::ops::Range;
@ -44,7 +44,7 @@ impl TextLocationChange {
}
/// 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 inserted = change.inserted_text_range();
let lines_removed = (removed.end.line - removed.start.line) as isize;
@ -99,42 +99,42 @@ mod test {
// initial change
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!(2, location_change.last_changed_line);
assert_eq!(8, location_change.column_offset);
// single line in the same line as previous
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!(2, location_change.last_changed_line);
assert_eq!(5, location_change.column_offset);
// single -> multiple lines in the same line as previous
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!(3, location_change.last_changed_line);
assert_eq!(2, location_change.column_offset);
// multiple -> multiple lines
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!(4 , location_change.last_changed_line);
assert_eq!(2 , location_change.column_offset);
// multiple -> single lines in the same line as previous
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!(4 , location_change.last_changed_line);
assert_eq!(-1, location_change.column_offset);
// single line in other line than previous
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!(5 , location_change.last_changed_line);
assert_eq!(-2, location_change.column_offset);

View File

@ -71,6 +71,7 @@ impl SubAssign for Size {
}
}
// === Span ===
/// Strongly typed span into container with index and size.
@ -144,6 +145,7 @@ impl Sub for Index {
}
}
// === TextLocation ===
/// A position of character in a multiline text.
@ -179,105 +181,56 @@ impl TextLocation {
// === 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 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.
/// A template for structure describing a text operation in one place.
///
/// In essence, it's `TextChange` with some additional useful information.
#[derive(Clone,Debug,Shrinkwrap)]
pub struct TextChangedNotification {
/// A change which has occurred.
#[shrinkwrap(main_field)]
pub change : TextChange,
/// The replaced range as char positions from document begin, instead of row:column pairs.
pub replaced_chars : Range<usize>,
/// This is a generalized template, because we use different representation for both index
/// (e.g. `Index` or `TextLocation`) and inserted content (it may be just String, but also e.g.
/// Vec<char>, or Vec<Vec<char>> split by newlines).
#[derive(Clone,Debug)]
pub struct TextChangeTemplate<Index,Content> {
/// 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 `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 ===

View File

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

View File

@ -67,7 +67,7 @@ impl IdGenerator {
}
/// Create a new IdGenerator counting from 0.
fn new() -> IdGenerator {
pub fn new() -> IdGenerator {
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 module;
pub mod notification;
pub mod project;
/// General-purpose `Result` supporting any `Error`-compatible failures.

View File

@ -8,16 +8,19 @@
use crate::prelude::*;
use crate::controller::FallibleResult;
use crate::controller::notification;
use crate::double_representation::text::apply_code_change_to_id_map;
use crate::executor::global::spawn;
use ast;
use ast::Ast;
use ast::HasRepr;
use ast::IdMap;
use data::text::Index;
use data::text::Size;
use data::text::Span;
use data::text::TextChangedNotification;
use data::text::TextChange;
use file_manager_client as fmc;
use flo_stream::MessagePublisher;
use flo_stream::Subscriber;
use json_rpc::error::RpcError;
use parser::api::IsParser;
use parser::Parser;
@ -76,8 +79,13 @@ shared! { Handle
id_map: IdMap,
/// The File Manager Client handle.
file_manager: fmc::Handle,
/// The Parser handle
/// The Parser handle.
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,
}
@ -88,17 +96,18 @@ shared! { Handle
}
/// Updates AST after code change.
pub fn apply_code_change(&mut self,change:&TextChangedNotification) -> FallibleResult<()> {
let mut code = self.code();
let replaced_range = change.replaced_chars.clone();
let inserted_string = change.inserted_string();
let replaced_size = Size::new(replaced_range.end - replaced_range.start);
let replaced_span = Span::new(Index::new(replaced_range.start),replaced_size);
pub fn apply_code_change(&mut self,change:&TextChange) -> FallibleResult<()> {
let mut code = self.code();
let replaced_size = change.replaced.end - change.replaced.start;
let replaced_span = Span::new(change.replaced.start,replaced_size);
let replaced_indices = change.replaced.start.value..change.replaced.end.value;
code.replace_range(replaced_range,&inserted_string);
apply_code_change_to_id_map(&mut self.id_map,&replaced_span,&inserted_string);
self.ast = self.parser.parse(code, self.id_map.clone())?;
code.replace_range(replaced_indices,&change.inserted);
apply_code_change_to_id_map(&mut self.id_map,&replaced_span,&change.inserted);
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));
Ok(())
}
@ -119,6 +128,16 @@ shared! { Handle
}
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.
///
/// 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> {
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");
let path = location.to_path();
file_manager.touch(path.clone()).await?;
let content = file_manager.read(path).await?;
let content = fm.read(path).await?;
logger.info(|| "Parsing code");
let ast = parser.parse(content,default())?;
let ast = parser.parse(content,default())?;
logger.info(|| "Code parsed");
logger.trace(|| format!("The parsed ast is {:?}", ast));
let id_map = default();
let data = Controller {location,ast,file_manager,parser,id_map,logger};
Ok(Handle::new_from_data(data))
self.with_borrowed(|data| data.update_ast(ast));
Ok(())
}
/// Save the module to file.
@ -152,35 +189,49 @@ impl Handle {
}
#[cfg(test)]
fn new_mock
pub fn new_mock
(location:Location, code:&str, id_map:IdMap, file_manager:fmc::Handle, mut parser:Parser)
-> FallibleResult<Self> {
let logger = Logger::new("Mocked Module Controller");
let ast = parser.parse(code.to_string(),id_map.clone())?;
let data = Controller {location,ast,file_manager,parser,id_map,logger};
let logger = Logger::new("Mocked Module Controller");
let ast = parser.parse(code.to_string(),id_map.clone())?;
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))
}
}
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)]
mod test {
use super::*;
use crate::controller::notification;
use crate::executor::test_utils::TestWithLocalPoolExecutor;
use ast;
use ast::BlockLine;
use data::text::Index;
use data::text::Span;
use data::text::Size;
use data::text::TextChange;
use data::text::TextLocation;
use file_manager_client::Path;
use json_rpc::test_util::transport::mock::MockTransport;
use parser::Parser;
use uuid::Uuid;
use wasm_bindgen_test::wasm_bindgen_test;
use file_manager_client::Path;
#[test]
fn get_location_from_path() {
@ -194,41 +245,47 @@ mod test {
#[wasm_bindgen_test]
fn update_ast_after_text_change() {
let transport = MockTransport::new();
let file_manager = file_manager_client::Handle::new(transport);
let parser = Parser::new().unwrap();
let location = Location("Test".to_string());
TestWithLocalPoolExecutor::set_up().run_test(async {
let transport = MockTransport::new();
let file_manager = file_manager_client::Handle::new(transport);
let parser = Parser::new().unwrap();
let location = Location("Test".to_string());
let uuid1 = Uuid::new_v4();
let uuid2 = Uuid::new_v4();
let uuid3 = Uuid::new_v4();
let code = "2+2";
let id_map = IdMap(vec!
[ (Span::new(Index::new(0), Size::new(1)),uuid1.clone())
, (Span::new(Index::new(2), Size::new(1)),uuid2)
, (Span::new(Index::new(0), Size::new(3)),uuid3)
]);
let uuid1 = Uuid::new_v4();
let uuid2 = Uuid::new_v4();
let uuid3 = Uuid::new_v4();
let code = "2+2";
let id_map = IdMap(vec!
[ (Span::new(Index::new(0), Size::new(1)),uuid1.clone())
, (Span::new(Index::new(2), Size::new(1)),uuid2)
, (Span::new(Index::new(0), Size::new(3)),uuid3)
]);
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();
// Change code from "2+2" to "22+2"
let change = TextChangedNotification {
change : TextChange::insert(TextLocation{line:0,column:1}, "2"),
replaced_chars: 1..1
};
controller.apply_code_change(&change).unwrap();
let expected_ast = Ast::new(ast::Module {
lines: vec![BlockLine {
elem: Some(Ast::new(ast::Infix {
larg : Ast::new(ast::Number{base:None, int:"22".to_string()}, Some(uuid1)),
loff : 0,
opr : Ast::new(ast::Opr {name:"+".to_string()}, None),
roff : 0,
rarg : Ast::new(ast::Number{base:None, int:"2".to_string()}, Some(uuid2)),
}, Some(uuid3))),
off: 0
}]
}, None);
assert_eq!(expected_ast, controller.with_borrowed(|data| data.ast.clone()));
let mut text_notifications = controller.subscribe_text_notifications();
let mut graph_notifications = controller.subscribe_graph_notifications();
// Change code from "2+2" to "22+2"
let change = TextChange::insert(Index::new(1),"2".to_string());
controller.apply_code_change(&change).unwrap();
let expected_ast = Ast::new(ast::Module {
lines: vec![BlockLine {
elem: Some(Ast::new(ast::Infix {
larg : Ast::new(ast::Number{base:None, int:"22".to_string()}, Some(uuid1)),
loff : 0,
opr : Ast::new(ast::Opr {name:"+".to_string()}, None),
roff : 0,
rarg : Ast::new(ast::Number{base:None, int:"2".to_string()}, Some(uuid2)),
}, Some(uuid3))),
off: 0
}]
}, None);
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 {
use super::*;
use crate::executor::global::spawn;
use crate::executor::global::set_spawner;
use crate::executor::test_utils::TestWithLocalPoolExecutor;
use crate::transport::test_utils::TestWithMockedTransport;
use file_manager_client::Path;
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_configure;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn obtain_module_controller() {
let mut executor = LocalPool::new();
let finished = Rc::new(RefCell::new(false));
let finished_clone = finished.clone_ref();
let mut transport = MockTransport::new();
let transport_clone = transport.clone_ref();
set_spawner(executor.spawner());
spawn(async move {
let project_ctrl = controller::project::Handle::new_running(transport_clone);
let transport = MockTransport::new();
let mut test = TestWithMockedTransport::set_up(&transport);
test.run_test(async move {
let project_ctrl = controller::project::Handle::new_running(transport);
let location = controller::module::Location("TestLocation".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!(another_loc, another_module_ctrl.location());
assert!(module_ctrl.identity_equals(&same_module_ctrl));
*finished_clone.borrow_mut() = true;
});
// Load module (touch + read content)
executor.run_until_stalled();
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"
}"#);
// 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());
test.when_stalled_send_response("2 + 2");
test.when_stalled_send_response("3+3");
}
#[wasm_bindgen_test]
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();
set_spawner(executor.spawner());
spawn(async move {
TestWithLocalPoolExecutor::set_up().run_test(async move {
let project_ctrl = controller::project::Handle::new_running(transport);
let file_manager_handle = project_ctrl.file_manager();
let path = Path("TestPath".to_string());
@ -241,42 +204,20 @@ mod test {
assert_eq!(path , text_ctrl .file_path() );
assert_eq!(another_path, another_txt_ctrl.file_path() );
assert!(text_ctrl.identity_equals(&same_text_ctrl));
*finished_clone.borrow_mut() = true;
});
executor.run_until_stalled();
assert!(*finished.borrow());
}
#[wasm_bindgen_test]
fn obtain_text_controller_for_module() {
let mut executor = LocalPool::new();
let finished = Rc::new(RefCell::new(false));
let finished_clone = finished.clone_ref();
let mut transport = MockTransport::new();
let transport_clone = transport.clone_ref();
set_spawner(executor.spawner());
spawn(async move {
let project_ctrl = controller::project::Handle::new_running(transport_clone);
let transport = MockTransport::new();
let mut test = TestWithMockedTransport::set_up(&transport);
test.run_test(async move {
let project_ctrl = controller::project::Handle::new_running(transport);
let path = controller::module::Location("test".to_string()).to_path();
let text_ctrl = project_ctrl.get_text_controller(path.clone()).await.unwrap();
let content = text_ctrl.read_content().await.unwrap();
assert_eq!("2 + 2", content.as_str());
*finished_clone.borrow_mut() = true;
});
executor.run_until_stalled();
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());
test.when_stalled_send_response("2 + 2");
}
}

View File

@ -6,36 +6,16 @@
use crate::prelude::*;
use crate::controller::FallibleResult;
use crate::controller::notification;
use crate::executor::global::spawn;
use data::text::TextChangedNotification;
use failure::_core::fmt::Formatter;
use failure::_core::fmt::Error;
use data::text::TextChange;
use file_manager_client as fmc;
use flo_stream::MessagePublisher;
use flo_stream::Publisher;
use flo_stream::Subscriber;
use json_rpc::error::RpcError;
use shapely::shared;
// ====================
// === 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),
}
use utils::channel::process_stream_with_handle;
// =======================
@ -56,16 +36,17 @@ enum FileHandle {
shared! { Handle
/// Data stored by the text controller.
#[derive(Debug)]
pub struct Controller {
file: FileHandle,
/// Sink where we put events to be consumed by the view.
notification_publisher: Publisher<Notification>,
notifications: notification::Publisher<notification::Text>,
}
impl {
/// Get subscriber receiving controller's notifications.
pub fn subscribe(&mut self) -> Subscriber<Notification> {
self.notification_publisher.subscribe()
pub fn subscribe(&mut self) -> Subscriber<notification::Text> {
self.notifications.subscribe()
}
/// Get clone of file path handled by this controller.
@ -85,7 +66,13 @@ impl Handle {
}
/// Create controller managing Luna module file.
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.
@ -119,7 +106,7 @@ impl Handle {
/// 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
/// 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() {
controller.apply_code_change(change)
} else {
@ -135,8 +122,8 @@ impl Handle {
/// Create controller managing plain text file.
fn new(file_handle:FileHandle) -> Self {
let state = Controller {
file : file_handle,
notification_publisher : Publisher::new(NOTIFICATION_BUFFER_SIZE),
file : file_handle,
notifications : default(),
};
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 ===
#[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 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.
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)
-> FallibleResult<Self> {
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 world = WorldData::new(&web::get_html_element_by_id("root").unwrap());
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::TextFieldProperties;
use basegl::display::world::*;
use data::text::TextChange;
use enso_frp::io::KeyboardActions;
use enso_frp::io::KeyMask;
use nalgebra::Vector2;
use nalgebra::zero;
use utils::channel::process_stream_with_handle;
// ==================
@ -79,19 +80,6 @@ impl TextEditor {
let text_size = 16.0;
let properties = TextFieldProperties {font,text_size,base_color,size};
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);
let data = TextEditorData {controller,text_field,padding,position,size,logger};
@ -101,23 +89,14 @@ impl TextEditor {
fn initialize(self, keyboard_actions:&mut KeyboardActions) -> Self {
let save_keys = KeyMask::new_control_character('s');
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() {
text_editor.borrow().save();
}
});
}));
self.with_borrowed(move |data| {
let logger = data.logger.clone();
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.setup_notifications();
executor::global::spawn(self.reload_content());
self.update();
self
}
@ -128,6 +107,54 @@ impl TextEditor {
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.
fn update(&self) {
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::StreamExt;
/// Emit message using `UnboundedSender`. Does not care if there are listeners.
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,
}
}
}