Original commit: 3985f1efab
This commit is contained in:
Michał Wawrzyniec Urbańczyk 2021-09-24 00:18:12 +02:00 committed by GitHub
parent 82e74121b9
commit 10bac80ee8
21 changed files with 1206 additions and 360 deletions

View File

@ -1,5 +1,15 @@
# Next Release
<br/>![Bug Fixes](/docs/assets/tags/bug_fixes.svg)
#### Visual Environment
- [Visualizations will be attached after project is ready.][1825] This addresses
a rare issue when initially opened visualizations were automatically closed
rather than filled with data.
[1825]: https://github.com/enso-org/ide/pull/1825
# Enso 2.0.0-alpha.16 (2021-09-16)
<br/>![New Features](/docs/assets/tags/new_features.svg)
@ -51,7 +61,7 @@
- [Visualization previews are disabled.][1817] Previously, hovering over a
node's output port for more than four seconds would temporarily reveal the
node's visualization. This behavior is disabled now
node's visualization. This behavior is disabled now.
[1817]: https://github.com/enso-org/ide/pull/1817

View File

@ -130,12 +130,18 @@ pub enum Notification {
/// execution context.
#[serde(rename = "executionContext/executionFailed")]
ExecutionFailed(ExecutionFailed),
/// Sent from the server to the client to inform about the successful execution of a context.
#[serde(rename = "executionContext/executionComplete")]
#[serde(rename_all="camelCase")]
#[allow(missing_docs)]
ExecutionComplete{context_id:ContextId},
/// Sent from the server to the client to inform about a status of execution.
#[serde(rename = "executionContext/executionStatus")]
ExecutionStatus(ExecutionStatus),
/// Sent from server to the client to inform abouth the change in the suggestions database.
/// Sent from server to the client to inform about the change in the suggestions database.
#[serde(rename = "search/suggestionsDatabaseUpdates")]
SuggestionDatabaseUpdates(SuggestionDatabaseUpdatesEvent),
@ -1090,4 +1096,20 @@ pub mod test {
payload : ExpressionUpdatePayload::Panic {trace,message}
}
}
#[test]
fn deserialize_execution_complete() {
use std::str::FromStr;
let text = r#"{
"jsonrpc" : "2.0",
"method" : "executionContext/executionComplete",
"params" : {"contextId":"5a85125a-2dc5-45c8-84fc-679bc9fc4b00"}
}"#;
let notification = serde_json::from_str::<Notification>(text).unwrap();
let expected = Notification::ExecutionComplete {
context_id : ContextId::from_str("5a85125a-2dc5-45c8-84fc-679bc9fc4b00").unwrap()
};
assert_eq!(notification,expected);
}
}

View File

@ -33,6 +33,16 @@ pub trait StreamTestExt<S:?Sized + Stream> {
}
}
/// Asserts that stream has exactly one value ready and returns it.
///
/// Same caveats apply as for `test_poll_next`.
fn expect_one(&mut self) -> S::Item
where S::Item:Debug {
let ret = self.expect_next();
self.expect_pending();
ret
}
/// Asserts that stream has terminated.
///
/// Same caveats apply as for `test_poll_next`.

View File

