From 3985f1efab6359ff3286c5728d6fa39f04cb39ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wawrzyniec=20Urba=C5=84czyk?= Date: Fri, 24 Sep 2021 00:18:12 +0200 Subject: [PATCH] Delayed Visualization Attaching (#1825) --- CHANGELOG.md | 12 +- .../src/language_server/types.rs | 24 +- src/rust/ide/lib/utils/src/test/stream.rs | 10 + src/rust/ide/src/controller/graph/executed.rs | 12 + src/rust/ide/src/controller/project.rs | 20 +- .../ide/src/controller/searcher/action.rs | 2 +- src/rust/ide/src/ide/integration.rs | 1 + src/rust/ide/src/ide/integration/project.rs | 402 ++++------- .../ide/src/ide/integration/visualization.rs | 670 ++++++++++++++++++ src/rust/ide/src/lib.rs | 5 + src/rust/ide/src/model/execution_context.rs | 70 +- .../ide/src/model/execution_context/plain.rs | 13 +- .../model/execution_context/synchronized.rs | 82 ++- src/rust/ide/src/model/module.rs | 2 +- src/rust/ide/src/model/module/synchronized.rs | 2 +- src/rust/ide/src/model/project.rs | 9 +- .../ide/src/model/project/synchronized.rs | 60 +- src/rust/ide/src/sync.rs | 147 ++++ src/rust/ide/src/test.rs | 19 +- src/rust/ide/tests/language_server.rs | 2 +- .../src/component/visualization/metadata.rs | 2 +- 21 files changed, 1206 insertions(+), 360 deletions(-) create mode 100644 src/rust/ide/src/ide/integration/visualization.rs create mode 100644 src/rust/ide/src/sync.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 39d58f0121..73432f3f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Next Release +
![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)
![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 diff --git a/src/rust/ide/lib/enso-protocol/src/language_server/types.rs b/src/rust/ide/lib/enso-protocol/src/language_server/types.rs index 04adffb2f2..f2bcb26862 100644 --- a/src/rust/ide/lib/enso-protocol/src/language_server/types.rs +++ b/src/rust/ide/lib/enso-protocol/src/language_server/types.rs @@ -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::(text).unwrap(); + let expected = Notification::ExecutionComplete { + context_id : ContextId::from_str("5a85125a-2dc5-45c8-84fc-679bc9fc4b00").unwrap() + }; + assert_eq!(notification,expected); + } } diff --git a/src/rust/ide/lib/utils/src/test/stream.rs b/src/rust/ide/lib/utils/src/test/stream.rs index debc768580..718e2a3bfd 100644 --- a/src/rust/ide/lib/utils/src/test/stream.rs +++ b/src/rust/ide/lib/utils/src/test/stream.rs @@ -33,6 +33,16 @@ pub trait StreamTestExt { } } + /// 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`. diff --git a/src/rust/ide/src/controller/graph/executed.rs b/src/rust/ide/src/controller/graph/executed.rs index 0019cfe541..cc2c54877d 100644 --- a/src/rust/ide/src/controller/graph/executed.rs +++ b/src/rust/ide/src/controller/graph/executed.rs @@ -116,6 +116,11 @@ impl Handle { Handle {logger,graph,execution_ctx,project,notifier} } + /// See [`model::ExecutionContext::when_ready`]. + pub fn when_ready(&self) -> StaticBoxFuture> { + 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, module:Option) + -> BoxFuture { + self.execution_ctx.modify_visualization(id,expression,module) + } + /// See [`model::ExecutionContext::detach_visualization`]. pub async fn detach_visualization(&self, id:VisualizationId) -> FallibleResult { self.execution_ctx.detach_visualization(id).await diff --git a/src/rust/ide/src/controller/project.rs b/src/rust/ide/src/controller/project.rs index 269f7ef946..80cef1d520 100644 --- a/src/rust/ide/src/controller/project.rs +++ b/src/rust/ide/src/controller/project.rs @@ -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!") + } }); } diff --git a/src/rust/ide/src/controller/searcher/action.rs b/src/rust/ide/src/controller/searcher/action.rs index af3a263f6f..e05bfacd91 100644 --- a/src/rust/ide/src/controller/searcher/action.rs +++ b/src/rust/ide/src/controller/searcher/action.rs @@ -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} } } diff --git a/src/rust/ide/src/ide/integration.rs b/src/rust/ide/src/ide/integration.rs index 8252acafaf..71dc4d0cba 100644 --- a/src/rust/ide/src/ide/integration.rs +++ b/src/rust/ide/src/ide/integration.rs @@ -2,6 +2,7 @@ pub mod project; pub mod file_system; +pub mod visualization; use crate::prelude::*; diff --git a/src/rust/ide/src/ide/integration/project.rs b/src/rust/ide/src/ide/integration/project.rs index b165c46044..83f6ec311b 100644 --- a/src/rust/ide/src/ide/integration/project.rs +++ b/src/rust/ide/src/ide/integration/project.rs @@ -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; - @@ -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>, connection_views : RefCell>, code_view : CloneRefCell, - visualizations : VisualizationMap, - error_visualizations : VisualizationMap, + visualizations : Rc, + error_visualizations : Rc, prompt_was_shown : Cell, displayed_project_list : CloneRefCell, } @@ -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) + '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 { 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.spawn_visualization_handler(visualizations_notifications, WhichVisualization::Normal); + this.spawn_visualization_handler(error_visualizations_notifications, WhichVisualization::Error); - this.view.graph().frp.remove_all_nodes(); + 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 + , notifier : impl Stream + Unpin + 'static + , visualizations_kind : WhichVisualization + ) { + let weak = Rc::downgrade(self); + let processor = async move |notification, this:Rc| { + 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 { + 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 + ) -> 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) { @@ -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) -> 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 { - 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) { 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 { - // 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>> { - 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 { - 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 { - 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, 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); } diff --git a/src/rust/ide/src/ide/integration/visualization.rs b/src/rust/ide/src/ide/integration/visualization.rs new file mode 100644 index 0000000000..cfe81ee53f --- /dev/null +++ b/src/rust/ide/src/ide/integration/visualization.rs @@ -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 { + 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 { + 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, + /// What we know about Language Server state of the visualization. + pub status : Synchronized, +} + +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> { + 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 { + 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, + executed_graph : ExecutedGraph, + project : model::Project, + notification_sender : futures::channel::mpsc::UnboundedSender, +} + +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,UnboundedReceiver) { + 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> { + 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 { + 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 { + 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, 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, target:ast::Id) + -> Option { + 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, 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, target:ast::Id, new_desired:Option) { + 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, target:ast::Id, new_desired:Option) { + 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 { + resolve_context_module(context_module,|| self.project.main_module()) + } + + fn prepare_visualization(&self, desired:Desired) -> FallibleResult { + 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, 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) -> 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, + module : Option, + }, + } + + #[derive(Shrinkwrap)] + #[shrinkwrap(mutable)] + struct VisOperationsTester { + #[shrinkwrap(main_field)] + pub inner : Fixture, + pub is_ready : Synchronized, + pub manager : Rc, + pub notifier : UnboundedReceiver, + pub requests : StaticBoxStream, + } + + 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)); + } +} diff --git a/src/rust/ide/src/lib.rs b/src/rust/ide/src/lib.rs index 2bcfdb36f5..2e4af634a1 100644 --- a/src/rust/ide/src/lib.rs +++ b/src/rust/ide/src/lib.rs @@ -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; diff --git a/src/rust/ide/src/model/execution_context.rs b/src/rust/ide/src/model/execution_context.rs index 605af98790..f6ebe18ba4 100644 --- a/src/rust/ide/src/model/execution_context.rs +++ b/src/rust/ide/src/model/execution_context.rs @@ -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, 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>; + /// 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 + 'a>; /// Push a new stack item to execution context. - fn push(&self, stack_item:LocalCall) -> BoxFuture; + #[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>; + #[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes + fn pop<'a>(&'a self) -> BoxFuture<'a, FallibleResult>; /// 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>>; + #[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes + fn attach_visualization<'a> + (&'a self, visualization:Visualization) + -> BoxFuture<'a, FallibleResult>>; + /// Detach the visualization from this execution context. - fn detach_visualization - (&self, id:VisualizationId) -> BoxFuture>; + #[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes + fn detach_visualization<'a> + (&'a self, id:VisualizationId) -> BoxFuture<'a, FallibleResult>; /// Modify visualization properties. See fields in [`Visualization`] structure. Passing `None` /// retains the old value. - fn modify_visualization - (&self, id:VisualizationId, expression:Option, module:Option) - -> BoxFuture; + #[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes + fn modify_visualization<'a> + (&'a self, id:VisualizationId, expression:Option, module:Option) + -> 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>> { + #[allow(clippy::needless_lifetimes)] // Note: Needless lifetimes + fn detach_all_visualizations<'a>(&'a self) -> BoxFuture<'a, Vec>> { 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; /// Execution Context Model which does not do anything besides storing data. diff --git a/src/rust/ide/src/model/execution_context/plain.rs b/src/rust/ide/src/model/execution_context/plain.rs index d6b6242ba2..ed1d8d7806 100644 --- a/src/rust/ide/src/model/execution_context/plain.rs +++ b/src/rust/ide/src/model/execution_context/plain.rs @@ -56,6 +56,8 @@ pub struct ExecutionContext { visualizations: RefCell>, /// Storage for information about computed values (like their types). pub computed_value_info_registry:Rc, + /// Execution context is considered ready once it completes it first execution after creation. + pub is_ready : crate::sync::Synchronized } 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> { + 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() diff --git a/src/rust/ide/src/model/execution_context/synchronized.rs b/src/rust/ide/src/model/execution_context/synchronized.rs index 247cafad6e..9306d93b69 100644 --- a/src/rust/ide/src/model/execution_context/synchronized.rs +++ b/src/rust/ide/src/model/execution_context/synchronized.rs @@ -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), +} + // ========================== @@ -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 { 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> { + 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 { diff --git a/src/rust/ide/src/model/module.rs b/src/rust/ide/src/model/module.rs index 334758c370..c8e4f4f53a 100644 --- a/src/rust/ide/src/model/module.rs +++ b/src/rust/ide/src/model/module.rs @@ -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, } diff --git a/src/rust/ide/src/model/module/synchronized.rs b/src/rust/ide/src/model/module/synchronized.rs index 203fc8102a..73bd686980 100644 --- a/src/rust/ide/src/model/module/synchronized.rs +++ b/src/rust/ide/src/model/module/synchronized.rs @@ -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 { diff --git a/src/rust/ide/src/model/project.rs b/src/rust/ide/src/model/project.rs index 793993e5f3..9dee404dd7 100644 --- a/src/rust/ide/src/model/project.rs +++ b/src/rust/ide/src/model/project.rs @@ -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())); + } } diff --git a/src/rust/ide/src/model/project/synchronized.rs b/src/rust/ide/src/model/project/synchronized.rs index 94889dc0ce..9e4aaeaefd 100644 --- a/src/rust/ide/src/model/project/synchronized.rs +++ b/src/rust/ide/src/model/project/synchronized.rs @@ -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(()) } diff --git a/src/rust/ide/src/sync.rs b/src/rust/ide/src/sync.rs new file mode 100644 index 0000000000..ab50ece245 --- /dev/null +++ b/src/rust/ide/src/sync.rs @@ -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 { + value : Rc>, + notifier : crate::notification::Publisher<()>, +} + +impl Synchronized { + /// 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(&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 { + 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(&self, mut tester:Condition) + -> impl Future> + 'static + where Condition : FnMut(&T) -> Option + '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(&self, mut tester:Condition) -> impl Future> + '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(&self, u:U) -> impl Future> + 'static + where for <'a> &'a T : PartialEq, + 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(())); + } +} diff --git a/src/rust/ide/src/test.rs b/src/rust/ide/src/test.rs index f4d437ecb2..f51c51f581 100644 --- a/src/rust/ide/src/test.rs +++ b/src/rust/ide/src/test.rs @@ -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 { 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, pub executed_graph : controller::ExecutedGraph, pub suggestion_db : Rc, 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) -> String { diff --git a/src/rust/ide/tests/language_server.rs b/src/rust/ide/tests/language_server.rs index bd424cf24b..49a3af09fc 100644 --- a/src/rust/ide/tests/language_server.rs +++ b/src/rust/ide/tests/language_server.rs @@ -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; diff --git a/src/rust/ide/view/graph-editor/src/component/visualization/metadata.rs b/src/rust/ide/view/graph-editor/src/component/visualization/metadata.rs index 5363c47e44..8595a40941 100644 --- a/src/rust/ide/view/graph-editor/src/component/visualization/metadata.rs +++ b/src/rust/ide/view/graph-editor/src/component/visualization/metadata.rs @@ -18,7 +18,7 @@ impl Metadata { pub fn new (preprocessor:&visualization::instance::PreprocessorConfiguration) -> Self { Self { - preprocessor : preprocessor.clone_ref(), + preprocessor:preprocessor.clone_ref(), } } }