mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 13:02:07 +03:00
Delayed Visualization Attaching (https://github.com/enso-org/ide/pull/1825)
Original commit: 3985f1efab
This commit is contained in:
parent
82e74121b9
commit
10bac80ee8
@ -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
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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`.
|
||||
|
@ -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
|
||||
|
@ -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!")
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
pub mod project;
|
||||
pub mod file_system;
|
||||
pub mod visualization;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
670
gui/src/rust/ide/src/ide/integration/visualization.rs
Normal file
670
gui/src/rust/ide/src/ide/integration/visualization.rs
Normal 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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
147
gui/src/rust/ide/src/sync.rs
Normal file
147
gui/src/rust/ide/src/sync.rs
Normal 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(()));
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -18,7 +18,7 @@ impl Metadata {
|
||||
pub fn new
|
||||
(preprocessor:&visualization::instance::PreprocessorConfiguration) -> Self {
|
||||
Self {
|
||||
preprocessor : preprocessor.clone_ref(),
|
||||
preprocessor:preprocessor.clone_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user