@ -116,6 +116,11 @@ impl Handle {
Handle {logger,graph,execution_ctx,project,notifier}
}
/// See [`model::ExecutionContext::when_ready`].
pub fn when_ready(&self) -> StaticBoxFuture<Option<()>> {
self.execution_ctx.when_ready()
}
/// See [`model::ExecutionContext::attach_visualization`].
pub async fn attach_visualization
(&self, visualization:Visualization)
@ -123,6 +128,13 @@ impl Handle {
self.execution_ctx.attach_visualization(visualization).await
}
/// See [`model::ExecutionContext::modify_visualization`].
pub fn modify_visualization
(&self, id:VisualizationId, expression:Option<String>, module:Option<model::module::QualifiedName>)
-> BoxFuture<FallibleResult> {
self.execution_ctx.modify_visualization(id,expression,module)
}
/// See [`model::ExecutionContext::detach_visualization`].
pub async fn detach_visualization(&self, id:VisualizationId) -> FallibleResult<Visualization> {
self.execution_ctx.detach_visualization(id).await

View File

@ -2,7 +2,6 @@
use crate::prelude::*;
use crate::controller::graph::executed::Notification as GraphNotification;
use crate::controller::ide::StatusNotificationPublisher;
use crate::double_representation::project;
use crate::model::module::QualifiedName;
@ -154,7 +153,7 @@ impl Project {
let main_module_text = controller::Text::new(&self.logger,&project,file_path).await?;
let main_graph = controller::ExecutedGraph::new(&self.logger,project,method).await?;
self.init_call_stack_from_metadata(&main_module_model, &main_graph).await;
self.init_call_stack_from_metadata(&main_module_model,&main_graph).await;
self.notify_about_compiling_process(&main_graph);
self.display_warning_on_unsupported_engine_version()?;
@ -213,15 +212,16 @@ impl Project {
}
fn notify_about_compiling_process(&self, graph:&controller::ExecutedGraph) {
let status_notif = self.status_notifications.clone_ref();
let compiling_process = status_notif.publish_background_task(COMPILING_STDLIB_LABEL);
let notifications = graph.subscribe();
let mut computed_value_notif = notifications.filter(|notification|
futures::future::ready(matches!(notification, GraphNotification::ComputedValueInfo(_)))
);
let status_notifier = self.status_notifications.clone_ref();
let compiling_process = status_notifier.publish_background_task(COMPILING_STDLIB_LABEL);
let execution_ready = graph.when_ready();
let logger = self.logger.clone_ref();
executor::global::spawn(async move {
computed_value_notif.next().await;
status_notif.published_background_task_finished(compiling_process);
if execution_ready.await.is_some() {
status_notifier.published_background_task_finished(compiling_process);
} else {
warning!(logger, "Executed graph dropped before first successful execution!")
}
});
}

View File

@ -414,7 +414,7 @@ impl<'a> RootCategoryBuilder<'a> {
let name = name.into();
let parent = self.root_category_id;
let category_id = self.list_builder.built_list.subcategories.len();
self.list_builder.built_list.subcategories.push(Subcategory {name,parent,icon});
self.list_builder.built_list.subcategories.push(Subcategory {name,icon,parent});
CategoryBuilder { list_builder:self.list_builder, category_id}
}
}

View File

@ -2,6 +2,7 @@
pub mod project;
pub mod file_system;
pub mod visualization;
use crate::prelude::*;

View File

@ -13,15 +13,16 @@ use crate::controller::searcher::action::MatchInfo;
use crate::controller::searcher::Actions;
use crate::controller::upload;
use crate::controller::upload::NodeFromDroppedFileHandler;
use crate::executor::global::spawn;
use crate::executor::global::spawn_stream_handler;
use crate::ide::integration::file_system::FileProvider;
use crate::ide::integration::file_system::create_node_from_file;
use crate::ide::integration::file_system::FileOperation;
use crate::ide::integration::file_system::do_file_operation;
use crate::ide::integration::visualization::Manager as VisualizationManager;
use crate::model::execution_context::ComputedValueInfo;
use crate::model::execution_context::ExpressionId;
use crate::model::execution_context::LocalCall;
use crate::model::execution_context::Visualization;
use crate::model::execution_context::VisualizationId;
use crate::model::execution_context::VisualizationUpdateData;
use crate::model::module::ProjectMetadata;
use crate::model::suggestion_database;
@ -36,6 +37,7 @@ use ensogl::display::traits::*;
use ensogl_gui_components::file_browser::model::AnyFolderContent;
use ensogl_gui_components::list_view;
use ensogl_web::drop;
use futures::future::LocalBoxFuture;
use ide_view::graph_editor;
use ide_view::graph_editor::component::node;
use ide_view::graph_editor::component::visualization;
@ -43,21 +45,10 @@ use ide_view::graph_editor::EdgeEndpoint;
use ide_view::graph_editor::GraphEditor;
use ide_view::graph_editor::SharedHashMap;
use ide_view::searcher::entry::AnyModelProvider;
use ide_view::searcher::entry::GlyphHighlightedLabel;
use ide_view::searcher::new::Icon;
use ide_view::open_dialog;
use utils::iter::split_by_predicate;
use futures::future::LocalBoxFuture;
use ide_view::searcher::entry::GlyphHighlightedLabel;
// ========================
// === VisualizationMap ===
// ========================
/// Map that keeps information about enabled visualization.
pub type VisualizationMap = SharedHashMap<graph_editor::NodeId,Visualization>;
@ -75,8 +66,6 @@ enum MissingMappingFor {
ControllerNode(ast::Id),
#[fail(display="Displayed connection {:?} is not bound to any controller connection.", _0)]
DisplayedConnection(graph_editor::EdgeId),
#[fail(display="Displayed visualization {:?} is not bound to any attached by controller.",_0)]
DisplayedVisualization(graph_editor::NodeId)
}
/// Error raised when reached some fatal inconsistency in data provided by GraphEditor.
@ -96,7 +85,14 @@ struct VisualizationAlreadyAttached(graph_editor::NodeId);
#[fail(display="The Graph Integration hsd no SearcherController.")]
struct MissingSearcherController;
/// Denotes visualizations set in the graph editor.
#[derive(Clone,Copy,Debug,Display)]
pub enum WhichVisualization {
/// Usual visualization, triggered by the user.
Normal,
/// Special visualization, attached automatically when there is an error on the node.
Error,
}
// ====================
// === FencedAction ===
@ -209,8 +205,8 @@ struct Model {
expression_types : SharedHashMap<ExpressionId,Option<graph_editor::Type>>,
connection_views : RefCell<BiMap<controller::graph::Connection,graph_editor::EdgeId>>,
code_view : CloneRefCell<ensogl_text::Text>,
visualizations : VisualizationMap,
error_visualizations : VisualizationMap,
visualizations : Rc<VisualizationManager>,
error_visualizations : Rc<VisualizationManager>,
prompt_was_shown : Cell<bool>,
displayed_project_list : CloneRefCell<ProjectsToOpen>,
}
@ -230,7 +226,6 @@ impl Integration {
) -> Self {
let logger = Logger::new("ViewIntegration");
let model = Model::new(logger,view,graph,text,ide,project,main_module);
let model = Rc::new(model);
let editor_outs = &model.view.graph().frp.output;
let code_editor = &model.view.code_editor().text_area();
let searcher_frp = &model.view.searcher().frp;
@ -274,7 +269,7 @@ impl Integration {
frp::extend! { network
eval editor_outs.visualization_preprocessor_changed ([model]((node_id,preprocessor)) {
if let Err(err) = model.visualization_preprocessor_changed(*node_id,preprocessor) {
if let Err(err) = model.visualization_preprocessor_changed(*node_id,preprocessor.clone_ref()) {
error!(model.logger, "Error when handling request for setting new \
visualization's preprocessor code: {err}");
}
@ -342,7 +337,7 @@ impl Integration {
let dest = dest.clone();
let operation = *op;
let model = model.clone_ref();
executor::global::spawn(async move {
spawn(async move {
if let Err(err) = do_file_operation(&project,&source,&dest,operation).await {
error!(logger, "Failed to {operation.verb()} file: {err}");
} else {
@ -475,7 +470,7 @@ impl Integration {
where Stream : StreamExt + Unpin + 'static,
Function : Fn(Stream::Item,Rc<Model>) + 'static {
let model = Rc::downgrade(&self.model);
executor::global::spawn_stream_handler(model,stream,move |item,model| {
spawn_stream_handler(model,stream,move |item,model| {
handler(item,model);
futures::future::ready(())
})
@ -568,24 +563,29 @@ impl Model {
, text : controller::Text
, ide : controller::Ide
, project : model::Project
, main_module : model::Module) -> Self {
, main_module : model::Module) -> Rc<Self> {
let node_views = default();
let node_view_by_expression = default();
let connection_views = default();
let expression_views = default();
let expression_types = default();
let code_view = default();
let visualizations = default();
let error_visualizations = default();
let searcher = default();
let prompt_was_shown = default();
let displayed_project_list = default();
let (visualizations, visualizations_notifications) = crate::integration::visualization::Manager::new(logger.sub("visualizations"), graph.clone_ref(),project.clone_ref());
let (error_visualizations, error_visualizations_notifications) = crate::integration::visualization::Manager::new(logger.sub("error_visualizations"), graph.clone_ref(),project.clone_ref());
let this = Model
{logger,view,graph,text,ide,searcher,project,main_module,node_views
,node_view_by_expression,expression_views,expression_types,connection_views,code_view
,visualizations,error_visualizations,prompt_was_shown,displayed_project_list};
let this = Rc::new(this);
this.view.graph().frp.remove_all_nodes();
this.spawn_visualization_handler(visualizations_notifications, WhichVisualization::Normal);
this.spawn_visualization_handler(error_visualizations_notifications, WhichVisualization::Error);
let graph_frp = this.view.graph().frp.clone_ref();
graph_frp.remove_all_nodes();
this.view.status_bar().clear_all();
this.init_project_name();
this.init_crumbs();
@ -599,11 +599,23 @@ impl Model {
this
}
fn spawn_visualization_handler
( self : &Rc<Self>
, notifier : impl Stream<Item=crate::integration::visualization::Notification> + Unpin + 'static
, visualizations_kind : WhichVisualization
) {
let weak = Rc::downgrade(self);
let processor = async move |notification, this:Rc<Self>| {
this.handle_visualization_update(visualizations_kind,notification);
};
spawn_stream_handler(weak,notifier,processor);
}
fn load_visualizations(&self) {
let logger = self.logger.clone_ref();
let controller = self.project.visualization().clone_ref();
let graph_editor = self.view.graph().clone_ref();
executor::global::spawn(async move {
spawn(async move {
let identifiers = controller.list_visualizations().await;
let identifiers = identifiers.unwrap_or_default();
for identifier in identifiers {
@ -636,7 +648,7 @@ impl Model {
let breadcrumbs = self.view.graph().model.breadcrumbs.clone_ref();
let logger = self.logger.clone_ref();
let name = name.into();
executor::global::spawn(async move {
spawn(async move {
if let Err(e) = project.rename_project(name).await {
info!(logger, "The project couldn't be renamed: {e}");
breadcrumbs.cancel_project_name_editing.emit(());
@ -971,23 +983,87 @@ impl Model {
self.view.graph().frp.input.set_method_pointer.emit(&event);
}
fn visualization_manager(&self, which:WhichVisualization) -> &Rc<VisualizationManager> {
match which {
WhichVisualization::Normal => &self.visualizations,
WhichVisualization::Error => &self.error_visualizations,
}
}
fn handle_visualization_update
( &self
, which : WhichVisualization
, notification : crate::integration::visualization::Notification
) {
use crate::integration::visualization::Notification;
warning!(self.logger, "Received update for {which} visualization: {notification:?}");
match notification {
Notification::ValueUpdate {target,data,..} => {
if let Ok(view_id) = self.get_displayed_node_id(target) {
let endpoint = &self.view.graph().frp.input.set_visualization_data;
match Self::deserialize_visualization_data(data) {
Ok(data) => endpoint.emit((view_id, data)),
Err(error) =>
// TODO [mwu]
// We should consider having the visualization also accept error input.
error!(self.logger, "Failed to deserialize visualization update: {error}"),
}
}
}
Notification::FailedToAttach {visualization,error} => {
error!(self.logger, "Visualization {visualization.id} failed to attach: {error}.");
if let Ok(node_view_id) = self.get_displayed_node_id(visualization.expression_id) {
self.view.graph().disable_visualization(node_view_id);
}
}
Notification::FailedToDetach {visualization,error} => {
error!(self.logger, "Visualization {visualization.id} failed to detach: {error}.");
// Here we cannot really do much. Failing to detach might mean that visualization
// was already detached, that we detached it but failed to observe this (e.g. due to
// a connectivity issue) or that we did something really wrong.
// For now, we will just forget about this visualization. Better to unlikely "leak"
// it rather than likely break visualizations on the node altogether.
let manager = self.visualization_manager(which);
let forgotten = manager.forget_visualization(visualization.expression_id);
if let Some(forgotten) = forgotten {
error!(self.logger, "The visualization will be forgotten: {forgotten:?}")
}
}
Notification::FailedToModify {desired,error} => {
error!(self.logger, "Visualization {desired.id} failed to be modified: {error} \
Will hide it in GUI.");
// Actually it would likely have more sense if we had just restored the previous
// visualization, as its LS state should be preserved. However, we already scrapped
// it on the GUI side and we don't even know its path anymore.
if let Ok(node_view_id) = self.get_displayed_node_id(desired.expression_id) {
self.view.graph().disable_visualization(node_view_id);
}
}
}
}
/// Route the metadata description as a desired visualization state to the Manager.
fn update_visualization
( &self
, node_id : graph_editor::NodeId
, which : WhichVisualization
, metadata : Option<visualization::Metadata>
) -> FallibleResult {
let target_id = self.get_controller_node_id(node_id)?;
let manager = self.visualization_manager(which);
manager.set_visualization(target_id,metadata);
Ok(())
}
/// Mark node as erroneous if given payload contains an error.
fn set_error
(&self, node_id:graph_editor::NodeId, error:Option<&ExpressionUpdatePayload>)
-> FallibleResult {
let error = self.convert_payload_to_error_view(error,node_id);
self.view.graph().set_node_error_status(node_id,error.clone());
let error_visualizations = self.error_visualizations.clone_ref();
let has_error_visualization = self.error_visualizations.contains_key(&node_id);
if error.is_some() && !has_error_visualization {
use graph_editor::builtin::visualization::native::error;
let endpoint = self.view.graph().frp.set_error_visualization_data.clone_ref();
let metadata = error::metadata();
self.attach_visualization(node_id,&metadata,endpoint,error_visualizations)?;
} else if error.is_none() && has_error_visualization {
self.detach_visualization(node_id,error_visualizations)?;
}
Ok(())
let error = self.convert_payload_to_error_view(error,node_id);
let has_error = error.is_some();
self.view.graph().set_node_error_status(node_id,error);
let metadata = has_error.then(graph_editor::builtin::visualization::native::error::metadata);
self.update_visualization(node_id,WhichVisualization::Error,metadata)
}
fn convert_payload_to_error_view
@ -1135,7 +1211,6 @@ impl Model {
analytics::remote_log_event("integration::node_entered");
self.view.graph().frp.deselect_all_nodes.emit(&());
self.push_crumb(local_call);
self.request_detaching_all_visualizations();
self.refresh_graph_view()
}
@ -1143,7 +1218,6 @@ impl Model {
pub fn on_node_exited(&self, id:double_representation::node::Id) -> FallibleResult {
analytics::remote_log_event("integration::node_exited");
self.view.graph().frp.deselect_all_nodes.emit(&());
self.request_detaching_all_visualizations();
self.refresh_graph_view()?;
self.pop_crumb();
let id = self.get_displayed_node_id(id)?;
@ -1160,20 +1234,6 @@ impl Model {
self.refresh_computed_infos(expressions)
}
/// Request controller to detach all attached visualizations.
pub fn request_detaching_all_visualizations(&self) {
let controller = self.graph.clone_ref();
let logger = self.logger.clone_ref();
let action = async move {
for result in controller.detach_all_visualizations().await {
if let Err(err) = result {
error!(logger,"Failed to detach one of the visualizations: {err:?}.");
}
}
};
executor::global::spawn(action);
}
/// Handle notification received from Graph Controller.
pub fn handle_graph_notification
(&self, notification:&Option<controller::graph::executed::Notification>) {
@ -1464,14 +1524,13 @@ impl Model {
fn visualization_shown_in_ui
(&self, (node_id,vis_metadata):&(graph_editor::NodeId,visualization::Metadata))
-> FallibleResult {
debug!(self.logger, "Visualization enabled on {node_id}: {vis_metadata:?}.");
let endpoint = self.view.graph().frp.input.set_visualization_data.clone_ref();
self.attach_visualization(*node_id,vis_metadata,endpoint,self.visualizations.clone_ref())?;
Ok(())
debug!(self.logger, "Visualization shown on {node_id}: {vis_metadata:?}.");
self.update_visualization(*node_id,WhichVisualization::Normal,Some(vis_metadata.clone()))
}
fn visualization_hidden_in_ui(&self, node_id:&graph_editor::NodeId) -> FallibleResult {
self.detach_visualization(*node_id,self.visualizations.clone_ref())
debug!(self.logger, "Visualization hidden on {node_id}.");
self.update_visualization(*node_id,WhichVisualization::Normal,None)
}
fn store_updated_stack_task(&self) -> impl FnOnce() -> FallibleResult + 'static {
@ -1508,7 +1567,7 @@ impl Model {
}
}
};
executor::global::spawn(enter_action);
spawn(enter_action);
}
Ok(())
}
@ -1542,7 +1601,7 @@ impl Model {
let _ = update_metadata().ok();
}
};
executor::global::spawn(exit_node_action);
spawn(exit_node_action);
}
fn code_changed_in_ui(&self, changes:&Vec<ensogl_text::Change>) -> FallibleResult {
@ -1559,7 +1618,7 @@ impl Model {
let logger = self.logger.clone_ref();
let controller = self.text.clone_ref();
let content = self.code_view.get().to_string();
executor::global::spawn(async move {
spawn(async move {
if let Err(err) = controller.store_content(content).await {
error!(logger, "Error while saving file: {err:?}");
}
@ -1580,44 +1639,20 @@ impl Model {
}
}
fn resolve_visualization_context
(&self, context:&visualization::instance::ContextModule)
-> FallibleResult<model::module::QualifiedName> {
use visualization::instance::ContextModule::*;
match context {
ProjectMain => Ok(self.project.main_module()),
Specific(module_name) => model::module::QualifiedName::from_text(module_name),
}
}
fn visualization_preprocessor_changed
( &self
, node_id : graph_editor::NodeId
, preprocessor : &visualization::instance::PreprocessorConfiguration
, preprocessor : visualization::instance::PreprocessorConfiguration
) -> FallibleResult {
if let Some(visualization) = self.visualizations.get_cloned(&node_id) {
let logger = self.logger.clone_ref();
let controller = self.graph.clone_ref();
let code = preprocessor.code.deref().into();
let module = self.resolve_visualization_context(&preprocessor.module)?;
let id = visualization.id;
executor::global::spawn(async move {
let result = controller.set_visualization_preprocessor(id,code,module);
if let Err(err) = result.await {
error!(logger, "Error when setting visualization preprocessor: {err}");
}
});
Ok(())
} else {
Err(MissingMappingFor::DisplayedVisualization(node_id).into())
}
let metadata = visualization::Metadata {preprocessor};
self.update_visualization(node_id,WhichVisualization::Normal,Some(metadata))
}
fn open_dialog_opened_in_ui(self:&Rc<Self>) {
debug!(self.logger, "Opened file dialog in ui. Providing content root list");
self.reload_files_in_file_browser();
let model = Rc::downgrade(self);
executor::global::spawn(async move {
spawn(async move {
if let Some(this) = model.upgrade() {
if let Ok(manage_projects) = this.ide.manage_projects() {
match manage_projects.list_projects().await {
@ -1644,7 +1679,7 @@ impl Model {
if let Some(id) = self.displayed_project_list.get().get_project_id_by_index(*entry_id) {
let logger = self.logger.clone_ref();
let ide = self.ide.clone_ref();
executor::global::spawn(async move {
spawn(async move {
if let Ok(manage_projects) = ide.manage_projects() {
if let Err(err) = manage_projects.open_project(id).await {
error!(logger, "Error while opening project: {err}");
@ -1711,181 +1746,6 @@ impl Model {
registry.get(id)
}
fn attach_visualization
( &self
, node_id : graph_editor::NodeId
, vis_metadata : &visualization::Metadata
, receive_data_endpoint : frp::Any<(graph_editor::NodeId,visualization::Data)>
, visualizations_map : VisualizationMap
) -> FallibleResult<VisualizationId> {
// Do nothing if there is already a visualization attached.
let err = || VisualizationAlreadyAttached(node_id);
(!visualizations_map.contains_key(&node_id)).ok_or_else(err)?;
debug!(self.logger, "Attaching visualization on node {node_id}.");
let visualization = self.prepare_visualization(node_id,vis_metadata)?;
let id = visualization.id;
let update_handler = self.visualization_update_handler(receive_data_endpoint,node_id);
// We cannot do this in the async task, as the user may decide to detach before server
// confirms that we actually have attached the visualization.
visualizations_map.insert(node_id,visualization);
let task = self.attaching_visualization_task(node_id, visualizations_map, update_handler);
executor::global::spawn(task);
Ok(id)
}
/// Try attaching visualization to the node.
///
/// In case of timeout failure, retries up to total `attempts` count will be made.
/// For other kind of errors no further attempts will be made.
fn try_attaching_visualization_task
(&self, node_id:graph_editor::NodeId, visualizations_map:VisualizationMap, attempts:usize)
-> impl Future<Output=AttachingResult<impl Stream<Item=VisualizationUpdateData>>> {
let logger = self.logger.clone_ref();
let controller = self.graph.clone_ref();
async move {
let mut last_error = None;
for i in 1 ..= attempts {
// We need to re-get this info in each iteration. It might change in the meantime.
let visualization_info = visualizations_map.get_cloned(&node_id);
if let Some(visualization_info) = visualization_info {
let id = visualization_info.id;
match controller.attach_visualization(visualization_info).await {
Ok(stream) => {
debug!(logger, "Successfully attached visualization {id} for node \
{node_id}.");
return AttachingResult::Attached(stream);
}
Err(e) if enso_protocol::language_server::is_timeout_error(&e) => {
warning!(logger, "Failed to attach visualization {id} for node \
{node_id} (attempt {i}). Will retry, as it is a timeout error.");
last_error = Some(e);
}
Err(e) => {
warning!(logger, "Failed to attach visualization {id} for node \
{node_id}: {e}");
return AttachingResult::Failed(e)
}
}
} else {
// If visualization is not present in the map, it means that UI detached it
// before we were able to attach it to the backend. Thus, it is fine to do
// nothing here and finish this task.
return AttachingResult::Aborted;
}
}
let error = last_error.unwrap_or_else(|| failure::format_err!("No attempts were made."));
error!(logger, "Failed to attach visualization for node {node_id}: {error}\nWill abort.");
AttachingResult::Failed(error)
}
}
/// Request attaching the visualization in the controller and handle result.
///
/// Updates the given `[VisualizationMap]` with the results. If the visualization failed to
/// attach, it will be disable in the graph view. On success, the visualization updates handler
/// will be spawned.
fn attaching_visualization_task
( &self
, node_id : graph_editor::NodeId
, visualizations_map : VisualizationMap
, update_handler : impl FnMut(VisualizationUpdateData) -> futures::future::Ready<()> + 'static
) -> impl Future<Output=()> {
let graph_frp = self.view.graph().frp.clone_ref();
let map = visualizations_map.clone();
let attempts = crate::constants::ATTACHING_TIMEOUT_RETRIES;
let stream_fut = self.try_attaching_visualization_task(node_id,map,attempts);
async move {
// No need to log anything here, as `try_attaching_visualization_task` does this.
match stream_fut.await {
AttachingResult::Attached(stream) => {
let updates_handler = stream.for_each(update_handler);
executor::global::spawn(updates_handler);
}
AttachingResult::Aborted => {
// Do nothing and be silent.
}
AttachingResult::Failed(_) => {
// If attaching is impossible, we should close the visualization.
visualizations_map.remove(&node_id);
graph_frp.disable_visualization.emit(&node_id);
}
}
}
}
/// Return an asynchronous event processor that routes visualization update to the given's
/// visualization respective FRP endpoint.
fn visualization_update_handler
( &self
, endpoint : frp::Any<(graph_editor::NodeId,visualization::Data)>
, node_id : graph_editor::NodeId
) -> impl FnMut(VisualizationUpdateData) -> futures::future::Ready<()> {
// TODO [mwu]
// For now only JSON visualizations are supported, so we can just assume JSON data in the
// binary package.
let logger = self.logger.clone_ref();
move |update| {
match Self::deserialize_visualization_data(update) {
Ok (data) => endpoint.emit((node_id,data)),
Err(error) =>
// TODO [mwu]
// We should consider having the visualization also accept error input.
error!(logger, "Failed to deserialize visualization update. {error}"),
}
futures::future::ready(())
}
}
/// Create a controller-compatible description of the visualization based on the input received
/// from the graph editor endpoints.
fn prepare_visualization
(&self, node_id:graph_editor::NodeId, metadata:&visualization::Metadata)
-> FallibleResult<Visualization> {
let module_designation = &metadata.preprocessor.module;
let visualisation_module = self.resolve_visualization_context(module_designation)?;
let id = VisualizationId::new_v4();
let expression = metadata.preprocessor.code.to_string();
let ast_id = self.get_controller_node_id(node_id)?;
Ok(Visualization{id,ast_id,expression,visualisation_module})
}
fn detach_visualization
( &self
, node_id : graph_editor::NodeId
, visualizations_map : VisualizationMap
) -> FallibleResult {
debug!(self.logger,"Node editor wants to detach visualization on {node_id}.");
let err = || NoSuchVisualization(node_id);
let id = visualizations_map.get_cloned(&node_id).ok_or_else(err)?.id;
let logger = self.logger.clone_ref();
let controller = self.graph.clone_ref();
// We first detach to allow re-attaching even before the server confirms the operation.
visualizations_map.remove(&node_id);
executor::global::spawn(async move {
if controller.detach_visualization(id).await.is_ok() {
debug!(logger,"Successfully detached visualization {id} from node {node_id}.");
} else {
error!(logger,"Failed to detach visualization {id} from node {node_id}.");
// TODO [mwu]
// We should somehow deal with this, but we have really no information, how to.
// If this failed because e.g. the visualization was already removed (or another
// reason to that effect), we should just do nothing.
// However, if it is issue like connectivity problem, then we should retry.
// However, even if had better error recognition, we won't always know.
// So we should also handle errors like unexpected visualization updates and use
// them to drive cleanups on such discrepancies.
}
});
Ok(())
}
fn setup_searcher_controller
(&self, weak_self:&Weak<Self>, mode:controller::searcher::Mode) -> FallibleResult {
let selected_nodes = self.view.graph().model.nodes.all_selected().iter().filter_map(|id| {
@ -1895,7 +1755,7 @@ impl Model {
let ide = self.ide.clone_ref();
let searcher = controller::Searcher::new_from_graph_controller
(&self.logger,ide,&self.project,controller,mode,selected_nodes)?;
executor::global::spawn(searcher.subscribe().for_each(f!([weak_self](notification) {
spawn(searcher.subscribe().for_each(f!([weak_self](notification) {
if let Some(this) = weak_self.upgrade() {
this.handle_searcher_notification(notification);
}

View File

@ -0,0 +1,670 @@
//! Utilities facilitating integration for visualizations.
use crate::prelude::*;
use crate::executor::global::spawn;
use crate::model::execution_context::Visualization;
use crate::model::execution_context::VisualizationId;
use crate::model::execution_context::VisualizationUpdateData;
use crate::sync::Synchronized;
use crate::controller::ExecutedGraph;
use futures::channel::mpsc::UnboundedReceiver;
use futures::future::ready;
use ide_view::graph_editor::component::visualization;
use ide_view::graph_editor::SharedHashMap;
use ide_view::graph_editor::component::visualization::instance::ContextModule;
use ide_view::graph_editor::component::visualization::Metadata;
// ================================
// === Resolving Context Module ===
// ================================
/// Resolve the context module to a fully qualified name.
pub fn resolve_context_module
( context_module : &ContextModule
, main_module_name : impl FnOnce() -> model::module::QualifiedName
) -> FallibleResult<model::module::QualifiedName> {
use visualization::instance::ContextModule::*;
match context_module {
ProjectMain => Ok(main_module_name()),
Specific(module_name) => model::module::QualifiedName::from_text(module_name),
}
}
// ==============
// === Errors ===
// ==============
#[allow(missing_docs)]
#[derive(Clone,Copy,Debug,Fail)]
#[fail(display="No visualization information for expression {}.", _0)]
pub struct NoVisualization(ast::Id);
// ====================
// === Notification ===
// ====================
/// Updates emitted by the Visualization Manager.
#[derive(Debug)]
pub enum Notification {
/// New update data has been received from Language Server.
ValueUpdate {
/// Expression on which the visualization is attached.
target : ast::Id,
/// Identifier of the visualization that received data.
visualization_id : VisualizationId,
/// Serialized binary data payload -- result of visualization evaluation.
data : VisualizationUpdateData
},
/// An attempt to attach a new visualization has failed.
FailedToAttach {
/// Visualization that failed to be attached.
visualization : Visualization,
/// Error from the request.
error : failure::Error
},
/// An attempt to detach a new visualization has failed.
FailedToDetach {
/// Visualization that failed to be detached.
visualization : Visualization,
/// Error from the request.
error : failure::Error
},
/// An attempt to modify a visualization has failed.
FailedToModify {
/// Visualization that failed to be modified.
desired : Visualization,
/// Error from the request.
error : failure::Error
},
}
// ==============
// === Status ===
// ==============
/// Describes the state of the visualization on the Language Server.
#[derive(Clone,Debug,PartialEq)]
pub enum Status {
/// Not attached and no ongoing background work.
NotAttached,
/// Attaching has been requested but result is still unknown.
BeingAttached(Visualization),
/// Attaching has been requested but result is still unknown.
BeingModified{
/// Current visualization state.
from : Visualization,
/// Target visualization state (will be achieved if operation completed successfully).
to : Visualization
},
/// Attaching has been requested but result is still unknown.
BeingDetached(Visualization),
/// Visualization attached and no ongoing background work.
Attached(Visualization),
}
impl Status {
/// What is the expected eventual visualization, assuming that any ongoing request will succeed.
pub fn target(&self) -> Option<&Visualization> {
match self {
Status::NotAttached => None,
Status::BeingAttached(v) => Some(v),
Status::BeingModified {to,..} => Some(to),
Status::BeingDetached(_) => None,
Status::Attached(v) => Some(v),
}
}
/// Check if there is an ongoing request to the Language Server for this visualization.
pub fn has_ongoing_work(&self) -> bool {
match self {
Status::NotAttached => false,
Status::BeingAttached(_) => true,
Status::BeingModified {..} => true,
Status::BeingDetached(_) => true,
Status::Attached(_) => false,
}
}
/// Get the target visualization id, or current visualization id otherwise.
///
/// Note that this might include id of a visualization that is not yet attached.
pub fn latest_id(&self) -> Option<VisualizationId> {
match self {
Status::NotAttached => None,
Status::BeingAttached(v) => Some(v.id),
Status::BeingModified {to,..} => Some(to.id),
Status::BeingDetached(v) => Some(v.id),
Status::Attached(v) => Some(v.id),
}
}
/// LS state of the currently attached visualization.
pub fn currently_attached(&self) -> Option<&Visualization> {
match self {
Status::NotAttached => None,
Status::BeingAttached(_) => None,
Status::BeingModified {from,..} => Some(from),
Status::BeingDetached(v) => Some(v),
Status::Attached(v) => Some(v),
}
}
}
impl Default for Status {
fn default() -> Self {
Status::NotAttached
}
}
// ===============
// === Desired ===
// ===============
/// Desired visualization described using unresolved view metadata structure.
#[allow(missing_docs)]
#[derive(Clone,Debug,PartialEq)]
pub struct Desired {
pub visualization_id : VisualizationId,
pub expression_id : ast::Id,
pub metadata : Metadata,
}
// ===================
// === Description ===
// ===================
/// Information on visualization that are stored by the Manager.
#[derive(Clone,Debug,Default)]
pub struct Description {
/// The visualization desired by the View. `None` denotes detached visualization.
pub desired : Option<Desired>,
/// What we know about Language Server state of the visualization.
pub status : Synchronized<Status>,
}
impl Description {
/// Future that gets resolved when ongoing LS call for this visualization is done.
///
/// The yielded value is a new visualization status, or `None` if the operation has been
/// aborted.
pub fn when_done(&self) -> impl Future<Output=Option<Status>> {
self.status.when_map(|status| (!status.has_ongoing_work()).then(|| status.clone()))
}
/// Get the target visualization id, or current visualization id otherwise.
///
/// Note that this might include id of a visualization that is not yet attached.
pub fn latest_id(&self) -> Option<VisualizationId> {
self.desired.as_ref().map(|desired| desired.visualization_id)
.or_else(|| self.status.get_cloned().latest_id())
}
}
/// Handles mapping between node expression id and the attached visualization, synchronizing desired
/// state with the Language Server.
///
/// As this type wraps asynchronous operations, it should be stored using `Rc` pointer.
#[derive(Debug)]
pub struct Manager {
logger : Logger,
visualizations : SharedHashMap<ast::Id, Description>,
executed_graph : ExecutedGraph,
project : model::Project,
notification_sender : futures::channel::mpsc::UnboundedSender<Notification>,
}
impl Manager {
/// Create a new manager for a given execution context.
///
/// Return a handle to the Manager and the receiver for notifications.
/// Note that receiver cannot be re-retrieved or changed in the future.
pub fn new(logger:Logger, executed_graph:ExecutedGraph, project:model::Project)
-> (Rc<Self>,UnboundedReceiver<Notification>) {
let (notification_sender,notification_receiver) = futures::channel::mpsc::unbounded();
let ret = Self {
logger,
visualizations: default(),
executed_graph,
project,
notification_sender
};
(Rc::new(ret),notification_receiver)
}
/// Borrow mutably a description of a given visualization.
fn borrow_mut(&self, target:ast::Id) -> FallibleResult<RefMut<Description>> {
let map = self.visualizations.raw.borrow_mut();
RefMut::filter_map(map, |map| map.get_mut(&target))
.map_err(|_| NoVisualization(target).into())
}
/// Set a new status for the visualization.
fn update_status(&self, target:ast::Id, new_status: Status) {
if let Ok(visualization) = self.borrow_mut(target) {
visualization.status.replace(new_status);
} else if Status::NotAttached == new_status {
// No information about detached visualization.
// Good, no need to fix anything.
} else {
// Something is going on with a visualization we dropped info about. Unexpected.
// Insert it back, so it can be properly detached (or whatever) later.
let visualization = Description {
desired : default(),
status : Synchronized::new(new_status),
};
self.visualizations.insert(target,visualization);
};
}
/// Get a copy of a visualization description.
pub fn get_cloned(&self, target:ast::Id) -> FallibleResult<Description> {
self.visualizations
.get_cloned(&target)
.ok_or_else(|| NoVisualization(target).into())
}
/// Get the visualization state that is desired (i.e. requested from GUI side) for a given node.
pub fn get_desired_visualization(&self, target:ast::Id) -> FallibleResult<Desired> {
self.get_cloned(target).and_then(|v| {
v.desired.ok_or_else(|| failure::format_err!("No desired visualization set for {}", target))
})
}
/// Request removing visualization from te expression, if present.
pub fn remove_visualization(self:&Rc<Self>, target:ast::Id) {
self.set_visualization(target,None)
}
/// Drops the information about visualization on a given node.
///
/// Should be used only if the visualization was detached (or otherwise broken) outside of the
/// `[Manager]` knowledge. Otherwise, the visualization will be dangling on the LS side.
pub fn forget_visualization(self:&Rc<Self>, target:ast::Id)
-> Option<Description> {
self.visualizations.remove(&target)
}
/// Request setting a given visualization on the node.
///
/// Note that `[Manager]` allows setting at most one visualization per expression. Subsequent
/// calls will chnge previous visualization to the a new one.
pub fn request_visualization(self:&Rc<Self>, target:ast::Id, requested:Metadata) {
self.set_visualization(target,Some(requested))
}
/// Set desired state of visualization on a node.
pub fn set_visualization(self:&Rc<Self>, target:ast::Id, new_desired:Option<Metadata>) {
let current = self.visualizations.get_cloned(&target);
if current.is_none() && new_desired.is_none() {
// Early return: requested to remove visualization that was already removed.
return
};
let current_id = current.as_ref().and_then(|current| current.latest_id());
let new_desired = new_desired.map(|new_desired| Desired {
expression_id : target,
visualization_id : current_id.unwrap_or_else(VisualizationId::new_v4),
metadata : new_desired,
});
self.write_new_desired(target,new_desired)
}
fn write_new_desired(self:&Rc<Self>, target:ast::Id, new_desired:Option<Desired>) {
debug!(self.logger, "Requested to set visualization {target}: {new_desired:?}");
let mut current = match self.visualizations.get_cloned(&target) {
None => {
if new_desired.is_none() {
// Already done.
return
} else {
Description::default()
}
}
Some(v) => v,
};
if current.desired != new_desired {
current.desired = new_desired;
self.visualizations.insert(target,current);
self.synchronize(target);
} else {
debug!(self.logger, "Visualization for {target} was already in the desired state: \
{new_desired:?}");
}
}
fn resolve_context_module(&self, context_module:&ContextModule) -> FallibleResult<model::module::QualifiedName> {
resolve_context_module(context_module,|| self.project.main_module())
}
fn prepare_visualization(&self, desired:Desired) -> FallibleResult<Visualization> {
let context_module = desired.metadata.preprocessor.module;
let resolved_module = self.resolve_context_module(&context_module)?;
Ok(Visualization {
id : desired.visualization_id,
expression_id : desired.expression_id,
preprocessor_code : desired.metadata.preprocessor.code.to_string(),
context_module : resolved_module,
})
}
/// Schedule an asynchronous task that will try applying local desired state of the
/// visualization to the language server.
pub fn synchronize(self:&Rc<Self>, target:ast::Id) {
let context = self.executed_graph.when_ready();
let weak = Rc::downgrade(self);
let task = async move || -> Option<()> {
context.await;
let description = weak.upgrade()?.visualizations.get_cloned(&target)?;
let status = description.when_done().await?;
// We re-get the visualization here, because desired visualization could have been
// modified while we were awaiting completion of previous request.
let this = weak.upgrade()?;
let description = this.visualizations.get_cloned(&target)?;
let desired_vis_id = description.desired.as_ref().map(|v| v.visualization_id);
let new_visualization = description.desired.and_then(|desired| {
this.prepare_visualization(desired.clone()).handle_err(|error| {
error!(this.logger, "Failed to prepare visualization {desired:?}: {error}")
})
});
match (status, new_visualization) {
// Nothing attached and we want to have something.
(Status::NotAttached, Some(new_visualization)) => {
info!(this.logger, "Will attach visualization {new_visualization.id} to \
expression {target}");
let status = Status::BeingAttached(new_visualization.clone());
this.update_status(target,status);
let notifier = this.notification_sender.clone();
let attaching_result = this.executed_graph.attach_visualization(new_visualization.clone());
match attaching_result.await {
Ok(update_receiver) => {
let visualization_id = new_visualization.id;
let status = Status::Attached(new_visualization);
this.update_status(target,status);
spawn(update_receiver.for_each(move |data| {
let notification = Notification::ValueUpdate {
target,
visualization_id,
data
};
let _ = notifier.unbounded_send(notification);
ready(())
}))
}
Err(error) => {
// TODO [mwu]
// We should somehow deal with this, but we have really no information, how to.
// If this failed because e.g. the visualization was already removed (or another
// reason to that effect), we should just do nothing.
// However, if it is issue like connectivity problem, then we should retry.
// However, even if had better error recognition, we won't always know.
// So we should also handle errors like unexpected visualization updates and use
// them to drive cleanups on such discrepancies.
let status = Status::NotAttached;
this.update_status(target,status);
let notification = Notification::FailedToAttach {
visualization:new_visualization,
error
};
let _ = notifier.unbounded_send(notification);
}
};
}
(Status::Attached(so_far),None)
| (Status::Attached(so_far),Some(_))
if !desired_vis_id.contains(&so_far.id) => {
info!(this.logger, "Will detach from {target}: {so_far:?}");
let status = Status::BeingDetached(so_far.clone());
this.update_status(target,status);
let detaching_result = this.executed_graph.detach_visualization(so_far.id);
match detaching_result.await {
Ok(_) => {
let status = Status::NotAttached;
this.update_status(target,status);
if let Some(vis) = this.visualizations.remove(&so_far.expression_id) {
if vis.desired.is_some() {
// Restore visualization that was re-requested while being detached.
this.visualizations.insert(so_far.expression_id, vis);
this.synchronize(so_far.expression_id);
}
}
}
Err(error) => {
let status = Status::Attached(so_far.clone());
this.update_status(target,status);
let notification = Notification::FailedToDetach {
visualization : so_far,
error
};
let _ = this.notification_sender.unbounded_send(notification);
}
};
}
(Status::Attached(so_far),Some(new_visualization))
if so_far != new_visualization && so_far.id == new_visualization.id => {
info!(this.logger, "Will modify visualization on {target} from {so_far:?} to \
{new_visualization:?}");
let status = Status::BeingModified {
from : so_far.clone(),
to : new_visualization.clone(),
};
this.update_status(target,status);
let id = so_far.id;
let expression = new_visualization.preprocessor_code.clone();
let module = new_visualization.context_module.clone();
let modifying_result = this.executed_graph.modify_visualization(id, Some(expression), Some(module));
match modifying_result.await {
Ok(_) => {
let status = Status::Attached(new_visualization);
this.update_status(target,status);
}
Err(error) => {
let status = Status::Attached(so_far);
this.update_status(target,status);
let notification = Notification::FailedToModify {
desired : new_visualization,
error
};
let _ = this.notification_sender.unbounded_send(notification);
}
};
}
_ => {}
};
Some(())
};
spawn(async move { task().await; });
}
}
// =============
// === Tests ===
// =============
#[cfg(test)]
mod tests {
use super::*;
use utils::test::traits::*;
use futures::future::ready;
use ide_view::graph_editor::component::visualization::instance::ContextModule;
use ide_view::graph_editor::component::visualization::instance::PreprocessorConfiguration;
use wasm_bindgen_test::wasm_bindgen_test;
#[derive(Shrinkwrap)]
#[shrinkwrap(mutable)]
struct Fixture {
#[shrinkwrap(main_field)]
inner : crate::test::mock::Fixture,
node_id : ast::Id,
}
impl Fixture {
fn new() -> Self {
let inner = crate::test::mock::Unified::new().fixture();
let node_id = inner.graph.nodes().unwrap().first().unwrap().id();
Self {inner,node_id}
}
fn vis_metadata(&self, code:impl Into<String>) -> Metadata {
Metadata {
preprocessor : PreprocessorConfiguration {
module : ContextModule::Specific(self.inner.module_name().to_string().into()),
code : code.into().into(),
}
}
}
}
#[derive(Clone,Debug)]
enum ExecutionContextRequest {
Attach(Visualization),
Detach(VisualizationId),
Modify {
id : VisualizationId,
expression : Option<String>,
module : Option<model::module::QualifiedName>,
},
}
#[derive(Shrinkwrap)]
#[shrinkwrap(mutable)]
struct VisOperationsTester {
#[shrinkwrap(main_field)]
pub inner : Fixture,
pub is_ready : Synchronized<bool>,
pub manager : Rc<Manager>,
pub notifier : UnboundedReceiver<Notification>,
pub requests : StaticBoxStream<ExecutionContextRequest>,
}
impl VisOperationsTester {
fn new
( inner:Fixture ) -> Self {
let faux_vis = Visualization {
id : default(),
expression_id :default(),
context_module: inner.project.qualified_module_name(inner.module.path()),
preprocessor_code : "faux value".into(),
};
let is_ready = Synchronized::new(false);
let mut execution_context = model::execution_context::MockAPI::new();
let (request_sender, requests_receiver) = futures::channel::mpsc::unbounded();
let requests = requests_receiver.boxed_local();
execution_context.expect_when_ready()
.returning_st(f!{[is_ready]() is_ready.when_eq(&true).boxed_local()});
let sender = request_sender.clone();
execution_context.expect_attach_visualization()
.returning_st(move |vis| {
sender.unbounded_send(ExecutionContextRequest::Attach(vis)).unwrap();
ready(Ok(futures::channel::mpsc::unbounded().1)).boxed_local()
});
let sender = request_sender.clone();
execution_context.expect_detach_visualization()
.returning_st(move |vis_id| {
sender.unbounded_send(ExecutionContextRequest::Detach(vis_id)).unwrap();
ready(Ok(faux_vis.clone())).boxed_local()
});
let sender = request_sender.clone();
execution_context.expect_modify_visualization()
.returning_st(move |id,expression,module| {
let request = ExecutionContextRequest::Modify{id,expression,module};
sender.unbounded_send(request).unwrap();
ready(Ok(())).boxed_local()
});
let execution_context = Rc::new(execution_context);
let executed_graph = controller::ExecutedGraph::new_internal(inner.graph.clone_ref(),inner.project.clone_ref(),execution_context);
let (manager,notifier) = Manager::new(inner.logger.sub("manager"), executed_graph.clone_ref(),inner.project.clone_ref());
Self {
inner,is_ready,manager,notifier,requests
}
}
}
fn matching_metadata(manager:&Manager, visualization:&Visualization, metadata:&Metadata) -> bool {
let PreprocessorConfiguration{module,code} = &metadata.preprocessor;
visualization.preprocessor_code == code.to_string()
&& visualization.context_module == manager.resolve_context_module(&module).unwrap()
}
#[wasm_bindgen_test]
fn test_visualization_manager() {
let fixture = Fixture::new();
let node_id = fixture.node_id;
let fixture = VisOperationsTester::new(fixture);
let desired_vis_1 = fixture.vis_metadata("expr1");
let desired_vis_2 = fixture.vis_metadata("expr2");
let VisOperationsTester{mut requests,manager,mut inner,is_ready,..} = fixture;
// No requests are sent before execution context is ready.
manager.request_visualization(node_id, desired_vis_1.clone());
manager.request_visualization(node_id, desired_vis_2.clone());
manager.request_visualization(node_id, desired_vis_1.clone());
manager.request_visualization(node_id, desired_vis_1.clone());
manager.request_visualization(node_id, desired_vis_2.clone());
inner.run_until_stalled();
requests.expect_pending();
// After signalling readiness, only the most recent visualization is attached.
is_ready.replace(true);
inner.run_until_stalled();
let request = requests.expect_one();
assert_matches!(request, ExecutionContextRequest::Attach(vis)
if matching_metadata(&manager, &vis, &desired_vis_2));
// Multiple detach-attach requests are collapsed into a single modify request.
requests.expect_pending();
manager.remove_visualization(node_id);
manager.request_visualization(node_id, desired_vis_2.clone());
manager.remove_visualization(node_id);
manager.remove_visualization(node_id);
manager.request_visualization(node_id, desired_vis_1.clone());
manager.request_visualization(node_id, desired_vis_1.clone());
inner.run_until_stalled();
assert_matches!(requests.expect_one(),
ExecutionContextRequest::Modify{expression,..} if expression.contains(&desired_vis_1.preprocessor.code.to_string()));
// If visualization changes ID, then we need to use detach-attach API.
// We don't attach it separately, as Manager identifies visualizations by their
// expression id rather than visualization id.
let desired_vis_3 = Desired {
visualization_id : VisualizationId::from_u128(900),
expression_id : node_id,
metadata : desired_vis_1.clone(),
};
let visualization_so_far = manager.get_cloned(node_id).unwrap().status.get_cloned();
manager.write_new_desired(node_id, Some(desired_vis_3.clone()));
inner.run_until_stalled();
// let request = requests.expect_next();
match requests.expect_next() {
ExecutionContextRequest::Detach(id) =>
assert_eq!(id, visualization_so_far.latest_id().unwrap()),
other =>
panic!("Expected a detach request, got: {:?}",other),
}
assert_matches!(requests.expect_next(), ExecutionContextRequest::Attach(vis)
if matching_metadata(&manager,&vis,&desired_vis_3.metadata));
}
}

View File

@ -15,6 +15,8 @@
#![feature(result_cloned)]
#![feature(result_into_ok_or_err)]
#![feature(map_try_insert)]
#![feature(assert_matches)]
#![feature(cell_filter_map)]
#![recursion_limit="512"]
#![warn(missing_docs)]
#![warn(trivial_casts)]
@ -33,6 +35,7 @@ pub mod executor;
pub mod ide;
pub mod model;
pub mod notification;
pub mod sync;
pub mod test;
pub mod transport;
@ -54,6 +57,8 @@ pub mod prelude {
pub use ast::prelude::*;
pub use wasm_bindgen::prelude::*;
pub use enso_logger::DefaultTraceLogger as Logger;
pub use crate::constants;
pub use crate::controller;
pub use crate::double_representation;

View File

@ -15,6 +15,7 @@ use enso_protocol::language_server::MethodPointer;
use enso_protocol::language_server::SuggestionId;
use enso_protocol::language_server::VisualisationConfiguration;
use flo_stream::Subscriber;
use mockall::automock;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
@ -209,34 +210,33 @@ pub struct LocalCall {
pub type VisualizationId = Uuid;
/// Description of the visualization setup.
#[derive(Clone,Debug)]
#[derive(Clone,Debug,PartialEq)]
pub struct Visualization {
/// Unique identifier of this visualization.
pub id: VisualizationId,
/// Node that is to be visualized.
pub ast_id: ExpressionId,
/// Expression that is to be visualized.
pub expression_id: ExpressionId,
/// An enso lambda that will transform the data into expected format, e.g. `a -> a.json`.
pub expression: String,
/// Visualization module - the module in which context the expression should be evaluated.
pub visualisation_module:ModuleQualifiedName
pub preprocessor_code: String,
/// Visualization module -- the module in which context the preprocessor code is evaluated.
pub context_module:ModuleQualifiedName
}
impl Visualization {
/// Creates a new visualization description. The visualization will get a randomly assigned
/// identifier.
pub fn new
(ast_id:ExpressionId, expression:impl Into<String>, visualisation_module:ModuleQualifiedName)
(expression_id:ExpressionId, preprocessor_code:String, context_module:ModuleQualifiedName)
-> Visualization {
let id = VisualizationId::new_v4();
let expression = expression.into();
Visualization {id,ast_id,expression,visualisation_module}
let id = VisualizationId::new_v4();
Visualization {id,expression_id,preprocessor_code,context_module}
}
/// Creates a `VisualisationConfiguration` that is used in communication with language server.
pub fn config
(&self, execution_context_id:Uuid) -> VisualisationConfiguration {
let expression = self.expression.clone();
let visualisation_module = self.visualisation_module.to_string();
let expression = self.preprocessor_code.clone();
let visualisation_module = self.context_module.to_string();
VisualisationConfiguration {execution_context_id,visualisation_module,expression}
}
}
@ -265,7 +265,14 @@ pub struct AttachedVisualization {
// =============
/// Execution Context Model API.
#[automock]
pub trait API : Debug {
/// Future that gets ready when execution context becomes ready (i.e. completed first
/// evaluation).
///
/// If execution context was already ready, returned future will be ready from the beginning.
fn when_ready(&self) -> StaticBoxFuture<Option<()>>;
/// Obtain the method pointer to the method of the call stack's top frame.
fn current_method(&self) -> MethodPointer;
@ -286,27 +293,33 @@ pub trait API : Debug {
fn stack_items<'a>(&'a self) -> Box<dyn Iterator<Item=LocalCall> + 'a>;
/// Push a new stack item to execution context.
fn push(&self, stack_item:LocalCall) -> BoxFuture<FallibleResult>;
#[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes
fn push<'a>(&'a self, stack_item:LocalCall) -> BoxFuture<'a, FallibleResult>;
/// Pop the last stack item from this context. It returns error when only root call remains.
fn pop(&self) -> BoxFuture<FallibleResult<LocalCall>>;
#[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes
fn pop<'a>(&'a self) -> BoxFuture<'a, FallibleResult<LocalCall>>;
/// Attach a new visualization for current execution context.
///
/// Returns a stream of visualization update data received from the server.
fn attach_visualization
(&self, visualization:Visualization)
-> BoxFuture<FallibleResult<futures::channel::mpsc::UnboundedReceiver<VisualizationUpdateData>>>;
#[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes
fn attach_visualization<'a>
(&'a self, visualization:Visualization)
-> BoxFuture<'a, FallibleResult<futures::channel::mpsc::UnboundedReceiver<VisualizationUpdateData>>>;
/// Detach the visualization from this execution context.
fn detach_visualization
(&self, id:VisualizationId) -> BoxFuture<FallibleResult<Visualization>>;
#[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes
fn detach_visualization<'a>
(&'a self, id:VisualizationId) -> BoxFuture<'a, FallibleResult<Visualization>>;
/// Modify visualization properties. See fields in [`Visualization`] structure. Passing `None`
/// retains the old value.
fn modify_visualization
(&self, id:VisualizationId, expression:Option<String>, module:Option<ModuleQualifiedName>)
-> BoxFuture<FallibleResult>;
#[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes
fn modify_visualization<'a>
(&'a self, id:VisualizationId, expression:Option<String>, module:Option<ModuleQualifiedName>)
-> BoxFuture<'a, FallibleResult>;
/// Dispatches the visualization update data (typically received from as LS binary notification)
/// to the respective's visualization update channel.
@ -317,7 +330,8 @@ pub trait API : Debug {
///
/// The requests are made in parallel (not one by one). Any number of them might fail.
/// Results for each visualization that was attempted to be removed are returned.
fn detach_all_visualizations(&self) -> BoxFuture<Vec<FallibleResult<Visualization>>> {
#[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes
fn detach_all_visualizations<'a>(&'a self) -> BoxFuture<'a, Vec<FallibleResult<Visualization>>> {
let visualizations = self.active_visualizations();
let detach_actions = visualizations.into_iter().map(move |v| {
self.detach_visualization(v)
@ -326,6 +340,16 @@ pub trait API : Debug {
}
}
// Note: Needless lifetimes
// ~~~~~~~~~~~~~~~~~~~~~~~~
// See Note: [Needless lifetimes] is `model/project.rs`.
impl Debug for MockAPI {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f,"Mock Execution Context")
}
}
/// The general, shared Execution Context Model handle.
pub type ExecutionContext = Rc<dyn API>;
/// Execution Context Model which does not do anything besides storing data.

View File

@ -56,6 +56,8 @@ pub struct ExecutionContext {
visualizations: RefCell<HashMap<VisualizationId,AttachedVisualization>>,
/// Storage for information about computed values (like their types).
pub computed_value_info_registry:Rc<ComputedValueInfoRegistry>,
/// Execution context is considered ready once it completes it first execution after creation.
pub is_ready : crate::sync::Synchronized<bool>
}
impl ExecutionContext {
@ -65,7 +67,8 @@ impl ExecutionContext {
let stack = default();
let visualizations = default();
let computed_value_info_registry = default();
Self {logger,entry_point,stack,visualizations, computed_value_info_registry }
let is_ready = default();
Self {logger,entry_point,stack,visualizations,computed_value_info_registry,is_ready }
}
/// Creates a `VisualisationConfiguration` for the visualization with given id. It may be used
@ -118,8 +121,8 @@ impl ExecutionContext {
let err = || InvalidVisualizationId(id);
let mut visualizations = self.visualizations.borrow_mut();
let visualization = &mut visualizations.get_mut(&id).ok_or_else(err)?.visualization;
if let Some(expression) = expression { visualization.expression = expression; }
if let Some(module) = module { visualization.visualisation_module = module; }
if let Some(expression) = expression { visualization.preprocessor_code = expression; }
if let Some(module) = module { visualization.context_module = module; }
Ok(())
}
@ -136,6 +139,10 @@ impl ExecutionContext {
}
impl model::execution_context::API for ExecutionContext {
fn when_ready(&self) -> StaticBoxFuture<Option<()>> {
self.is_ready.when_eq(&true).boxed_local()
}
fn current_method(&self) -> MethodPointer {
if let Some(top_frame) = self.stack.borrow().last() {
top_frame.definition.clone()

View File

@ -10,7 +10,30 @@ use crate::model::execution_context::VisualizationId;
use crate::model::module;
use enso_protocol::language_server;
use enso_protocol::language_server::ExpressionUpdates;
// ====================
// === Notification ===
// ====================
/// Notification received by the synchronized execution context.
///
/// They are based on the relevant language server notifications.
#[derive(Clone,Debug)]
pub enum Notification {
/// Evaluation of this execution context has completed successfully.
///
/// It does not mean that there are no errors or panics, "successful" refers to the interpreter
/// run itself. This notification is expected basically on each computation that does not crash
/// the compiler.
Completed,
/// Visualization update data.
///
/// Execution context is responsible for routing them into the computed value registry.
ExpressionUpdates(Vec<language_server::ExpressionUpdate>),
}
// ==========================
@ -50,7 +73,7 @@ impl ExecutionContext {
let logger = Logger::new_sub(&parent,iformat!{"ExecutionContext {id}"});
let model = model::execution_context::Plain::new(&logger,root_definition);
info!(logger, "Created. Id: {id}.");
let this = Self {id,model,language_server,logger };
let this = Self {id,model,language_server,logger};
this.push_root_frame().await?;
info!(this.logger, "Pushed root frame.");
Ok(this)
@ -77,7 +100,7 @@ impl ExecutionContext {
(&self, vis:Visualization) -> FallibleResult<Visualization> {
let vis_id = vis.id;
let exe_id = self.id;
let ast_id = vis.ast_id;
let ast_id = vis.expression_id;
let ls = self.language_server.clone_ref();
let logger = self.logger.clone_ref();
info!(logger,"About to detach visualization by id: {vis_id}.");
@ -89,14 +112,27 @@ impl ExecutionContext {
}
/// Handles the update about expressions being computed.
pub fn handle_expression_updates
(&self, notification:ExpressionUpdates) -> FallibleResult {
self.model.computed_value_info_registry.apply_updates(notification.updates);
pub fn handle_notification
(&self, notification: Notification) -> FallibleResult {
match notification {
Notification::Completed => {
if !self.model.is_ready.replace(true) {
WARNING!("Context {self.id} Became ready");
}
}
Notification::ExpressionUpdates(updates) => {
self.model.computed_value_info_registry.apply_updates(updates);
}
}
Ok(())
}
}
impl model::execution_context::API for ExecutionContext {
fn when_ready(&self) -> StaticBoxFuture<Option<()>> {
self.model.when_ready()
}
fn current_method(&self) -> language_server::MethodPointer {
self.model.current_method()
}
@ -157,8 +193,9 @@ impl model::execution_context::API for ExecutionContext {
// has been successfully attached.
let config = vis.config(self.id);
let stream = self.model.attach_visualization(vis.clone());
async move {
let result = self.language_server.attach_visualisation(&vis.id,&vis.ast_id,&config).await;
let result = self.language_server.attach_visualisation(&vis.id, &vis.expression_id, &config).await;
if let Err(e) = result {
self.model.detach_visualization(vis.id)?;
Err(e.into())
@ -225,6 +262,7 @@ pub mod test {
use crate::model::traits::*;
use enso_protocol::language_server::CapabilityRegistration;
use enso_protocol::language_server::ExpressionUpdates;
use enso_protocol::language_server::response::CreateExecutionContext;
use json_rpc::expect_call;
use utils::test::ExpectTuple;
@ -254,7 +292,7 @@ pub mod test {
let method = data.main_method_pointer();
let context = ExecutionContext::create(logger,connection,method);
let context = test.expect_completion(context).unwrap();
Fixture {test,data,context}
Fixture {data,context,test}
}
/// What is expected server's response to a successful creation of this context.
@ -346,15 +384,15 @@ pub mod test {
#[test]
fn attaching_visualizations_and_notifying() {
let vis = Visualization {
id : model::execution_context::VisualizationId::new_v4(),
ast_id : model::execution_context::ExpressionId::new_v4(),
expression : "".to_string(),
visualisation_module : MockData::new().module_qualified_name(),
id : model::execution_context::VisualizationId::new_v4(),
expression_id : model::execution_context::ExpressionId::new_v4(),
preprocessor_code : "".to_string(),
context_module : MockData::new().module_qualified_name(),
};
let Fixture{mut test,context,..} = Fixture::new_customized(|ls,data| {
let exe_id = data.context_id;
let vis_id = vis.id;
let ast_id = vis.ast_id;
let ast_id = vis.expression_id;
let config = vis.config(exe_id);
expect_call!(ls.attach_visualisation(vis_id,ast_id,config) => Ok(()));
@ -391,9 +429,9 @@ pub mod test {
fn detaching_all_visualizations() {
let vis = Visualization {
id : model::execution_context::VisualizationId::new_v4(),
ast_id : model::execution_context::ExpressionId::new_v4(),
expression : "".to_string(),
visualisation_module : MockData::new().module_qualified_name(),
expression_id : model::execution_context::ExpressionId::new_v4(),
preprocessor_code : "".to_string(),
context_module : MockData::new().module_qualified_name(),
};
let vis2 = Visualization{
id : VisualizationId::new_v4(),
@ -404,7 +442,7 @@ pub mod test {
let exe_id = data.context_id;
let vis_id = vis.id;
let vis2_id = vis2.id;
let ast_id = vis.ast_id;
let ast_id = vis.expression_id;
let config = vis.config(exe_id);
let config2 = vis2.config(exe_id);
@ -425,17 +463,17 @@ pub mod test {
#[test]
fn modifying_visualizations() {
let vis = Visualization {
id : model::execution_context::VisualizationId::new_v4(),
ast_id : model::execution_context::ExpressionId::new_v4(),
expression : "x -> x.to_json.to_string".to_string(),
visualisation_module : MockData::new().module_qualified_name(),
id : model::execution_context::VisualizationId::new_v4(),
expression_id : model::execution_context::ExpressionId::new_v4(),
preprocessor_code : "x -> x.to_json.to_string".to_string(),
context_module : MockData::new().module_qualified_name(),
};
let vis_id = vis.id;
let new_expression = "x -> x";
let new_module = "Test.Test_Module";
let Fixture{mut test,context,..} = Fixture::new_customized(|ls,data| {
let exe_id = data.context_id;
let ast_id = vis.ast_id;
let ast_id = vis.expression_id;
let config = vis.config(exe_id);
let expected_config = language_server::types::VisualisationConfiguration {

View File

@ -357,7 +357,7 @@ pub struct NodeMetadata {
/// Was node selected in the view.
#[serde(default)]
pub selected:bool,
/// Was node selected in the view.
/// Information about enabled visualization. Exact format is defined by the integration layer.
#[serde(default)]
pub visualization:serde_json::Value,
}

View File

@ -615,7 +615,7 @@ pub mod test {
let module = fixture.synchronized_module();
let new_content = "main =\n println \"Test\"".to_string();
let new_ast = parser.parse_module(new_content.clone(), default()).unwrap();
let new_ast = parser.parse_module(new_content,default()).unwrap();
module.update_ast(new_ast).unwrap();
runner.perhaps_run_until_stalled(&mut fixture);
let change = TextChange {

View File

@ -239,10 +239,17 @@ pub mod test {
project.expect_name().returning_st(move || name.clone());
}
/// Sets up name expectation on the mock project, returning a given name.
pub fn expect_qualified_name
(project:&mut MockAPI, name:&QualifiedName) {
let name = name.clone();
project.expect_qualified_name().returning_st(move || name.clone());
}
pub fn expect_qualified_module_name
(project:&mut MockAPI) {
let name = project.qualified_name();
project.expect_qualified_module_name()
.returning_st(move |path:&model::module::Path|
path.qualified_module_name(name.clone()));
}
}

View File

@ -5,6 +5,7 @@ use crate::prelude::*;
use crate::double_representation::identifier::ReferentName;
use crate::double_representation::project::QualifiedName;
use crate::model::execution_context::VisualizationUpdateData;
use crate::model::execution_context::synchronized::Notification as ExecutionUpdate;
use crate::model::execution_context;
use crate::model::module;
use crate::model::SuggestionDatabase;
@ -15,7 +16,9 @@ use crate::transport::web::WebSocket;
use enso_protocol::binary;
use enso_protocol::binary::message::VisualisationContext;
use enso_protocol::language_server;
use enso_protocol::language_server::{CapabilityRegistration, ContentRoot};
use enso_protocol::language_server::CapabilityRegistration;
use enso_protocol::language_server::ContentRoot;
use enso_protocol::language_server::ExpressionUpdates;
use enso_protocol::language_server::MethodPointer;
use enso_protocol::project_manager;
use enso_protocol::project_manager::MissingComponentAction;
@ -74,10 +77,10 @@ impl ExecutionContextsRegistry {
}
/// Handles the update about expressions being computed.
pub fn handle_expression_updates
(&self, update:language_server::ExpressionUpdates) -> FallibleResult {
self.with_context(update.context_id, |ctx| {
ctx.handle_expression_updates(update)
pub fn handle_update
(&self, id:execution_context::Id, update:ExecutionUpdate) -> FallibleResult {
self.with_context(id, |ctx| {
ctx.handle_notification(update)
})
}
@ -366,6 +369,25 @@ impl Project {
}
}
/// Handler that routes execution updates to their respective contexts.
///
/// The function has a weak handle to the execution context registry, will stop working once
/// the registry is dropped.
pub fn execution_update_handler(&self) -> impl Fn(execution_context::Id, ExecutionUpdate) + Clone {
let logger = self.logger.clone_ref();
let registry = Rc::downgrade(&self.execution_contexts);
move |id,update| {
if let Some(registry) = registry.upgrade() {
if let Err(error) = registry.handle_update(id, update) {
error!(logger,"Failed to handle the execution context update: {error}");
}
} else {
warning!(logger,"Received an execution context notification despite execution \
context being already dropped.");
}
}
}
/// Returns a handling function capable of processing updates from the json-rpc protocol.
/// Such function will be then typically used to process events stream from the json-rpc
/// connection handler.
@ -377,26 +399,25 @@ impl Project {
// underlying RPC handlers and their types are separate.
// This generalization should be reconsidered once the old JSON-RPC handler is phased out.
// See: https://github.com/enso-org/ide/issues/587
let logger = self.logger.clone_ref();
let publisher = self.notifications.clone_ref();
let weak_execution_contexts = Rc::downgrade(&self.execution_contexts);
let weak_suggestion_db = Rc::downgrade(&self.suggestion_db);
let weak_content_roots = Rc::downgrade(&self.content_roots);
let logger = self.logger.clone_ref();
let publisher = self.notifications.clone_ref();
let weak_suggestion_db = Rc::downgrade(&self.suggestion_db);
let weak_content_roots = Rc::downgrade(&self.content_roots);
let execution_update_handler = self.execution_update_handler();
move |event| {
debug!(logger, "Received an event from the json-rpc protocol: {event:?}");
use enso_protocol::language_server::Event;
use enso_protocol::language_server::Notification;
match event {
Event::Notification(Notification::FileEvent(_)) => {}
Event::Notification(Notification::ExpressionUpdates(updates)) => {
if let Some(execution_contexts) = weak_execution_contexts.upgrade() {
let result = execution_contexts.handle_expression_updates(updates);
if let Err(error) = result {
error!(logger,"Failed to handle the expression update: {error}");
}
} else {
error!(logger,"Received a `ExpressionUpdates` update despite execution \
context being already dropped.");
}
let ExpressionUpdates{context_id,updates} = updates;
let execution_update = ExecutionUpdate::ExpressionUpdates(updates);
execution_update_handler(context_id,execution_update);
}
Event::Notification(Notification::ExecutionStatus(_)) => {}
Event::Notification(Notification::ExecutionComplete {context_id}) => {
execution_update_handler(context_id,ExecutionUpdate::Completed);
}
Event::Notification(Notification::ExpressionValuesComputed(_)) => {
// the notification is superseded by `ExpressionUpdates`.
@ -432,7 +453,6 @@ impl Project {
Event::Error(error) => {
error!(logger,"Error emitted by the JSON-RPC data connection: {error}.");
}
_ => {}
}
futures::future::ready(())
}

View File

@ -0,0 +1,147 @@
//! Synchronization facilities.
use crate::prelude::*;
/// Wrapper for a value that allows asynchronous observation of its updates.
#[derive(Derivative,CloneRef,Debug,Default)]
#[clone_ref(bound="")]
#[derivative(Clone(bound=""))]
pub struct Synchronized<T> {
value : Rc<RefCell<T>>,
notifier : crate::notification::Publisher<()>,
}
impl<T> Synchronized<T> {
/// Wrap a value into `Synchronized`.
pub fn new(t:T) -> Self {
Self {
value : Rc::new(RefCell::new(t)),
notifier : default(),
}
}
/// Replace the value with a new one. Return the previous value.
pub fn replace(&self, new_value:T) -> T {
let previous = std::mem::replace(self.value.borrow_mut().deref_mut(), new_value);
self.notifier.notify(());
previous
}
/// Take the value out and replace it with a default-constructed one.
pub fn take(&self) -> T where T:Default {
self.replace(default())
}
/// Uses a given function to update the stored value.
///
/// The function is invoked with value borrowed mutably, it must not use this value in any wya.
pub fn update<R>(&self, f:impl FnOnce(&mut T) -> R) -> R {
f(self.value.borrow_mut().deref_mut())
}
/// Get a copy of the stored value.
pub fn get_cloned(&self) -> T where T:Clone {
self.borrow().clone()
}
/// Borrow the store value.
pub fn borrow(&self) -> Ref<T> {
self.value.borrow()
}
/// Run given tester function on each value update. Stop when function returns `Some` value.
/// Forwards the returned value from the tester or `None` if the value was dropped before the
/// tester returned `Some`.
pub fn when_map<Condition,R>(&self, mut tester:Condition)
-> impl Future<Output=Option<R>> + 'static
where Condition : FnMut(&T) -> Option<R> + 'static,
R : 'static,
T : 'static, {
if let Some(ret) = tester(self.value.borrow().deref()) {
futures::future::ready(Some(ret)).left_future()
} else {
// We pass strong value reference to the future, because we want to able to notify
// our observers about changes, even if these notifications are processed after this
// object is dropped.
let value = self.value.clone_ref();
let tester = move |_:()| {
let result = tester(value.borrow().deref());
futures::future::ready(result)
};
self.notifier.subscribe()
.filter_map(tester)
.boxed_local()
.into_future()
.map(|(head, _tail)| head)
.right_future()
}
}
/// Get a Future that resolves once the value satisfies the condition checked by a given
/// function.
pub fn when<Condition>(&self, mut tester:Condition) -> impl Future<Output=Option<()>> + 'static
where Condition : FnMut(&T) -> bool + 'static,
T : 'static {
self.when_map(move |value| tester(value).as_some(()))
}
/// Get a Future that resolves once the value is equal to a given argument.
pub fn when_eq<U>(&self, u:U) -> impl Future<Output=Option<()>> + 'static
where for <'a> &'a T : PartialEq<U>,
U : 'static,
T : 'static {
self.when(move |val_ref| val_ref == u)
}
}
#[cfg(test)]
mod tests {
use super::*;
use utils::test::traits::*;
use crate::executor::test_utils::TestWithLocalPoolExecutor;
#[test]
fn synchronized() {
let mut fixture = TestWithLocalPoolExecutor::set_up();
let flag = Synchronized::new(false);
assert_eq!(*flag.borrow(), false);
// If condition was already met, be immediately ready.
let mut on_false = flag.when(|f| *f == false).boxed_local();
assert_eq!(on_false.expect_ready(), Some(()));
// Otherwise not ready.
let mut on_true = flag.when(|f| *f == true).boxed_local();
on_true.expect_pending();
// Faux no-op change. Should not spawn a new task.
flag.replace(false);
fixture.expect_finished();
on_true.expect_pending();
// Real change, future now should complete.
flag.replace(true);
fixture.expect_finished();
assert_eq!(on_true.expect_ready(), Some(()));
// After dropping the flag, pending future should complete with None.
let mut on_false = flag.when(|f| *f == false).boxed_local();
on_false.expect_pending();
drop(flag);
assert_eq!(on_false.expect_ready(), None);
}
#[test]
fn some_on_drop_before_notification() {
let mut fixture = TestWithLocalPoolExecutor::set_up();
let number = Synchronized::new(10);
let mut fut = number.when_map(|v| (*v == 0).then_some(())).boxed_local();
fixture.run_until_stalled();
fut.expect_pending();
number.replace(0);
drop(number);
assert_eq!(fixture.expect_completion(&mut fut), Some(()));
}
}

View File

@ -217,7 +217,7 @@ pub mod mock {
crate::controller::Graph::new(logger,module,db,parser,definition).expect("Graph could not be created")
}
pub fn execution_context(&self) -> model::ExecutionContext {
pub fn execution_context(&self) -> Rc<model::execution_context::Plain> {
let logger = Logger::new_sub(&self.logger,"Mocked Execution Context");
Rc::new(model::execution_context::Plain::new(logger,self.method_pointer()))
}
@ -234,6 +234,7 @@ pub mod mock {
let mut project = model::project::MockAPI::new();
model::project::test::expect_name(&mut project,&self.project_name.project);
model::project::test::expect_qualified_name(&mut project,&self.project_name);
model::project::test::expect_qualified_module_name(&mut project);
model::project::test::expect_parser(&mut project,&self.parser);
model::project::test::expect_module(&mut project,module);
model::project::test::expect_execution_ctx(&mut project,execution_context);
@ -313,18 +314,26 @@ pub mod mock {
}
}
#[derive(Debug)]
impl Default for Unified {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug,Shrinkwrap)]
#[shrinkwrap(mutable)]
pub struct Fixture {
pub logger : Logger,
pub data : Unified,
pub module : model::Module,
pub graph : controller::Graph,
pub execution : model::ExecutionContext,
pub execution : Rc<model::execution_context::Plain>,
pub executed_graph : controller::ExecutedGraph,
pub suggestion_db : Rc<model::SuggestionDatabase>,
pub project : model::Project,
pub ide : controller::Ide,
pub searcher : controller::Searcher,
#[shrinkwrap(main_field)]
pub executor : TestWithLocalPoolExecutor, // Last to drop the executor as last.
}
@ -367,6 +376,10 @@ pub mod mock {
};
(model,controller)
}
pub fn module_name(&self) -> model::module::QualifiedName {
self.module.path().qualified_module_name(self.project.qualified_name())
}
}
pub fn indent(line:impl AsRef<str>) -> String {

View File

@ -325,7 +325,7 @@ async fn binary_visualization_updates_test_hlp() {
let project = ide.current_project();
info!(logger,"Got project: {project:?}");
let expression = "x -> x.json_serialize";
let expression = "x -> x.json_serialize".to_owned();
use ensogl::system::web::sleep;
use controller::project::MAIN_DEFINITION_NAME;

View File

@ -18,7 +18,7 @@ impl Metadata {
pub fn new
(preprocessor:&visualization::instance::PreprocessorConfiguration) -> Self {
Self {
preprocessor : preprocessor.clone_ref(),
preprocessor:preprocessor.clone_ref(),
}
}
}