From 4b14e163f1825e46e88b0b10b44d1ea3f2a57cc2 Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Fri, 21 May 2021 15:24:17 +0200 Subject: [PATCH] Create new project action in searcher. (https://github.com/enso-org/ide/pull/1566) Original commit: https://github.com/enso-org/ide/commit/5adc95289dc6359fc56146bd9796f74009426029 --- ide/CHANGELOG.md | 39 +- .../src/language_server/types.rs | 1 - .../lib/enso-protocol/src/project_manager.rs | 10 +- ide/src/rust/ide/src/controller.rs | 18 +- ide/src/rust/ide/src/controller/ide.rs | 150 ++ .../rust/ide/src/controller/ide/desktop.rs | 113 ++ ide/src/rust/ide/src/controller/ide/plain.rs | 83 + ide/src/rust/ide/src/controller/project.rs | 244 +++ ide/src/rust/ide/src/controller/searcher.rs | 93 +- .../ide/src/controller/searcher/action.rs | 20 +- ide/src/rust/ide/src/ide.rs | 150 +- ide/src/rust/ide/src/ide/initializer.rs | 119 +- ide/src/rust/ide/src/ide/integration.rs | 1490 ++--------------- .../rust/ide/src/ide/integration/project.rs | 1446 ++++++++++++++++ ide/src/rust/ide/src/model/project.rs | 2 +- .../ide/src/model/project/synchronized.rs | 97 +- ide/src/rust/ide/src/test.rs | 25 +- ide/src/rust/ide/src/tests.rs | 3 +- ide/src/rust/ide/tests/language_server.rs | 20 +- ide/src/rust/ide/view/src/status_bar.rs | 10 + 20 files changed, 2381 insertions(+), 1752 deletions(-) create mode 100644 ide/src/rust/ide/src/controller/ide.rs create mode 100644 ide/src/rust/ide/src/controller/ide/desktop.rs create mode 100644 ide/src/rust/ide/src/controller/ide/plain.rs create mode 100644 ide/src/rust/ide/src/controller/project.rs create mode 100644 ide/src/rust/ide/src/ide/integration/project.rs diff --git a/ide/CHANGELOG.md b/ide/CHANGELOG.md index fabb2107bf6b..9cb96fe3e532 100644 --- a/ide/CHANGELOG.md +++ b/ide/CHANGELOG.md @@ -4,17 +4,23 @@ #### Visual Environment +- [Create New Project action in Searcher][1566]. When you bring the searcher + with tab having no node selected, a new action will be available next to the + examples and code suggestions: `Create New Project`. When you choose it by + clicking with mouse or selecting and pressing enter, a new unnamed project + will be created and opened in the application. Then you can give a name to + this project. + +#### EnsoGL (rendering engine) + - [Components for picking numbers and ranges.][1524]. We now have some internal re-usable UI components for selecting numbers or a range. Stay tuned for them appearing in the IDE. -#### EnsoGL (rendering engine) -
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) #### Visual Environment -- [Delete key will delete selected nodes][1538]. - [It is possible to move around after deleting a node with a selected visualization][1556]. Deleting a node while its attached visualization was selected made it impossible to pan or zoom around the stage afterwards. This @@ -27,16 +33,37 @@ #### Enso Compiler +[1524]: https://github.com/enso-org/ide/pull/1524 +[1556]: https://github.com/enso-org/ide/pull/1556 +[1561]: https://github.com/enso-org/ide/pull/1561 +[1566]: https://github.com/enso-org/ide/pull/1566 + +# Enso 2.0.0-alpha.5 (2021-05-14) + +
![New Features](/docs/assets/tags/new_features.svg) + +#### Visual Environment + +#### EnsoGL (rendering engine) + +
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) + +#### Visual Environment + +- [Delete key will delete selected nodes][1538]. + +#### EnsoGL (rendering engine) + +#### Enso Compiler + - [Updated Enso engine to version 0.2.11][1541]. If you're interested in the enhancements and fixes made to the Enso compiler, you can find their release notes [here](https://github.com/enso-org/enso/blob/main/RELEASES.md). -[1524]: https://github.com/enso-org/ide/pull/1524 -[1541]: https://github.com/enso-org/ide/pull/1511 +[1541]: https://github.com/enso-org/ide/pull/1541 [1538]: https://github.com/enso-org/ide/pull/1538 -[1561]: https://github.com/enso-org/ide/pull/1561
diff --git a/ide/src/rust/ide/lib/enso-protocol/src/language_server/types.rs b/ide/src/rust/ide/lib/enso-protocol/src/language_server/types.rs index 4ae912d000d9..f17d2f919567 100644 --- a/ide/src/rust/ide/lib/enso-protocol/src/language_server/types.rs +++ b/ide/src/rust/ide/lib/enso-protocol/src/language_server/types.rs @@ -18,7 +18,6 @@ pub type Event = json_rpc::handler::Event; // ============ /// A path is a representation of a path relative to a specified content root. -// FIXME [mwu] Consider rename to something like `FilePath`, see https://github.com/enso-org/enso/issues/708 #[derive(Clone,Debug,Serialize,Deserialize,Hash,PartialEq,Eq)] #[serde(rename_all="camelCase")] pub struct Path { diff --git a/ide/src/rust/ide/lib/enso-protocol/src/project_manager.rs b/ide/src/rust/ide/lib/enso-protocol/src/project_manager.rs index 3d2fb8b41098..299657a66410 100644 --- a/ide/src/rust/ide/lib/enso-protocol/src/project_manager.rs +++ b/ide/src/rust/ide/lib/enso-protocol/src/project_manager.rs @@ -100,7 +100,7 @@ impl Display for IpWithSocket { } /// Project name. -#[derive(Debug,Display,Clone,Serialize,Deserialize,From,PartialEq,Shrinkwrap)] +#[derive(Clone,Debug,Deserialize,Display,Eq,From,Hash,PartialEq,Serialize,Shrinkwrap)] #[shrinkwrap(mutable)] pub struct ProjectName(pub String); @@ -111,6 +111,14 @@ impl ProjectName { } } +impl AsRef for ProjectName { + fn as_ref(&self) -> &str { &self.0 } +} + +impl From for String { + fn from(name:ProjectName) -> Self { name.0 } +} + /// Project information, such as name, its id and last time it was opened. #[derive(Debug,Clone,Serialize,Deserialize,PartialEq)] pub struct ProjectMetadata { diff --git a/ide/src/rust/ide/src/controller.rs b/ide/src/rust/ide/src/controller.rs index 72752b54a484..164c89a99571 100644 --- a/ide/src/rust/ide/src/controller.rs +++ b/ide/src/rust/ide/src/controller.rs @@ -2,28 +2,22 @@ //! between clients of remote services (like language server and file manager) //! and views. //! -//! The controllers create a tree-like structure, with project controller being -//! a root, then module controllers below, then graph/text controller and so on. -//! -//! As a general rule, while the "upper" (i.e. closer to root) nodes may keep -//! handles to the "lower" nodes (e.g. to allow their reuse), they should never -//! manage their lifetime. -//! -//! Primarily views are considered owners of their respective controllers. -//! Additionally, controllers are allowed to keep strong handle "upwards". -//! -//! Controllers store their handles using `utils::cell` handle types to ensure -//! that mutable state is safely accessed. +//! The API of each controller is "view-facing", in contrast to the models in [`crate::model`] which +//! are focusing on reflecting the Engine entities (thus can be called "Engine-facing"). pub mod graph; +pub mod ide; pub mod module; pub mod text; +pub mod project; pub mod visualization; pub mod searcher; pub use graph::Handle as Graph; pub use graph::executed::Handle as ExecutedGraph; +pub use self::ide::Ide; pub use module::Handle as Module; +pub use project::Project; pub use text::Handle as Text; pub use visualization::Handle as Visualization; pub use searcher::Searcher; diff --git a/ide/src/rust/ide/src/controller/ide.rs b/ide/src/rust/ide/src/controller/ide.rs new file mode 100644 index 000000000000..0be8807e1bac --- /dev/null +++ b/ide/src/rust/ide/src/controller/ide.rs @@ -0,0 +1,150 @@ +//! IDE controller +//! +//! The IDE controller expose functionality bound to the application as a whole, not to specific +//! component or opened project. + +pub mod desktop; +pub mod plain; + +use crate::prelude::*; + +use crate::notification; + +use mockall::automock; +use parser::Parser; + + + +// ============================ +// === Status Notifications === +// ============================ + +/// The handle used to pair the ProcessStarted and ProcessFinished notifications. +pub type BackgroundTaskHandle = usize; + +/// A notification which should be displayed to the User on the status bar. +#[allow(missing_docs)] +#[derive(Clone,Debug)] +pub enum StatusNotification { + /// Notification about single event, should be logged in an event log window. + Event { label:String }, + /// Notification about new background task done in IDE (like compiling library). + BackgroundTaskStarted { label:String, handle: BackgroundTaskHandle }, + /// Notification that some task notified in [`BackgroundTaskStarted`] has been finished. + BackgroundTaskFinished { handle:BackgroundTaskHandle }, +} + +/// A publisher for status notification events. +#[derive(Clone,CloneRef,Debug,Default)] +pub struct StatusNotificationPublisher { + publisher : notification::Publisher, + next_process_handle : Rc>, +} + +impl StatusNotificationPublisher { + /// Constructor. + pub fn new() -> Self { default() } + + /// Publish a new status event (see [`StatusNotification::Event`]) + pub fn publish_event(&self, label:impl Into) { + let label = label.into(); + let notification = StatusNotification::Event {label}; + executor::global::spawn(self.publisher.publish(notification)); + } + + /// Publish a notification about new process (see [`StatusNotification::ProcessStarted`]). + /// + /// Returns the handle to be used when notifying about process finishing. + pub fn publish_background_task(&self, label:impl Into) -> BackgroundTaskHandle { + let label = label.into(); + let handle = self.next_process_handle.get(); + self.next_process_handle.set(handle + 1); + let notification = StatusNotification::BackgroundTaskStarted {label,handle}; + executor::global::spawn(self.publisher.publish(notification)); + handle + } + + /// Publish a notfication that process has finished (see [`StatusNotification::ProcessFinished`]) + pub fn published_background_task_finished(&self, handle:BackgroundTaskHandle) { + let notification = StatusNotification::BackgroundTaskFinished {handle}; + executor::global::spawn(self.publisher.publish(notification)); + } + + /// The asynchronous stream of published notifications. + pub fn subscribe(&self) -> impl Stream { + self.publisher.subscribe() + } +} + + + +// ==================== +// === Notification === +// ==================== + +/// Notification of IDE Controller. +/// +/// In contrast to [`StatusNotification`], which is a notification from any application part to +/// be delivered to User (displayed on some event log or status bar), this is a notification to be +/// used internally in code. +#[derive(Copy,Clone,Debug)] +pub enum Notification { + /// User created a new project. The new project is opened in IDE. + NewProjectCreated +} + + + +// =========== +// === API === +// =========== + +/// The API of all project management operations. +/// +/// It is a separate trait, because those methods are not supported in some environments (see also +/// [`API::manage_projects`]). +pub trait ManagingProjectAPI { + + /// Create a new unnamed project and open it in the IDE. + fn create_new_project(&self) -> BoxFuture; +} + +/// The API of IDE Controller. +#[automock] +pub trait API:Debug { + /// The model of currently opened project. + /// + /// IDE can have only one project opened at a time. + fn current_project(&self) -> model::Project; + + /// Getter of Status Notification Publisher. + fn status_notifications(&self) -> &StatusNotificationPublisher; + + /// The Parser Handle. + fn parser(&self) -> &Parser; + + /// Subscribe the controller notifications. + fn subscribe(&self) -> StaticBoxStream; + + /// Return the Managing Project API. + /// + /// It may be some delegated object or just the reference to self. + // Automock macro does not work without explicit lifetimes here. + #[allow(clippy::needless_lifetimes)] + fn manage_projects<'a>(&'a self) -> FallibleResult<&'a dyn ManagingProjectAPI>; +} + +/// A polymorphic handle of IDE controller. +pub type Ide = Rc; + +/// The IDE Controller for desktop environments. +pub type Desktop = desktop::Handle; + +/// The Plain IDE controller with a single project and no possibility to change it. +pub type Plain = plain::Handle; + +impl Debug for MockAPI { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f,"Mocked Ide Controller") + } +} diff --git a/ide/src/rust/ide/src/controller/ide/desktop.rs b/ide/src/rust/ide/src/controller/ide/desktop.rs new file mode 100644 index 000000000000..7ddcbc9f7f49 --- /dev/null +++ b/ide/src/rust/ide/src/controller/ide/desktop.rs @@ -0,0 +1,113 @@ +//! The Desktop IDE Controller +//! +//! See [`crate::controller::ide`] for more detailed description of IDE Controller API. + +use crate::prelude::*; + +use crate::controller::ide::API; +use crate::controller::ide::ManagingProjectAPI; +use crate::controller::ide::StatusNotificationPublisher; +use crate::controller::ide::Notification; +use crate::controller::project::ENGINE_VERSION_FOR_NEW_PROJECTS; +use crate::ide::initializer; +use crate::notification; + +use enso_protocol::project_manager; +use enso_protocol::project_manager::MissingComponentAction; +use enso_protocol::project_manager::ProjectName; +use parser::Parser; + + + +// ================= +// === Constants === +// ================= + +const UNNAMED_PROJECT_NAME:&str = "Unnamed"; + + + +// ============================= +// === The Controller Handle === +// ============================= + +/// The Desktop IDE Controller handle. +/// +/// The desktop controller has access to the Project Manager, and thus is able to perform all +/// project management operations. +#[derive(Clone,CloneRef,Derivative)] +#[derivative(Debug)] +pub struct Handle { + logger : Logger, + current_project : Rc>, + #[derivative(Debug="ignore")] + project_manager : Rc, + status_notifications : StatusNotificationPublisher, + parser : Parser, + notifications : notification::Publisher, +} + +impl Handle { + /// Create a project controller handle with already loaded project model. + pub fn new_with_project + (project_manager:Rc, initial_project:model::Project) -> Self { + let logger = Logger::new("controller::ide::Desktop"); + let current_project = Rc::new(CloneRefCell::new(initial_project)); + let status_notifications = default(); + let parser = Parser::new_or_panic(); + let notifications = default(); + Self {logger,current_project,project_manager,status_notifications,parser,notifications} + } + + /// Create a project controller handle which opens the project with the given name, or creates it + /// if it does not exist. + pub async fn new_with_opened_project + (project_manager:Rc, name:ProjectName) -> FallibleResult { + // TODO[ao]: Reuse of initializer used in previous code design. It should be soon replaced + // anyway, because we will soon resign from the "open or create" approach when opening + // IDE. See https://github.com/enso-org/ide/issues/1492 for details. + let initializer = initializer::WithProjectManager::new(project_manager.clone_ref(),name); + let model = initializer.initialize_project_model().await?; + Ok(Self::new_with_project(project_manager,model)) + } +} + +impl API for Handle { + fn current_project (&self) -> model::Project { self.current_project.get() } + fn status_notifications(&self) -> &StatusNotificationPublisher { &self.status_notifications } + fn parser (&self) -> &Parser { &self.parser } + + fn subscribe(&self) -> StaticBoxStream { + self.notifications.subscribe().boxed_local() + } + + fn manage_projects(&self) -> FallibleResult<&dyn ManagingProjectAPI> { + Ok(self) + } +} + +impl ManagingProjectAPI for Handle { + fn create_new_project(&self) -> BoxFuture { + async move { + use model::project::Synchronized as Project; + + let list = self.project_manager.list_projects(&None).await?; + let names:HashSet = list.projects.into_iter().map(|p| p.name.into()).collect(); + let without_suffix = UNNAMED_PROJECT_NAME.to_owned(); + let with_suffix = (1..).map(|i| format!("{}_{}", UNNAMED_PROJECT_NAME, i)); + let mut candidates = std::iter::once(without_suffix).chain(with_suffix); + // The iterator have no end, so we can safely unwrap. + let name = candidates.find(|c| names.contains(c)).unwrap(); + let version = Some(ENGINE_VERSION_FOR_NEW_PROJECTS.to_owned()); + let action = MissingComponentAction::Install; + + let create_result = self.project_manager.create_project(&name,&version,&action).await?; + let new_project_id = create_result.project_id; + let project_mgr = self.project_manager.clone_ref(); + let new_project = Project::new_opened(&self.logger,project_mgr,new_project_id,name); + self.current_project.set(new_project.await?); + executor::global::spawn(self.notifications.publish(Notification::NewProjectCreated)); + Ok(()) + }.boxed_local() + } +} diff --git a/ide/src/rust/ide/src/controller/ide/plain.rs b/ide/src/rust/ide/src/controller/ide/plain.rs new file mode 100644 index 000000000000..66ee8a093fec --- /dev/null +++ b/ide/src/rust/ide/src/controller/ide/plain.rs @@ -0,0 +1,83 @@ +//! The Plain IDE Controller. +//! +//! See [`crate::controller::ide`] for more detailed description of IDE Controller API. + +use crate::prelude::*; +use crate::controller::ide::ManagingProjectAPI; +use crate::controller::ide::Notification; +use crate::controller::ide::StatusNotificationPublisher; + +use enso_protocol::project_manager::ProjectName; +use parser::Parser; + + + +// ============= +// === Error === +// ============= + +#[allow(missing_docs)] +#[derive(Copy,Clone,Debug,Fail)] +#[fail(display="Project operations are not supported.")] +pub struct ProjectOperationsNotSupported; + + + +// =============================== +// === Plain Controller Handle === +// =============================== + +/// Plain IDE Controller Handle. +/// +/// The Plain Controller does not allow for managing projects: it has the single project model +/// as a project opened in IDE which does not change (it is set up during construction). +#[allow(missing_docs)] +#[derive(Clone,CloneRef,Debug)] +pub struct Handle { + pub logger : Logger, + pub status_notifications : StatusNotificationPublisher, + pub parser : Parser, + pub project : model::Project, +} + +impl Handle { + /// Create IDE Controller for a given opened project. + pub fn new(project:model::Project) -> Self { + let logger = Logger::new("controller::ide::Plain"); + let status_notifications = default(); + let parser = Parser::new_or_panic(); + Self {logger,status_notifications,parser,project} + } + + /// Create IDE Controller from Language Server endpoints, describing the opened project. + pub async fn from_ls_endpoints + ( project_name : ProjectName + , version : semver::Version + , json_endpoint : String + , binary_endpoint : String + ) -> FallibleResult { + let logger = Logger::new("controller::ide::Plain"); + //TODO [ao]: this should be not the default; instead project model should not need the id. + // See https://github.com/enso-org/ide/issues/1572 + let project_id = default(); + let project = model::project::Synchronized::new_connected + (&logger,None,json_endpoint,binary_endpoint,version,project_id,project_name).await?; + let status_notifications = default(); + let parser = Parser::new_or_panic(); + Ok(Self{logger,status_notifications,parser,project}) + } +} + +impl controller::ide::API for Handle { + fn current_project (&self) -> model::Project { self.project.clone_ref() } + fn status_notifications(&self) -> &StatusNotificationPublisher { &self.status_notifications } + fn parser (&self) -> &Parser { &self.parser } + + fn subscribe(&self) -> StaticBoxStream { + futures::stream::empty().boxed_local() + } + + fn manage_projects(&self) -> FallibleResult<&dyn ManagingProjectAPI> { + Err(ProjectOperationsNotSupported.into()) + } +} diff --git a/ide/src/rust/ide/src/controller/project.rs b/ide/src/rust/ide/src/controller/project.rs new file mode 100644 index 000000000000..c6323ce1d670 --- /dev/null +++ b/ide/src/rust/ide/src/controller/project.rs @@ -0,0 +1,244 @@ +//! A Project Controller. + +use crate::prelude::*; + +use crate::controller::graph::executed::Notification as GraphNotification; +use crate::controller::ide::StatusNotificationPublisher; + +use enso_frp::web::platform; +use enso_frp::web::platform::Platform; +use enso_protocol::language_server::MethodPointer; +use enso_protocol::language_server::Path; +use parser::Parser; + + + +// ================= +// === Constants === +// ================= + +/// The label of compiling stdlib message process. +pub const COMPILING_STDLIB_LABEL:&str = "Compiling standard library. It can take up to 1 minute."; + +/// The requirements for Engine's version, in format understandable by +/// [`semver::VersionReq::parse`]. +pub const ENGINE_VERSION_SUPPORTED : &str = "^0.2.11"; + +/// The Engine version used in projects created in IDE. +// Usually it is a good idea to synchronize this version with the bundled Engine version in +// src/js/lib/project-manager/src/build.ts. See also https://github.com/enso-org/ide/issues/1359 +pub const ENGINE_VERSION_FOR_NEW_PROJECTS : &str = "0.2.11"; + +/// The name of the module initially opened in the project view. +/// +/// Currently, this name is hardcoded in the engine services and is populated for each project +/// created using engine's Project Picker service. +pub const INITIAL_MODULE_NAME:&str = "Main"; + +/// Name of the main definition. +/// +/// This is the definition whose graph will be opened on IDE start. +pub const MAIN_DEFINITION_NAME:&str = "main"; + +/// The code with definition of the default `main` method. +pub fn default_main_method_code() -> String { + format!(r#"{} = "Hello, World!""#, MAIN_DEFINITION_NAME) +} + +/// The default content of the newly created initial main module file. +pub fn default_main_module_code() -> String { + default_main_method_code() +} + +/// Method pointer that described the main method, i.e. the method that project view wants to open +/// and which presence is currently required. +pub fn main_method_ptr(project_name:impl Str, module_path:&model::module::Path) -> MethodPointer { + module_path.method_pointer(project_name,MAIN_DEFINITION_NAME) +} + + +// ============== +// === Handle === +// ============== + +// === SetupResult === + +/// The result of initial project setup, containing handy controllers to be used in the initial +/// view. +#[derive(Clone,CloneRef,Debug)] +pub struct InitializationResult { + /// The Text Controller for Main module code to be displayed in Code Editor. + pub main_module_text:controller::Text, + /// The Graph Controller for main definition's graph, to be displayed in Graph Editor. + pub main_graph:controller::ExecutedGraph, +} + +/// Project Controller Handle. +/// +/// This controller supports IDE-related operations on a specific project. +#[allow(missing_docs)] +#[derive(Clone,CloneRef,Debug)] +pub struct Project { + pub logger : Logger, + pub model : model::Project, + pub status_notifications : StatusNotificationPublisher, +} + +impl Project { + /// Create a controller of given project. + pub fn new(model:model::Project, status_notifications:StatusNotificationPublisher) -> Self { + let logger = Logger::new("controller::Project"); + Self {logger,model,status_notifications} + } + + /// Do the initial setup of opened project. + /// + /// This function should be called always after opening a new project in IDE. It checks if main + /// module and main method are present in the project, and recreates them if missing. + /// It also sends status notifications and warnings about the opened project (like + /// warning about unsupported engine version). + /// + /// Returns the controllers of module and graph which should be displayed in the view. + pub async fn initialize(&self) -> FallibleResult { + let project = self.model.clone_ref(); + let parser = self.model.parser(); + let module_path = self.initial_module_path()?; + let file_path = module_path.file_path().clone(); + + // TODO [mwu] This solution to recreate missing main file should be considered provisional + // until proper decision is made. See: https://github.com/enso-org/enso/issues/1050 + self.recreate_if_missing(&file_path,default_main_method_code()).await?; + let method = main_method_ptr(project.name(),&module_path); + let module = self.model.module(module_path).await?; + Self::add_main_if_missing(project.name().as_ref(),&module,&method,&parser)?; + + // Here, we should be relatively certain (except race conditions in case of multiple + // clients that we currently do not support) that main module exists and contains main + // method. Thus, we should be able to successfully create a graph controller for it. + 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.notify_about_compiling_process(&main_graph); + self.display_warning_on_unsupported_engine_version()?; + + Ok(InitializationResult {main_module_text,main_graph}) + } +} + + +// === Project Initialization Utilities === + +impl Project { + /// Returns the path to the initially opened module in the given project. + fn initial_module_path(&self) -> FallibleResult { + crate::ide::initial_module_path(&self.model) + } + + /// Create a file with default content if it does not already exist. + pub async fn recreate_if_missing(&self, path:&Path, default_content:String) -> FallibleResult { + let rpc = self.model.json_rpc(); + if !rpc.file_exists(path).await?.exists { + rpc.write_file(path,&default_content).await?; + } + Ok(()) + } + + /// Add main method definition to the given module, if the method is not already defined. + /// + /// The lookup will be done using the given `main_ptr` value. + pub fn add_main_if_missing + (project_name:&str, module:&model::Module, main_ptr:&MethodPointer, parser:&Parser) + -> FallibleResult { + if module.lookup_method(project_name,main_ptr).is_err() { + let mut info = module.info(); + let main_code = default_main_method_code(); + let main_ast = parser.parse_line(main_code)?; + info.add_ast(main_ast,double_representation::module::Placement::End)?; + module.update_ast(info.ast)?; + } + Ok(()) + } + + 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(_))) + ); + executor::global::spawn(async move { + computed_value_notif.next().await; + status_notif.published_background_task_finished(compiling_process); + }); + } + + fn display_warning_on_unsupported_engine_version(&self) -> FallibleResult { + let requirements = semver::VersionReq::parse(ENGINE_VERSION_SUPPORTED)?; + let version = self.model.engine_version(); + if !requirements.matches(version) { + let message = format!("Unsupported Engine version. Please update engine_version in {} \ + to {}.",self.package_yaml_path(),ENGINE_VERSION_FOR_NEW_PROJECTS); + self.status_notifications.publish_event(message); + } + Ok(()) + } + + fn package_yaml_path(&self) -> String { + let project_name = self.model.name(); + match platform::current() { + Some(Platform::Linux) | + Some(Platform::MacOS) => format!("~/enso/projects/{}/package.yaml",project_name), + Some(Platform::Windows) => + format!("%userprofile%\\enso\\projects\\{}\\package.yaml",project_name), + _ => format!("/{}/package.yaml",project_name) + } + } +} + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + + use crate::executor::test_utils::TestWithLocalPoolExecutor; + + #[test] + fn new_project_engine_version_fills_requirements() { + let requirements = semver::VersionReq::parse(ENGINE_VERSION_SUPPORTED).unwrap(); + let version = semver::Version::parse(ENGINE_VERSION_FOR_NEW_PROJECTS).unwrap(); + assert!(requirements.matches(&version)) + } + + #[wasm_bindgen_test] + fn adding_missing_main() { + let _ctx = TestWithLocalPoolExecutor::set_up(); + let parser = parser::Parser::new_or_panic(); + let mut data = crate::test::mock::Unified::new(); + let module_name = data.module_path.module_name(); + let main_ptr = main_method_ptr(&data.project_name,&data.module_path); + + // Check that module without main gets it after the call. + let empty_module_code = ""; + data.set_code(empty_module_code); + let module = data.module(); + assert!(module.lookup_method(&data.project_name,&main_ptr).is_err()); + Project::add_main_if_missing(&data.project_name, &module, &main_ptr, &parser).unwrap(); + assert!(module.lookup_method(&data.project_name,&main_ptr).is_ok()); + + // Now check that modules that have main already defined won't get modified. + let mut expect_intact = move |code:&str| { + data.set_code(code); + let module = data.module(); + Project::add_main_if_missing(&data.project_name, &module, &main_ptr, &parser).unwrap(); + assert_eq!(code,module.ast().repr()); + }; + expect_intact("main = 5"); + expect_intact("here.main = 5"); + expect_intact(&format!("{}.main = 5",module_name)); + } +} diff --git a/ide/src/rust/ide/src/controller/searcher.rs b/ide/src/rust/ide/src/controller/searcher.rs index 36290c2eebc1..fbbda11fb1c6 100644 --- a/ide/src/rust/ide/src/controller/searcher.rs +++ b/ide/src/rust/ide/src/controller/searcher.rs @@ -59,6 +59,14 @@ pub struct NotASuggestion { index : usize, } +#[allow(missing_docs)] +#[derive(Debug,Fail)] +#[fail(display="An action \"{}\" is not supported (...)", action_label)] +pub struct NotSupported { + action_label : String, + reason : failure::Error, +} + #[allow(missing_docs)] #[derive(Copy,Clone,Debug,Fail)] #[fail(display="An action cannot be executed when searcher is in \"edit node\" mode.")] @@ -452,28 +460,29 @@ pub struct Searcher { mode : Immutable, database : Rc, language_server : Rc, - parser : Parser, + ide : controller::Ide, this_arg : Rc>, position_in_code : Immutable, - project_name : ImString, } impl Searcher { /// Create new Searcher Controller. pub async fn new ( parent : impl AnyLogger + , ide : controller::Ide , project : &model::Project , method : language_server::MethodPointer , mode : Mode , selected_nodes : Vec ) -> FallibleResult { let graph = controller::ExecutedGraph::new(&parent,project.clone_ref(),method).await?; - Self::new_from_graph_controller(parent,project,graph,mode,selected_nodes) + Self::new_from_graph_controller(parent,ide,project,graph,mode,selected_nodes) } /// Create new Searcher Controller, when you have Executed Graph Controller handy. pub fn new_from_graph_controller ( parent : impl AnyLogger + , ide : controller::Ide , project : &model::Project , graph : controller::ExecutedGraph , mode : Mode @@ -492,15 +501,13 @@ impl Searcher { let position = TextLocation::convert_span(module_ast.repr(),&def_span).end; let this_arg = Rc::new(matches!(mode, Mode::NewNode{..}).and_option_from(|| ThisNode::new(selected_nodes,&graph.graph()))); let ret = Self { - logger,graph,this_arg, + logger,graph,this_arg,ide, data : Rc::new(RefCell::new(data)), notifier : default(), mode : Immutable(mode), database : project.suggestion_db(), language_server : project.json_rpc(), - parser : project.parser(), position_in_code : Immutable(position), - project_name : project.name(), }; ret.reload_list(); Ok(ret) @@ -522,7 +529,7 @@ impl Searcher { /// in a new action list (the appropriate notification will be emitted). pub fn set_input(&self, new_input:String) -> FallibleResult { debug!(self.logger, "Manually setting input to {new_input}."); - let parsed_input = ParsedInput::new(new_input,&self.parser)?; + let parsed_input = ParsedInput::new(new_input,&self.ide.parser())?; let old_expr = self.data.borrow().input.expression.repr(); let new_expr = parsed_input.expression.repr(); @@ -562,7 +569,7 @@ impl Searcher { let id = self.data.borrow().input.next_completion_id(); let picked_completion = FragmentAddedByPickingSuggestion {id,picked_suggestion}; let code_to_insert = self.code_to_insert(&picked_completion).code; - let added_ast = self.parser.parse_line(&code_to_insert)?; + let added_ast = self.ide.parser().parse_line(&code_to_insert)?; let pattern_offset = self.data.borrow().input.pattern_offset; let new_expression = match self.data.borrow_mut().input.expression.take() { None => { @@ -618,6 +625,27 @@ impl Searcher { Mode::NewNode {position} => self.add_example(&example,position).map(Some), _ => Err(CannotExecuteWhenEditingNode.into()) } + Action::CreateNewProject => { + match self.ide.manage_projects() { + Ok(_) => { + let ide = self.ide.clone_ref(); + let logger = self.logger.clone_ref(); + executor::global::spawn(async move { + // We checked that manage_projects returns Some just a moment ago, so + // unwrapping is safe. + let result = ide.manage_projects().unwrap().create_new_project().await; + if let Err(err) = result { + error!(logger, "Error when creating new project: {err}"); + } + }); + Ok(None) + }, + Err(err) => Err(NotSupported{ + action_label : Action::CreateNewProject.to_string(), + reason : err, + }.into()) + } + } } } @@ -652,7 +680,7 @@ impl Searcher { /// expression, otherwise a new node is added. This will also add all imports required by /// picked suggestions. pub fn commit_node(&self) -> FallibleResult { - let input_chain = self.data.borrow().input.as_prefix_chain(&self.parser); + let input_chain = self.data.borrow().input.as_prefix_chain(self.ide.parser()); let expression = match (self.this_var(),input_chain) { (Some(this_var),Some(input)) => @@ -697,10 +725,10 @@ impl Searcher { let graph = self.graph.graph(); let mut module = double_representation::module::Info{ast:graph.module.ast()}; let graph_definition_name = graph.graph_info()?.source.name.item; - let new_definition = example.definition_to_add(&module, &self.parser)?; + let new_definition = example.definition_to_add(&module, self.ide.parser())?; let new_definition_name = Ast::var(new_definition.name.name.item.clone()); let new_definition_place = double_representation::module::Placement::Before(graph_definition_name); - module.add_method(new_definition,new_definition_place,&self.parser)?; + module.add_method(new_definition,new_definition_place,self.ide.parser())?; // === Add new node === @@ -719,7 +747,7 @@ impl Searcher { // === Add imports === let here = self.module_qualified_name(); for import in example.imports.iter().map(QualifiedName::from_text).filter_map(Result::ok) { - module.add_module_import(&here, &self.parser, &import); + module.add_module_import(&here,self.ide.parser(),&import); } graph.module.update_ast(module.ast)?; graph.module.set_node_metadata(node.id(),NodeMetadata {position,intended_method})?; @@ -749,7 +777,7 @@ impl Searcher { let without_enso_project = imports.filter(|i| i.to_string() != ENSO_PROJECT_SPECIAL_MODULE); for mut import in without_enso_project { import.remove_main_module_segment(); - module.add_module_import(&here, &self.parser, &import); + module.add_module_import(&here,self.ide.parser(),&import); } self.graph.graph().module.update_ast(module.ast) } @@ -845,6 +873,9 @@ impl Searcher { if matches!(self.mode.deref(), Mode::NewNode{..}) && self.this_arg.is_none() { actions.extend(self.database.iterate_examples().map(Action::Example)); Self::add_enso_project_entries(&actions)?; + if self.ide.manage_projects().is_ok() { + actions.extend(std::iter::once(Action::CreateNewProject)); + } } for response in completion_responses { let response = response?; @@ -932,7 +963,7 @@ impl Searcher { } fn module_qualified_name(&self) -> QualifiedName { - self.graph.graph().module.path().qualified_module_name(&*self.project_name) + self.graph.graph().module.path().qualified_module_name(&*self.ide.current_project().name()) } /// Get the user action basing of current input (see `UserAction` docs). @@ -1027,6 +1058,7 @@ fn apply_this_argument(this_var:&str, ast:&Ast) -> Ast { pub mod test { use super::*; + use crate::controller::ide::plain::ProjectOperationsNotSupported; use crate::executor::test_utils::TestWithLocalPoolExecutor; use crate::model::SuggestionDatabase; use crate::model::suggestion_database::entry::Argument; @@ -1041,6 +1073,8 @@ pub mod test { use utils::test::traits::*; use enso_protocol::language_server::SuggestionId; + + pub fn completion_response(results:&[SuggestionId]) -> language_server::response::Completion { language_server::response::Completion { results : results.to_vec(), @@ -1112,25 +1146,32 @@ pub mod test { let mut client = language_server::MockClient::default(); client.require_all_calls(); client_setup(&mut data,&mut client); - let end_of_code = TextLocation::at_document_end(&data.graph.module.code); - let code_range = TextLocation::at_document_begin()..=end_of_code; - let graph = data.graph.controller(); - let node = &graph.graph().nodes().unwrap()[0]; - let this = ThisNode::new(vec![node.info.id()],&graph.graph()); - let this = data.selected_node.and_option(this); - let logger = Logger::new("Searcher");// new_empty - let database = Rc::new(SuggestionDatabase::new_empty(&logger)); + let end_of_code = TextLocation::at_document_end(&data.graph.module.code); + let code_range = TextLocation::at_document_begin()..=end_of_code; + let graph = data.graph.controller(); + let node = &graph.graph().nodes().unwrap()[0]; + let this = ThisNode::new(vec![node.info.id()],&graph.graph()); + let this = data.selected_node.and_option(this); + let logger = Logger::new("Searcher");// new_empty + let database = Rc::new(SuggestionDatabase::new_empty(&logger)); + let mut ide = controller::ide::MockAPI::new(); + let mut project = model::project::MockAPI::new(); + let project_name = ImString::new(&data.graph.graph.project_name); + project.expect_name().returning_st(move || project_name.clone_ref()); + let project = Rc::new(project); + ide.expect_parser().return_const(Parser::new_or_panic()); + ide.expect_current_project().returning_st(move || project.clone_ref()); + ide.expect_manage_projects().returning_st(move || Err(ProjectOperationsNotSupported.into())); let module_name = QualifiedName::from_segments(PROJECT_NAME, &[MODULE_NAME]).unwrap(); let searcher = Searcher { graph,logger,database, + ide : Rc::new(ide), data : default(), notifier : default(), mode : Immutable(Mode::NewNode {position:default()}), language_server : language_server::Connection::new_mock_rc(client), - parser : Parser::new_or_panic(), this_arg : Rc::new(this), position_in_code : Immutable(end_of_code), - project_name : ImString::new(&data.graph.graph.project_name), }; let entry1 = model::suggestion_database::Entry { name : "testFunction1".to_string(), @@ -1606,7 +1647,7 @@ pub mod test { // Completion was picked and edited. Case::new("2 + 2",&["sum1 = 2 + 2","operator1 = sum1.var.testFunction1"], |f| { f.searcher.use_suggestion(f.entry1.clone()).unwrap(); - let new_parsed_input = ParsedInput::new("var.testFunction1",&f.searcher.parser); + let new_parsed_input = ParsedInput::new("var.testFunction1",&f.searcher.ide.parser()); f.searcher.data.borrow_mut().input = new_parsed_input.unwrap(); }), // Variable name already present, need to use it. And not break it. @@ -1638,7 +1679,7 @@ pub mod test { fn expect_inserted_import_for(entry:&action::Suggestion, expected_import:Vec<&QualifiedName>) { let Fixture{test:_test,mut searcher,..} = Fixture::new(); let module = searcher.graph.graph().module.clone_ref(); - let parser = searcher.parser.clone_ref(); + let parser = searcher.ide.parser().clone_ref(); let picked_method = FragmentAddedByPickingSuggestion { id : CompletedFragmentId::Function, diff --git a/ide/src/rust/ide/src/controller/searcher/action.rs b/ide/src/rust/ide/src/controller/searcher/action.rs index 1706b3cda80a..2135358d2e8f 100644 --- a/ide/src/rust/ide/src/controller/searcher/action.rs +++ b/ide/src/rust/ide/src/controller/searcher/action.rs @@ -23,12 +23,13 @@ pub enum Action { /// Add to the current module a new function with example code, and a new node in /// current scene calling that function. Example(Example), - // In future, other action types will be added (like project/module management, etc.). + /// Create a new untitled project and open it. + CreateNewProject, + // In the future, other action types will be added (like project/module management, etc.). } -impl Action { - /// The caption to display in searcher list. - pub fn caption(&self) -> String { +impl Display for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Suggestion(completion) => if let Some(self_type) = completion.self_type.as_ref() { let should_put_project_name = self_type.name == constants::PROJECTS_MAIN_MODULE @@ -36,11 +37,12 @@ impl Action { let self_type_name = if should_put_project_name { self_type.project_name.as_ref() } else { &self_type.name }; - format!("{}.{}",self_type_name,completion.name) + write!(f,"{}.{}",self_type_name,completion.name) } else { - completion.name.clone() + write!(f, "{}", completion.name.clone()) } - Self::Example(example) => format!("Example: {}", example.name), + Self::Example(example) => write!(f,"Example: {}", example.name), + Self::CreateNewProject => write!(f,"New Project"), } } } @@ -70,10 +72,10 @@ pub struct ListEntry { impl ListEntry { /// Update the current match info according to the new filtering pattern. pub fn update_matching_info(&mut self, pattern:impl Str) { - let matches = fuzzly::matches(self.action.caption(), pattern.as_ref()); + let matches = fuzzly::matches(self.action.to_string(),pattern.as_ref()); let subsequence = matches.and_option_from(|| { let metric = fuzzly::metric::default(); - fuzzly::find_best_subsequence(self.action.caption(), pattern, metric) + fuzzly::find_best_subsequence(self.action.to_string(),pattern,metric) }); self.match_info = match subsequence { Some(subsequence) => MatchInfo::Matches {subsequence}, diff --git a/ide/src/rust/ide/src/ide.rs b/ide/src/rust/ide/src/ide.rs index 963cdf5b3c4b..e84c0a2e831f 100644 --- a/ide/src/rust/ide/src/ide.rs +++ b/ide/src/rust/ide/src/ide.rs @@ -4,15 +4,10 @@ pub mod integration; use crate::prelude::*; -use crate::controller::FilePath; -use crate::controller::graph::executed::Notification as GraphNotification; -use crate::model::module::Path as ModulePath; +use crate::controller::project::INITIAL_MODULE_NAME; use crate::ide::integration::Integration; use ensogl::application::Application; -use enso_protocol::language_server::MethodPointer; -use ide_view::status_bar; -use parser::Parser; pub use initializer::Initializer; @@ -26,39 +21,6 @@ pub use initializer::Initializer; pub const BACKEND_DISCONNECTED_MESSAGE:&str = "Connection to the backend has been lost. Please try restarting IDE."; -/// The name of the module initially opened in the project view. -/// -/// Currently this name is hardcoded in the engine services and is populated for each project -/// created using engine's Project Picker service. -/// -/// TODO [mwu] Name of the module that will be initially opened in the text editor. -/// Provisionally the Project View is hardcoded to open with a single text -/// editor and it will be connected with a file with module of this name. -/// To be replaced with better mechanism once we decide how to describe -/// default initial layout for the project. -pub const INITIAL_MODULE_NAME:&str = "Main"; - -/// Name of the main definition. -/// -/// This is the definition whose graph will be opened on IDE start. -pub const MAIN_DEFINITION_NAME:&str = "main"; - -/// The code with definition of the default `main` method. -pub fn default_main_method_code() -> String { - format!(r#"{} = "Hello, World!""#, MAIN_DEFINITION_NAME) -} - -/// The default content of the newly created initial main module file. -pub fn default_main_module_code() -> String { - default_main_method_code() -} - -/// Method pointer that described the main method, i.e. the method that project view wants to open -/// and which presence is currently required. -pub fn main_method_ptr(project_name:impl Str, module_path:&model::module::Path) -> MethodPointer { - module_path.method_pointer(project_name,MAIN_DEFINITION_NAME) -} - // =========== @@ -77,111 +39,15 @@ pub struct Ide { impl Ide { /// Constructor. - pub async fn new(application:Application, view:ide_view::project::View, project:model::Project) -> FallibleResult { - let logger = Logger::new("Ide"); - let module_path = initial_module_path(&project)?; - let file_path = module_path.file_path().clone(); - // TODO [mwu] This solution to recreate missing main file should be considered provisional - // until proper decision is made. See: https://github.com/enso-org/enso/issues/1050 - recreate_if_missing(&project, &file_path, default_main_method_code()).await?; - - let method = main_method_ptr(project.name(),&module_path); - let module = project.module(module_path).await?; - add_main_if_missing(project.name().as_ref(),&module,&method,&project.parser())?; - - // Here, we should be relatively certain (except race conditions in case of multiple clients - // that we currently do not support) that main module exists and contains main method. - // Thus, we should be able to successfully create a graph controller for it. - let graph = controller::ExecutedGraph::new(&logger,project.clone_ref(),method).await?; - let text = controller::Text::new(&logger, &project, file_path).await?; - let visualization = project.visualization().clone(); - - let status_bar = view.status_bar().clone_ref(); - let intro_msg = "Compiling standard library. It can take up to 1 minute."; - status_bar.add_process(status_bar::process::Label::new(intro_msg)); - let compiling_process = status_bar.last_process.value(); - let notifications = graph.subscribe(); - let mut computed_value_notifications = notifications.filter(|notification| - futures::future::ready(matches!(notification, GraphNotification::ComputedValueInfo(_))) - ); - executor::global::spawn(async move { - computed_value_notifications.next().await; - status_bar.finish_process(compiling_process); - }); - - let integration = Integration::new(view,graph,text,visualization,project); - Ok(Ide {application,integration}) + pub async fn new + (application:Application, view:ide_view::project::View, controller:controller::Ide) + -> Self { + let integration = integration::Integration::new(controller,view); + Ide {application,integration} } } - -/// Returns the path to the initially opened module in the given project. -pub fn initial_module_path(project:&model::Project) -> FallibleResult { +/// The Path of the module initially opened after opening project in IDE. +pub fn initial_module_path(project:&model::Project) -> FallibleResult { model::module::Path::from_name_segments(project.content_root_id(),&[INITIAL_MODULE_NAME]) } - -/// Create a file with default content if it does not already exist. -pub async fn recreate_if_missing(project:&model::Project, path:&FilePath, default_content:String) --> FallibleResult { - let rpc = project.json_rpc(); - if !rpc.file_exists(path).await?.exists { - rpc.write_file(path,&default_content).await?; - } - Ok(()) -} - -/// Add main method definition to the given module, if the method is not already defined. -/// -/// The lookup will be done using the given `main_ptr` value. -pub fn add_main_if_missing -(project_name:&str, module:&model::Module, main_ptr:&MethodPointer, parser:&Parser) --> FallibleResult { - if module.lookup_method(project_name,main_ptr).is_err() { - let mut info = module.info(); - let main_code = default_main_method_code(); - let main_ast = parser.parse_line(main_code)?; - info.add_ast(main_ast,double_representation::module::Placement::End)?; - module.update_ast(info.ast)?; - } - Ok(()) -} - - - -// ============= -// === Tests === -// ============= - -#[cfg(test)] -mod tests { - use super::*; - use crate::executor::test_utils::TestWithLocalPoolExecutor; - - #[wasm_bindgen_test] - fn adding_missing_main() { - let _ctx = TestWithLocalPoolExecutor::set_up(); - let parser = parser::Parser::new_or_panic(); - let mut data = crate::test::mock::Unified::new(); - let module_name = data.module_path.module_name(); - let main_ptr = main_method_ptr(&data.project_name,&data.module_path); - - // Check that module without main gets it after the call. - let empty_module_code = ""; - data.set_code(empty_module_code); - let module = data.module(); - assert!(module.lookup_method(&data.project_name,&main_ptr).is_err()); - add_main_if_missing(&data.project_name,&module,&main_ptr,&parser).unwrap(); - assert!(module.lookup_method(&data.project_name,&main_ptr).is_ok()); - - // Now check that modules that have main already defined won't get modified. - let mut expect_intact = move |code:&str| { - data.set_code(code); - let module = data.module(); - add_main_if_missing(&data.project_name,&module,&main_ptr,&parser).unwrap(); - assert_eq!(code,module.ast().repr()); - }; - expect_intact("main = 5"); - expect_intact("here.main = 5"); - expect_intact(&format!("{}.main = 5",module_name)); - } -} diff --git a/ide/src/rust/ide/src/ide/initializer.rs b/ide/src/rust/ide/src/ide/initializer.rs index d58e0342256a..45d62831400b 100644 --- a/ide/src/rust/ide/src/ide/initializer.rs +++ b/ide/src/rust/ide/src/ide/initializer.rs @@ -3,18 +3,16 @@ use crate::prelude::*; use crate::config; +use crate::controller::project::ENGINE_VERSION_FOR_NEW_PROJECTS; use crate::ide::Ide; use crate::transport::web::WebSocket; -use enso_protocol::binary; -use enso_protocol::language_server; use enso_protocol::project_manager; use enso_protocol::project_manager::ProjectName; use uuid::Uuid; use ensogl::application::Application; use ensogl::system::web; -use ensogl::system::web::platform; -use ensogl::system::web::platform::Platform; + // ================= @@ -25,11 +23,6 @@ use ensogl::system::web::platform::Platform; // download required version of Engine. This should be handled properly when implementing // https://github.com/enso-org/ide/issues/1034 const PROJECT_MANAGER_TIMEOUT_SEC : u64 = 2 * 60 * 60; -const ENGINE_VERSION_SUPPORTED : &str = "^0.2.11"; - -// Usually it is a good idea to synchronize this version with the bundled Engine version in -// src/js/lib/project-manager/src/build.ts. See also https://github.com/enso-org/ide/issues/1359 -const ENGINE_VERSION_FOR_NEW_PROJECTS : &str = "0.2.11"; @@ -71,19 +64,20 @@ impl Initializer { let executor = setup_global_executor(); executor::global::spawn(async move { info!(self.logger, "Starting IDE with the following config: {self.config:?}"); - let project_model = self.initialize_project_model(); + let application = Application::new(&web::get_html_element_by_id("root").unwrap()); let view = application.new_view::(); let status_bar = view.status_bar().clone_ref(); + // We know the name of new project before it loads. We set it right now to avoid + // displaying placeholder on the scene during loading. view.graph().model.breadcrumbs.project_name(self.config.project_name.to_string()); application.display.add_child(&view); // TODO [mwu] Once IDE gets some well-defined mechanism of reporting // issues to user, such information should be properly passed // in case of setup failure. - let result = (async { - let project_model = project_model.await?; - self.display_warning_on_unsupported_engine_version(&view,&project_model)?; - Ide::new(application,view.clone_ref(),project_model).await + let result:FallibleResult = (async { + let controller = self.initialize_ide_controller().await?; + Ok(Ide::new(application,view.clone_ref(),controller).await) }).await; match result { @@ -108,33 +102,30 @@ impl Initializer { std::mem::forget(executor); } - /// Initialize and return a new Project Model. + /// Initialize and return a new Ide Controller. /// /// This will setup all required connections to backend properly, according to the /// configuration. - pub async fn initialize_project_model(&self) -> FallibleResult { + pub async fn initialize_ide_controller(&self) -> FallibleResult { use crate::config::BackendService::*; match &self.config.backend { ProjectManager { endpoint } => { let project_manager = self.setup_project_manager(endpoint).await?; - let logger = self.logger.clone_ref(); let project_name = self.config.project_name.clone(); - let initializer = WithProjectManager {logger,project_manager,project_name}; - initializer.initialize_project_model().await + let controller = controller::ide::Desktop::new_with_opened_project + (project_manager,project_name).await?; + Ok(Rc::new(controller)) } LanguageServer {json_endpoint,binary_endpoint} => { - let logger = &self.logger; - let project_manager = None; let json_endpoint = json_endpoint.clone(); let binary_endpoint = binary_endpoint.clone(); + let project_name = self.config.project_name.clone(); // TODO[ao]: we should think how to handle engine's versions in cloud. // https://github.com/enso-org/ide/issues/1195 - let version = ENGINE_VERSION_FOR_NEW_PROJECTS.to_owned(); - let project_id = default(); - let project_name = self.config.project_name.clone(); - let project_model = create_project_model(logger,project_manager,json_endpoint - ,binary_endpoint,version,project_id,project_name); - project_model.await + let version = semver::Version::parse(ENGINE_VERSION_FOR_NEW_PROJECTS)?; + let controller = controller::ide::Plain::from_ls_endpoints + (project_name,version,json_endpoint,binary_endpoint).await?; + Ok(Rc::new(controller)) } } } @@ -149,29 +140,6 @@ impl Initializer { executor::global::spawn(project_manager.runner()); Ok(Rc::new(project_manager)) } - - fn display_warning_on_unsupported_engine_version - (&self, view:&ide_view::project::View, project:&model::Project) -> FallibleResult { - let requirements = semver::VersionReq::parse(ENGINE_VERSION_SUPPORTED)?; - let version = project.engine_version(); - if !requirements.matches(version) { - let message = format!("Unsupported Engine version. Please update engine_version in {} \ - to {}.",self.package_yaml_path(),ENGINE_VERSION_FOR_NEW_PROJECTS); - let label = ide_view::status_bar::event::Label::from(message); - view.status_bar().add_event(label); - } - Ok(()) - } - - fn package_yaml_path(&self) -> String { - let project_name = &self.config.project_name; - match platform::current() { - Some(Platform::Linux) | - Some(Platform::MacOS) => format!("~/enso/projects/{}/package.yaml", project_name), - Some(Platform::Windows) => format!("%userprofile%\\enso\\projects\\{}\\package.yaml", project_name), - _ => format!("/{}/package.yaml", project_name) - } - } } @@ -198,9 +166,9 @@ pub struct WithProjectManager { impl WithProjectManager { /// Constructor. pub fn new - (parent:impl AnyLogger, project_manager:Rc, project_name:ProjectName) + (project_manager:Rc, project_name:ProjectName) -> Self { - let logger = Logger::sub(parent,"WithProjectManager"); + let logger = Logger::new("initializer::WithProjectManager"); Self {logger,project_manager,project_name} } @@ -208,19 +176,12 @@ impl WithProjectManager { /// /// If the project with given name does not exist yet, it will be created. pub async fn initialize_project_model(self) -> FallibleResult { - use project_manager::MissingComponentAction::*; - let project_id = self.get_project_or_create_new().await?; - let opened_project = self.project_manager.open_project(&project_id,&Install).await?; let logger = &self.logger; - let json_endpoint = opened_project.language_server_json_address.to_string(); - let binary_endpoint = opened_project.language_server_binary_address.to_string(); - let engine_version = opened_project.engine_version; - let project_manager = Some(self.project_manager); + let project_manager = self.project_manager; let project_name = self.project_name; - let project_model = create_project_model(logger,project_manager,json_endpoint - ,binary_endpoint,engine_version,project_id,project_name); - project_model.await + model::project::Synchronized::new_opened(logger,project_manager,project_id,project_name) + .await } /// Creates a new project and returns its id, so the newly connected project can be opened. @@ -267,33 +228,6 @@ pub fn setup_global_executor() -> executor::web::EventLoopExecutor { executor } -/// Initializes the json and binary connection to Language Server, and creates a Project Model -async fn create_project_model -(logger : &Logger - , project_manager : Option> - , json_endpoint : String - , binary_endpoint : String - , engine_version : String - , project_id : Uuid - , project_name : ProjectName -) -> FallibleResult { - info!(logger, "Establishing Language Server connection."); - let client_id = Uuid::new_v4(); - let json_ws = WebSocket::new_opened(logger,&json_endpoint).await?; - let binary_ws = WebSocket::new_opened(logger,&binary_endpoint).await?; - let client_json = language_server::Client::new(json_ws); - let client_binary = binary::Client::new(logger,binary_ws); - crate::executor::global::spawn(client_json.runner()); - crate::executor::global::spawn(client_binary.runner()); - let connection_json = language_server::Connection::new(client_json,client_id).await?; - let connection_binary = binary::Connection::new(client_binary,client_id).await?; - let version = semver::Version::parse(&engine_version)?; - let ProjectName(name) = project_name; - let project = model::project::Synchronized::from_connections - (logger,project_manager,connection_json,connection_binary,version,project_id,name).await?; - Ok(Rc::new(project)) -} - // ============= @@ -309,13 +243,6 @@ mod test { - #[test] - fn new_project_engine_version_fills_requirements() { - let requirements = semver::VersionReq::parse(ENGINE_VERSION_SUPPORTED).unwrap(); - let version = semver::Version::parse(ENGINE_VERSION_FOR_NEW_PROJECTS).unwrap(); - assert!(requirements.matches(&version)) - } - #[wasm_bindgen_test(async)] async fn get_project_or_create_new() { let logger = Logger::new("test"); diff --git a/ide/src/rust/ide/src/ide/integration.rs b/ide/src/rust/ide/src/ide/integration.rs index 6da99dc3b4f7..55ff337f6990 100644 --- a/ide/src/rust/ide/src/ide/integration.rs +++ b/ide/src/rust/ide/src/ide/integration.rs @@ -1,1437 +1,137 @@ -//! View of the node editor. -// TODO[ao] this module should be completely reworked when doing the -// https://github.com/enso-org/ide/issues/597 -// There should be a wrapper for each view which "fences" the input : emitting events in this -// wrapper should not notify the outputs. +//! The integration layer between IDE controllers and the view. + +pub mod project; use crate::prelude::*; -use crate::controller::graph::Connections; -use crate::controller::graph::NodeTrees; -use crate::controller::searcher::action::MatchInfo; -use crate::controller::searcher::Actions; -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::suggestion_database; +use crate::controller::ide::StatusNotification; -use analytics; -use bimap::BiMap; -use enso_data::text::TextChange; -use enso_frp as frp; -use ensogl::display::traits::*; -use ensogl_gui_components::list_view; -use enso_protocol::language_server::ExpressionUpdatePayload; -use ide_view::graph_editor; -use ide_view::graph_editor::component::node; -use ide_view::graph_editor::component::visualization; -use ide_view::graph_editor::EdgeEndpoint; -use ide_view::graph_editor::GraphEditor; use ide_view::graph_editor::SharedHashMap; -use utils::iter::split_by_predicate; - - - -// ============== -// === Errors === -// ============== - -/// Error returned by various function inside GraphIntegration, when our mappings from controller -/// items (node or connections) to displayed items are missing some information. -#[derive(Copy,Clone,Debug,Fail)] -enum MissingMappingFor { - #[fail(display="Displayed node {:?} is not bound to any controller node.",_0)] - DisplayedNode(graph_editor::NodeId), - #[fail(display="Controller node {:?} is not bound to any displayed node.",_0)] - 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. -#[derive(Copy,Clone,Debug,Fail)] -#[fail(display="Discrepancy in a GraphEditor component")] -struct GraphEditorInconsistency; - -#[derive(Copy,Clone,Debug,Fail)] -#[fail(display="No visualization associated with view node {} found.", _0)] -struct NoSuchVisualization(graph_editor::NodeId); - -#[derive(Copy,Clone,Debug,Fail)] -#[fail(display="Graph node {} already has visualization attached.", _0)] -struct VisualizationAlreadyAttached(graph_editor::NodeId); -#[derive(Copy,Clone,Debug,Fail)] -#[fail(display="The Graph Integration hsd no SearcherController.")] -struct MissingSearcherController; +// ======================= +// === IDE Integration === +// ======================= -// ==================== -// === FencedAction === -// ==================== - -/// An utility to FRP network. It is wrapped closure in a set of FRP nodes. The closure is called -/// on `trigger`, and `is_running` contains information if we are still inside closure call. It -/// allows us to block some execution path to avoid infinite loops. -/// -/// ### Example -/// -/// Here we want to do some updates when node was added to graph, but not during set up. -/// ```rust,compile_fail -/// frp::new_network! { network -/// let set_up = FencedAction::fence(&network, |()| { -/// frp.add_node.emit(()); -/// // other things. -/// }); -/// def _update = frp.node_added.map2(&set_up.is_running, |id,is_set_up| { -/// if !is_set_up { -/// update_something(id) -/// } -/// }); -/// } -/// // This will run the set up closure, but without calling update_something. -/// set_up.trigger.emit(()); -/// ``` -#[derive(CloneRef)] -struct FencedAction { - trigger : frp::Source, - is_running : frp::Stream, -} - -impl FencedAction { - /// Wrap the `action` in `FencedAction`. - fn fence(network:&frp::Network, action:impl Fn(&Parameter) + 'static) -> Self { - frp::extend! { network - trigger <- source::(); - triggered <- trigger.constant(()); - switch <- any(...); - switch <+ triggered; - performed <- trigger.map(move |param| action(param)); - switch <+ performed; - is_running <- switch.toggle(); - } - Self {trigger,is_running} - } -} - - - -// ============================== -// === GraphEditorIntegration === -// ============================== - -/// The identifier base that will be used to name the methods introduced by "collapse nodes" -/// refactoring. Names are typically generated by taking base and appending subsequent integers, -/// until the generated name does not collide with any known identifier. -const COLLAPSED_FUNCTION_NAME:&str = "func"; - -/// The default X position of the node when user did not set any position of node - possibly when -/// node was added by editing text. -const DEFAULT_NODE_X_POSITION : f32 = -100.0; -/// The default Y position of the node when user did not set any position of node - possibly when -/// node was added by editing text. -const DEFAULT_NODE_Y_POSITION : f32 = 200.0; - -/// Default node position -- acts as a starting points for laying out nodes with no position defined -/// in the metadata. -pub fn default_node_position() -> Vector2 { - Vector2::new(DEFAULT_NODE_X_POSITION,DEFAULT_NODE_Y_POSITION) -} - -/// A structure which handles integration between controller and graph_editor EnsoGl control. -/// All changes made by user in view are reflected in controller, and all controller notifications -/// update view accordingly. -//TODO[ao] soon we should rearrange modules and crates to avoid such long names. -#[allow(missing_docs)] -#[derive(Clone,CloneRef,Debug)] -pub struct Integration { - model : Rc, - network : frp::Network, -} - -impl Integration { - /// Get GraphEditor. - pub fn graph_editor(&self) -> GraphEditor { - self.model.view.graph().clone_ref() - } - - /// Get the controller associated with this graph editor. - pub fn graph_controller(&self) -> &controller::ExecutedGraph { - &self.model.graph - } -} +// === Model === +/// The model of integration object. It is extracted and kept in Rc, so it can be referred to from +/// various FRP endpoints or executor tasks. #[derive(Debug)] struct Model { - logger : Logger, - view : ide_view::project::View, - graph : controller::ExecutedGraph, - text : controller::Text, - searcher : RefCell>, - project : model::Project, - visualization : controller::Visualization, - node_views : RefCell>, - node_view_by_expression : RefCell>, - expression_views : RefCell>, - expression_types : SharedHashMap>, - connection_views : RefCell>, - code_view : CloneRefCell, - visualizations : SharedHashMap, - error_visualizations : SharedHashMap, -} - - -// === Construction And Setup === - -impl Integration { - /// Constructor. It creates GraphEditor and integrates it with given controller handle. - pub fn new - ( view : ide_view::project::View - , graph : controller::ExecutedGraph - , text : controller::Text - , visualization : controller::Visualization - , project : model::Project - ) -> Self { - let logger = Logger::new("ViewIntegration"); - let model = Model::new(logger,view,graph,text,visualization,project); - 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; - let project_frp = &model.view.frp; - frp::new_network! {network - let invalidate = FencedAction::fence(&network,f!([model](()) { - let result = model.refresh_graph_view(); - if let Err(err) = result { - error!(model.logger,"Error while invalidating graph: {err}"); - } - })); - } - - - // === Breadcrumb Selection === - - let breadcrumbs = &model.view.graph().model.breadcrumbs; - frp::extend! {network - eval_ breadcrumbs.output.breadcrumb_pop(model.node_exited_in_ui(&()).ok()); - eval breadcrumbs.output.breadcrumb_push((local_call) { - model.expression_entered_in_ui(&local_call.as_ref().map(|local_call| { - let definition = (**local_call.definition).clone(); - let call = local_call.call; - LocalCall{call,definition} - })).ok() - }); - } - - - // === Project Renaming === - - let breadcrumbs = &model.view.graph().model.breadcrumbs; - frp::extend! {network - eval breadcrumbs.output.project_name((name) { - model.rename_project(name); - }); - } - - - // === Setting Visualization Preprocessor === - - frp::extend! { network - eval editor_outs.visualization_preprocessor_changed ([model]((node_id,preprocessor)) { - if let Err(err) = model.visualization_preprocessor_changed(*node_id,preprocessor) { - error!(model.logger, "Error when handling request for setting new \ - visualization's preprocessor code: {err}"); - } - }); - } - - - // === Visualization Reload === - - frp::extend! { network - eval editor_outs.visualization_registry_reload_requested ([model](()) { - model.view.graph().reset_visualization_registry(); - model.load_visualizations(); - }); - } - - - // === UI Actions === - - let inv = &invalidate.trigger; - let node_editing_in_ui = Model::node_editing_in_ui(Rc::downgrade(&model)); - let code_changed = Self::ui_action(&model,Model::code_changed_in_ui ,inv); - let node_removed = Self::ui_action(&model,Model::node_removed_in_ui ,inv); - let nodes_collapsed = Self::ui_action(&model,Model::nodes_collapsed_in_ui ,inv); - let node_entered = Self::ui_action(&model,Model::node_entered_in_ui ,inv); - let node_exited = Self::ui_action(&model,Model::node_exited_in_ui ,inv); - let connection_created = Self::ui_action(&model,Model::connection_created_in_ui ,inv); - let connection_removed = Self::ui_action(&model,Model::connection_removed_in_ui ,inv); - let node_moved = Self::ui_action(&model,Model::node_moved_in_ui ,inv); - let node_editing = Self::ui_action(&model,node_editing_in_ui ,inv); - let node_expression_set = Self::ui_action(&model,Model::node_expression_set_in_ui ,inv); - let used_as_suggestion = Self::ui_action(&model,Model::used_as_suggestion_in_ui ,inv); - let node_editing_committed = Self::ui_action(&model,Model::node_editing_committed_in_ui,inv); - let visualization_enabled = Self::ui_action(&model,Model::visualization_enabled_in_ui ,inv); - let visualization_disabled = Self::ui_action(&model,Model::visualization_disabled_in_ui,inv); - frp::extend! {network - eval code_editor.content ((content) model.code_view.set(content.clone_ref())); - - // Notifications from graph controller - let handle_graph_notification = FencedAction::fence(&network, - f!((notification:&Option) - model.handle_graph_notification(notification); - )); - - // Notifications from graph controller - let handle_text_notification = FencedAction::fence(&network, - f!((notification:&Option) - model.handle_text_notification(*notification); - )); - - // Changes in Graph Editor - is_handling_notification <- handle_graph_notification.is_running - || handle_text_notification.is_running; - is_hold <- is_handling_notification || invalidate.is_running; - on_connection_removed <- editor_outs.on_edge_endpoint_unset._0(); - _action <- code_editor.changed .map2(&is_hold,code_changed); - _action <- editor_outs.node_removed .map2(&is_hold,node_removed); - _action <- editor_outs.nodes_collapsed .map2(&is_hold,nodes_collapsed); - _action <- editor_outs.node_entered .map2(&is_hold,node_entered); - _action <- editor_outs.node_exited .map2(&is_hold,node_exited); - _action <- editor_outs.on_edge_endpoints_set .map2(&is_hold,connection_created); - _action <- editor_outs.visualization_enabled .map2(&is_hold,visualization_enabled); - _action <- editor_outs.visualization_disabled .map2(&is_hold,visualization_disabled); - _action <- on_connection_removed .map2(&is_hold,connection_removed); - _action <- editor_outs.node_position_set_batched.map2(&is_hold,node_moved); - _action <- editor_outs.node_being_edited .map2(&is_hold,node_editing); - _action <- editor_outs.node_expression_set .map2(&is_hold,node_expression_set); - _action <- searcher_frp.used_as_suggestion .map2(&is_hold,used_as_suggestion); - _action <- project_frp.editing_committed .map2(&is_hold,node_editing_committed); - - eval_ project_frp.editing_committed (invalidate.trigger.emit(())); - eval_ project_frp.editing_aborted (invalidate.trigger.emit(())); - eval_ project_frp.save_module (model.module_saved_in_ui()); - } - - frp::extend! { network - eval_ editor_outs.node_editing_started([] analytics::remote_log_event("graph_editor::node_editing_started")); - eval_ editor_outs.node_editing_finished([] analytics::remote_log_event("graph_editor::node_editing_finished")); - eval_ editor_outs.node_added([] analytics::remote_log_event("graph_editor::node_added")); - eval_ editor_outs.node_removed([] analytics::remote_log_event("graph_editor::node_removed")); - eval_ editor_outs.nodes_collapsed([] analytics::remote_log_event("graph_editor::nodes_collapsed")); - eval_ editor_outs.node_entered([] analytics::remote_log_event("graph_editor::node_enter_request")); - eval_ editor_outs.node_exited([] analytics::remote_log_event("graph_editor::node_exit_request")); - eval_ editor_outs.on_edge_endpoints_set([] analytics::remote_log_event("graph_editor::edge_endpoints_set")); - eval_ editor_outs.visualization_enabled([] analytics::remote_log_event("graph_editor::visualization_enabled")); - eval_ editor_outs.visualization_disabled([] analytics::remote_log_event("graph_editor::visualization_disabled")); - eval_ on_connection_removed([] analytics::remote_log_event("graph_editor::connection_removed")); - eval_ searcher_frp.used_as_suggestion([] analytics::remote_log_event("searcher::used_as_suggestion")); - eval_ project_frp.editing_committed([] analytics::remote_log_event("project::editing_committed"));} - - - let ret = Self {model,network}; - ret.connect_frp_to_graph_controller_notifications(handle_graph_notification.trigger); - ret.connect_frp_text_controller_notifications(handle_text_notification.trigger); - ret.setup_handling_project_notifications(); - ret - } - - fn spawn_sync_stream_handler(&self, stream:Stream, handler:Function) - where Stream : StreamExt + Unpin + 'static, - Function : Fn(Stream::Item,Rc) + 'static { - let model = Rc::downgrade(&self.model); - executor::global::spawn_stream_handler(model,stream,move |item,model| { - handler(item,model); - futures::future::ready(()) - }) - } - - fn setup_handling_project_notifications(&self) { - let stream = self.model.project.subscribe(); - let logger = self.model.logger.clone_ref(); - let status_bar = self.model.view.status_bar().clone_ref(); - self.spawn_sync_stream_handler(stream, move |notification,_| { - info!(logger,"Processing notification {notification:?}"); - let message = match notification { - model::project::Notification::ConnectionLost(_) => - crate::BACKEND_DISCONNECTED_MESSAGE, - }; - let message = ide_view::status_bar::event::Label::from(message); - status_bar.add_event(message); - }) - } - - fn connect_frp_to_graph_controller_notifications - (&self, frp_endpoint : frp::Source>) { - let stream = self.model.graph.subscribe(); - let logger = self.model.logger.clone_ref(); - self.spawn_sync_stream_handler(stream, move |notification,_model| { - info!(logger,"Processing notification {notification:?}"); - frp_endpoint.emit(&Some(notification)); - }) - } - - fn connect_frp_text_controller_notifications - (&self, frp_endpoint : frp::Source>) { - let stream = self.model.text.subscribe(); - let logger = self.model.logger.clone_ref(); - self.spawn_sync_stream_handler(stream, move |notification,_model| { - info!(logger,"Processing notification {notification:?}"); - frp_endpoint.emit(&Some(notification)); - }); - } - - /// Convert a function being a method of GraphEditorIntegratedWithControllerModel to a closure - /// suitable for connecting to GraphEditor frp network. Returned lambda takes `Parameter` and a - /// bool, which indicates if this action is currently on hold (e.g. due to performing - /// invalidation). - fn ui_action - ( model : &Rc - , action : Action - , invalidate : &frp::Source<()> - ) -> impl Fn(&Parameter,&bool) - where Action : Fn(&Model,&Parameter) - -> FallibleResult + 'static { - f!([model,invalidate] (parameter,is_hold) { - if !*is_hold { - let result = action(&*model,parameter); - if let Err(err) = result { - error!(model.logger,"Error while performing UI action on controllers: {err}"); - info!(model.logger,"Invalidating displayed graph"); - invalidate.emit(()); - } - } - }) - } + logger : Logger, + controller : controller::Ide, + view : ide_view::project::View, + project_integration : RefCell>, } impl Model { - fn new - ( logger : Logger - , view : ide_view::project::View - , graph : controller::ExecutedGraph - , text : controller::Text - , visualization : controller::Visualization - , project : model::Project) -> 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 this = Model - {logger,view,graph,text,searcher,project,visualization,node_views - ,node_view_by_expression,expression_views,expression_types,connection_views - ,code_view,visualizations,error_visualizations}; + /// Create a new project integration + fn setup_and_display_new_project(self:Rc) { + // Remove the old integration first. We want to be sure the old and new integrations will + // not race for the view. + *self.project_integration.borrow_mut() = None; - this.init_project_name(); - this.load_visualizations(); - if let Err(err) = this.refresh_graph_view() { - error!(this.logger,"Error while initializing graph editor: {err}."); - } - if let Err(err) = this.refresh_code_editor() { - error!(this.logger,"Error while initializing code editor: {err}."); - } - this - } + let project_model = self.controller.current_project(); + let status_notifications = self.controller.status_notifications().clone_ref(); + let project = controller::Project::new(project_model,status_notifications.clone_ref()); - fn load_visualizations(&self) { - let logger = self.logger.clone_ref(); - let controller = self.visualization.clone_ref(); - let graph_editor = self.view.graph().clone_ref(); executor::global::spawn(async move { - let identifiers = controller.list_visualizations().await; - let identifiers = identifiers.unwrap_or_default(); - for identifier in identifiers { - match controller.load_visualization(&identifier).await { - Ok(visualization) => { - graph_editor.frp.register_visualization.emit(Some(visualization)); - } - Err(err) => { - error!(logger, "Error while loading visualization {identifier}: {err:?}"); - } + match project.initialize().await { + Ok(result) => { + let view = self.view.clone_ref(); + let text = result.main_module_text; + let graph = result.main_graph; + let ide = self.controller.clone_ref(); + let project = project.model; + *self.project_integration.borrow_mut() = Some(project::Integration::new(view,graph,text,ide,project)); + } + Err(err) => { + let err_msg = format!("Failed to initialize project: {}", err); + error!(self.logger,"{err_msg}"); + status_notifications.publish_event(err_msg) } } - info!(logger, "Visualizations Initialized."); }); } } -// === Project renaming === - -impl Model { - fn init_project_name(&self) { - let project_name = self.project.name().to_string(); - self.view.graph().model.breadcrumbs.input.project_name.emit(project_name); - } +// === Integration === - fn rename_project(&self, name:impl Str) { - if self.project.name() != name.as_ref() { - let project = self.project.clone_ref(); - let breadcrumbs = self.view.graph().model.breadcrumbs.clone_ref(); - let logger = self.logger.clone_ref(); - let name = name.into(); - executor::global::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(()); - } - }); - } - } +/// The Integration Object +/// +/// It is responsible for integrating IDE controllers and views, so user actions will work, and +/// notifications from controllers will update the view. +#[derive(Clone,CloneRef,Debug)] +pub struct Integration { + model : Rc, } - -// === Updating Graph View === - -impl Model { - /// Refresh displayed code to be up to date with module state. - pub fn refresh_code_editor(&self) -> FallibleResult { - let current_code = self.code_view.get().to_string(); - let new_code = self.graph.graph().module.ast().repr(); - if new_code != current_code { - self.code_view.set(new_code.as_str().into()); - self.view.code_editor().text_area().set_content(new_code); - } - Ok(()) - } - - /// Reload whole displayed content to be up to date with module state. - pub fn refresh_graph_view(&self) -> FallibleResult { - info!(self.logger, "Refreshing the graph view."); - let connections_info = self.graph.connections()?; - self.refresh_node_views(&connections_info, true)?; - self.refresh_connection_views(connections_info.connections)?; - Ok(()) - } - - fn refresh_node_views - (&self, connections_info:&Connections, update_position:bool) -> FallibleResult { - let base_default_position = default_node_position(); - let mut trees = connections_info.trees.clone(); - let nodes = self.graph.graph().nodes()?; - let (without_pos,with_pos) = split_by_predicate(&nodes, |n| n.has_position()); - let bottommost_node_pos = with_pos.iter() - .filter_map(|node| node.metadata.as_ref()?.position) - .min_by(model::module::Position::ord_by_y) - .map(|pos| pos.vector) - .unwrap_or(base_default_position); - - let default_positions : HashMap<_,_> = (1..).zip(&without_pos).map(|(i,node)| { - let dy = i as f32 * self.view.default_gap_between_nodes.value(); - let pos = Vector2::new(bottommost_node_pos.x, bottommost_node_pos.y - dy); - (node.info.id(), pos) - }).collect(); - - let ids = nodes.iter().map(|node| node.info.id()).collect(); - self.retain_node_views(&ids); - for node_info in &nodes { - let id = node_info.info.id(); - let node_trees = trees.remove(&id).unwrap_or_else(default); - let default_pos = default_positions.get(&id).unwrap_or(&base_default_position); - let displayed = self.node_views.borrow_mut().get_by_left(&id).cloned(); - match displayed { - Some(displayed) => { - if update_position { - self.refresh_node_position(displayed,node_info) - }; - self.refresh_node_expression(displayed,node_info,node_trees); +impl Integration { + /// Create the integration of given controller and view. + pub fn new(controller:controller::Ide, view:ide_view::project::View) -> Self { + let logger = Logger::new("ide::Integration"); + let project_integration = default(); + let model = Rc::new(Model {logger,controller,view,project_integration}); + Self {model} . init() + } + + /// Initialize integration, so FRP outputs of the view will call the proper controller methods, + /// and controller notifications will be delivered to the view accordingly. + pub fn init(self) -> Self { + self.initialize_status_bar_integration(); + self.initialize_controller_integration(); + self.model.clone_ref().setup_and_display_new_project(); + self + } + + fn initialize_status_bar_integration(&self) { + use controller::ide::BackgroundTaskHandle as ControllerHandle; + use ide_view::status_bar::process::Id as ViewHandle; + + let logger = self.model.logger.clone_ref(); + let process_map = SharedHashMap::::new(); + let status_bar = self.model.view.status_bar().clone_ref(); + let status_notif_sub = self.model.controller.status_notifications().subscribe(); + let status_notif_updates = status_notif_sub.for_each(move |notification| { + info!(logger, "Received notification {notification:?}"); + match notification { + StatusNotification::Event {label} => { + status_bar.add_event(ide_view::status_bar::event::Label::new(label)); }, - None => self.create_node_view(node_info,node_trees,*default_pos), - } - } - Ok(()) - } - - /// Refresh the expressions (e.g., types, ports) for all nodes. - fn refresh_graph_expressions(&self) -> FallibleResult { - info!(self.logger, "Refreshing the graph expressions."); - let connections = self.graph.connections()?; - self.refresh_node_views(&connections, false)?; - self.refresh_connection_views(connections.connections) - } - - /// Retain only given nodes in displayed graph. - fn retain_node_views(&self, ids:&HashSet) { - let to_remove = { - let borrowed = self.node_views.borrow(); - let filtered = borrowed.iter().filter(|(id,_)| !ids.contains(id)); - filtered.map(|(k,v)| (*k,*v)).collect_vec() - }; - for (id,displayed_id) in to_remove { - self.view.graph().frp.input .remove_node.emit(&displayed_id); - self.node_views.borrow_mut().remove_by_left(&id); - } - } - - fn create_node_view - (&self, info:&controller::graph::Node, trees:NodeTrees, default_pos:Vector2) { - let id = info.info.id(); - let displayed_id = self.view.graph().add_node(); - self.refresh_node_view(displayed_id, info, trees); - if info.metadata.as_ref().and_then(|md| md.position).is_none() { - self.view.graph().frp.input.set_node_position.emit(&(displayed_id, default_pos)); - // If position wasn't present in metadata, we initialize it. - if let Err(err) = self.set_node_metadata_position(id,default_pos) { - debug!(self.logger, "Failed to set default position information IDs: {id:?} because \ - of error {err:?}"); - } - } - self.node_views.borrow_mut().insert(id, displayed_id); - } - - fn deserialize_visualization_data - (data:VisualizationUpdateData) -> FallibleResult { - let binary = data.as_ref(); - let as_text = std::str::from_utf8(binary)?; - let as_json : serde_json::Value = serde_json::from_str(as_text)?; - Ok(visualization::Data::from(as_json)) - } - - fn refresh_node_view - (&self, id:graph_editor::NodeId, node:&controller::graph::Node, trees:NodeTrees) { - self.refresh_node_position(id,node); - self.refresh_node_expression(id, node, trees) - } - - /// Update the position of the node based on its current metadata. - fn refresh_node_position(&self, id:graph_editor::NodeId, node:&controller::graph::Node) { - let position = node.metadata.as_ref().and_then(|md| md.position); - if let Some(position) = position { - self.view.graph().frp.input.set_node_position.emit(&(id, position.vector)); - } - } - - /// Update the expression of the node and all related properties e.g., types, ports). - fn refresh_node_expression(&self, id:graph_editor::NodeId, node:&controller::graph::Node, trees:NodeTrees) { - let code_and_trees = graph_editor::component::node::Expression { - pattern : node.info.pattern().map(|t|t.repr()), - code : node.info.expression().repr(), - whole_expression_id : node.info.expression().id , - input_span_tree : trees.inputs, - output_span_tree : trees.outputs.unwrap_or_else(default) - }; - let expression_changed = - !self.expression_views.borrow().get(&id).contains(&&code_and_trees); - if expression_changed { - for sub_expression in node.info.ast().iter_recursive() { - if let Some(expr_id) = sub_expression.id { - self.node_view_by_expression.borrow_mut().insert(expr_id,id); - } - } - self.view.graph().frp.input.set_node_expression.emit(&(id,code_and_trees.clone())); - self.expression_views.borrow_mut().insert(id,code_and_trees); - } - - // Set initially available type information on ports (identifiable expression's sub-parts). - for expression_part in node.info.expression().iter_recursive() { - if let Some(id) = expression_part.id { - self.refresh_computed_info(id,expression_changed); - } - } - } - - /// Like `refresh_computed_info` but for multiple expressions. - fn refresh_computed_infos(&self, expressions_to_refresh:&[ExpressionId]) -> FallibleResult { - debug!(self.logger, "Refreshing type information for IDs: {expressions_to_refresh:?}."); - for id in expressions_to_refresh { - self.refresh_computed_info(*id,false) - } - Ok(()) - } - - /// Look up the computed information for a given expression and pass the information to the - /// graph editor view. - /// - /// The computed value information includes the expression type and the target method pointer. - fn refresh_computed_info(&self, id:ExpressionId, force_type_info_refresh:bool) { - let info = self.lookup_computed_info(&id); - let info = info.as_ref(); - let typename = info.and_then(|info| info.typename.clone().map(graph_editor::Type)); - if let Some(node_id) = self.node_view_by_expression.borrow().get(&id).cloned() { - self.set_type(node_id,id,typename, force_type_info_refresh); - let method_pointer = info.and_then(|info| { - info.method_call.and_then(|entry_id| { - let opt_method = self.project.suggestion_db().lookup_method_ptr(entry_id).ok(); - opt_method.map(|method| graph_editor::MethodPointer(Rc::new(method))) - }) - }); - self.set_method_pointer(id,method_pointer); - if self.node_views.borrow().get_by_left(&id).contains(&&node_id) { - let set_error_result = self.set_error(node_id,info.map(|info| &info.payload)); - if let Err(error) = set_error_result { - error!(self.logger, "Error when setting error on node: {error}"); - } - } - } - } - - /// Set given type (or lack of such) on the given sub-expression. - fn set_type - ( &self - , node_id : graph_editor::NodeId - , id : ExpressionId - , typename : Option - , force_refresh : bool - ) { - // We suppress spurious type information updates here, as they were causing performance - // issues. See: https://github.com/enso-org/ide/issues/952 - let previous_type_opt = self.expression_types.insert(id,typename.clone()); - if force_refresh || previous_type_opt.as_ref() != Some(&typename) { - let event = (node_id,id,typename); - self.view.graph().frp.input.set_expression_usage_type.emit(&event); - } - } - - /// Set given method pointer (or lack of such) on the given sub-expression. - fn set_method_pointer(&self, id:ExpressionId, method:Option) { - let event = (id,method); - self.view.graph().frp.input.set_method_pointer.emit(&event); - } - - /// 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()); - if error.is_some() && !self.error_visualizations.contains_key(&node_id) { - use graph_editor::builtin::visualization::native::error; - let endpoint = self.view.graph().frp.set_error_visualization_data.clone_ref(); - let metadata = error::metadata(); - let vis_map = self.error_visualizations.clone_ref(); - self.attach_visualization(node_id,&metadata,endpoint,vis_map)?; - } else if error.is_none() && self.error_visualizations.contains_key(&node_id) { - self.detach_visualization(node_id,self.error_visualizations.clone_ref())?; - } - Ok(()) - } - - fn convert_payload_to_error_view - (&self, payload:Option<&ExpressionUpdatePayload>, node_id:graph_editor::NodeId) - -> Option { - use ExpressionUpdatePayload::*; - use node::error::Kind; - let (kind,message,trace) = match payload { - None | - Some(Value ) => None, - Some(DataflowError { trace }) => Some((Kind::Dataflow, None ,trace)), - Some(Panic { message,trace }) => Some((Kind::Panic , Some(message),trace)), - }?; - let propagated = if kind == Kind::Panic { - let root_cause = self.get_node_causing_error_on_current_graph(&trace); - !root_cause.contains(&node_id) - } else { - // TODO[ao]: traces are not available for Dataflow errors. - false - }; - - let kind = Immutable(kind); - let message = Rc::new(message.cloned()); - let propagated = Immutable(propagated); - Some(node::error::Error {kind,message,propagated}) - } - - /// Get the node being a main cause of some error from the current nodes on the scene. Returns - /// [`None`] if the error is not present on the scene at all. - fn get_node_causing_error_on_current_graph - (&self, trace:&[ExpressionId]) -> Option { - let node_view_by_expression = self.node_view_by_expression.borrow(); - trace.iter().find_map(|expr_id| node_view_by_expression.get(&expr_id).copied()) - } - - /// Set the position in the node's metadata. - fn set_node_metadata_position(&self, node_id:ast::Id,pos:Vector2) -> FallibleResult { - self.graph.graph().module.with_node_metadata(node_id, Box::new(|md| { - md.position = Some(model::module::Position::new(pos.x,pos.y)); - })) - } - - fn refresh_connection_views - (&self, connections:Vec) -> FallibleResult { - self.retain_connection_views(&connections); - for con in connections { - if !self.connection_views.borrow().contains_left(&con) { - let targets = self.edge_targets_from_controller_connection(con.clone())?; - self.view.graph().frp.input.connect_nodes.emit(&targets); - let edge_id = self.view.graph().frp.output.on_edge_add.value(); - self.connection_views.borrow_mut().insert(con, edge_id); - } - } - Ok(()) - } - - fn edge_targets_from_controller_connection - (&self, connection:controller::graph::Connection) -> FallibleResult<(EdgeEndpoint,EdgeEndpoint)> { - let src_node = self.get_displayed_node_id(connection.source.node)?; - let dst_node = self.get_displayed_node_id(connection.destination.node)?; - let src = EdgeEndpoint::new(src_node,connection.source.port); - let data = EdgeEndpoint::new(dst_node,connection.destination.port); - Ok((src,data)) - } - - /// Retain only given connections in displayed graph. - fn retain_connection_views(&self, connections:&[controller::graph::Connection]) { - let to_remove = { - let borrowed = self.connection_views.borrow(); - let filtered = borrowed.iter().filter(|(con,_)| !connections.contains(con)); - filtered.map(|(_,edge_id)| *edge_id).collect_vec() - }; - for edge_id in to_remove { - self.view.graph().frp.input.remove_edge.emit(&edge_id); - self.connection_views.borrow_mut().remove_by_right(&edge_id); - } - } -} - - -// === Handling Controller Notifications === - -impl Model { - /// Handle notification received from controller about the whole graph being invalidated. - pub fn on_graph_invalidated(&self) -> FallibleResult { - self.refresh_graph_view() - } - - /// Handle notification received from controller about the graph receiving new type information. - pub fn on_graph_expression_update(&self) -> FallibleResult { - self.refresh_graph_expressions() - } - - /// Handle notification received from controller about the whole graph being invalidated. - pub fn on_text_invalidated(&self) -> FallibleResult { - self.refresh_code_editor() - } - - /// Handle notification received from controller about values having been entered. - pub fn on_node_entered(&self, local_call:&LocalCall) -> FallibleResult { - analytics::remote_log_event("integration::node_entered"); - let definition = local_call.definition.clone().into(); - let call = local_call.call; - let local_call = graph_editor::LocalCall{call,definition}; - self.view.graph().frp.deselect_all_nodes.emit(&()); - self.view.graph().model.breadcrumbs.push_breadcrumb.emit(&Some(local_call)); - self.request_detaching_all_visualizations(); - self.refresh_graph_view() - } - - /// Handle notification received from controller about node having been exited. - 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.view.graph().model.breadcrumbs.pop_breadcrumb.emit(()); - let id = self.get_displayed_node_id(id)?; - self.view.graph().frp.select_node.emit(&id); - Ok(()) - } - - /// Handle notification received from controller about values having been computed. - pub fn on_values_computed(&self, expressions:&[ExpressionId]) -> FallibleResult { - 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) { - use controller::graph::executed::Notification; - use controller::graph::Notification::*; - - debug!(self.logger, "Received graph notification {notification:?}"); - let result = match notification { - Some(Notification::Graph(Invalidate)) => self.on_graph_invalidated(), - Some(Notification::Graph(PortsUpdate)) => self.on_graph_expression_update(), - Some(Notification::ComputedValueInfo(update)) => self.on_values_computed(update), - Some(Notification::SteppedOutOfNode(id)) => self.on_node_exited(*id), - Some(Notification::EnteredNode(local_call)) => self.on_node_entered(local_call), - None => { - warning!(self.logger,"Handling `None` notification is not implemented; \ - performing full invalidation"); - self.refresh_graph_view() - } - }; - if let Err(err) = result { - error!(self.logger,"Error while updating graph after receiving {notification:?} from \ - controller: {err}"); - } - } - - /// Handle notification received from Text Controller. - pub fn handle_text_notification(&self, notification:Option) { - use controller::text::Notification; - - debug!(self.logger, "Received text notification {notification:?}"); - let result = match notification { - Some(Notification::Invalidate) => self.on_text_invalidated(), - other => { - warning!(self.logger,"Handling notification {other:?} is not implemented; \ - performing full invalidation"); - self.refresh_code_editor() - } - }; - if let Err(err) = result { - error!(self.logger,"Error while updating graph after receiving {notification:?} from \ - controller: {err}"); - } - } - - pub fn handle_searcher_notification(&self, notification:controller::searcher::Notification) { - use controller::searcher::Notification; - use controller::searcher::UserAction; - debug!(self.logger, "Received searcher notification {notification:?}"); - match notification { - Notification::NewActionList => with(self.searcher.borrow(), |searcher| { - if let Some(searcher) = &*searcher { - match searcher.actions() { - Actions::Loading => self.view.searcher().clear_actions(), - Actions::Loaded {list:actions} => { - let list_is_empty = actions.matching_count() == 0; - let user_action = searcher.current_user_action(); - let intended_function = searcher.intended_function_suggestion(); - let provider = DataProviderForView - { actions,user_action,intended_function}; - self.view.searcher().set_actions(Rc::new(provider)); - - // Usually we want to select first entry and display docs for it - // But not when user finished typing function or argument. - let starting_typing = user_action == UserAction::StartingTypingArgument; - if !starting_typing && !list_is_empty { - self.view.searcher().select_action(0); - } - } - Actions::Error(err) => { - error!(self.logger, "Error while obtaining list from searcher: {err}"); - self.view.searcher().clear_actions(); - }, - }; - } - }) - } - } -} - - -// === Passing UI Actions To Controllers === - -// These functions are called with FRP event values as arguments. The FRP values are always provided -// by reference, including "trivially-copy" types and Vecs, To keep code cleaner we take -// all parameters by reference. -#[allow(clippy::trivially_copy_pass_by_ref)] -#[allow(clippy::ptr_arg)] -impl Model { - fn node_removed_in_ui(&self, node:&graph_editor::NodeId) -> FallibleResult { - debug!(self.logger, "Removing node."); - let id = self.get_controller_node_id(*node)?; - self.node_views.borrow_mut().remove_by_left(&id); - self.graph.graph().remove_node(id)?; - Ok(()) - } - - fn node_moved_in_ui - (&self, (displayed_id,pos):&(graph_editor::NodeId,Vector2)) -> FallibleResult { - debug!(self.logger, "Moving node."); - if let Ok(id) = self.get_controller_node_id(*displayed_id) { - self.set_node_metadata_position(id,*pos)?; - } - Ok(()) - } - - fn nodes_collapsed_in_ui - (&self, (collapsed,_new_node_view_id):&(Vec,graph_editor::NodeId)) - -> FallibleResult { - debug!(self.logger, "Collapsing node."); - let ids = self.get_controller_node_ids(collapsed)?; - let _new_node_id = self.graph.graph().collapse(ids,COLLAPSED_FUNCTION_NAME)?; - // TODO [mwu] https://github.com/enso-org/ide/issues/760 - // As part of this issue, storing relation between new node's controller and view ids will - // be necessary. - Ok(()) - } - - fn node_expression_set_in_ui - (&self, (displayed_id,expression):&(graph_editor::NodeId,String)) -> FallibleResult { - debug!(self.logger, "Setting node expression."); - let searcher = self.searcher.borrow(); - let code_and_trees = graph_editor::component::node::Expression::new_plain(expression); - self.expression_views.borrow_mut().insert(*displayed_id,code_and_trees); - if let Some(searcher) = searcher.as_ref() { - searcher.set_input(expression.clone())?; - } - Ok(()) - } - - fn node_editing_in_ui(weak_self:Weak) - -> impl Fn(&Self,&Option) -> FallibleResult { - move |this,displayed_id| { - if let Some(displayed_id) = displayed_id { - debug!(this.logger, "Starting node editing."); - let id = this.get_controller_node_id(*displayed_id); - let mode = match id { - Ok(node_id) => controller::searcher::Mode::EditNode {node_id}, - Err(MissingMappingFor::DisplayedNode(id)) => { - let node_view = this.view.graph().model.nodes.get_cloned_ref(&id); - let position = node_view.map(|node| node.position().xy()); - let position = position.map(|vector| model::module::Position{vector}); - controller::searcher::Mode::NewNode {position} - }, - Err(other) => return Err(other.into()), - }; - let selected_nodes = this.view.graph().model.selected_nodes().iter().filter_map(|id| { - this.get_controller_node_id(*id).ok() - }).collect_vec(); - let controller = this.graph.clone_ref(); - let searcher = controller::Searcher::new_from_graph_controller - (&this.logger,&this.project,controller,mode,selected_nodes)?; - executor::global::spawn(searcher.subscribe().for_each(f!([weak_self](notification) { - if let Some(this) = weak_self.upgrade() { - this.handle_searcher_notification(notification); + StatusNotification::BackgroundTaskStarted {label,handle} => { + status_bar.add_process(ide_view::status_bar::process::Label::new(label)); + let view_handle = status_bar.last_process.value(); + process_map.insert(handle,view_handle); + }, + StatusNotification::BackgroundTaskFinished {handle} => { + if let Some(view_handle) = process_map.remove(&handle) { + status_bar.finish_process(view_handle); + } else { + warning!(logger, "Controllers finished process not displayed in view"); } - futures::future::ready(()) - }))); - *this.searcher.borrow_mut() = Some(searcher); - } else { - debug!(this.logger, "Finishing node editing."); - } - Ok(()) - } - } - - fn used_as_suggestion_in_ui - (&self, entry:&Option) -> FallibleResult { - debug!(self.logger, "Using as suggestion."); - if let Some(entry) = entry { - let graph_frp = &self.view.graph().frp; - let error = || MissingSearcherController; - let searcher = self.searcher.borrow().clone().ok_or_else(error)?; - let error = || GraphEditorInconsistency; - let edited_node = graph_frp.output.node_being_edited.value().ok_or_else(error)?; - let code = searcher.use_as_suggestion(*entry)?; - let code_and_trees = node::Expression::new_plain(code); - graph_frp.input.set_node_expression.emit(&(edited_node,code_and_trees)); - } - Ok(()) - } - - fn node_editing_committed_in_ui - (&self, (displayed_id,entry_id):&(graph_editor::NodeId, Option)) - -> FallibleResult { - use crate::controller::searcher::action::Action::Example; - debug!(self.logger, "Committing node expression."); - let error = || MissingSearcherController; - let searcher = self.searcher.replace(None).ok_or_else(error)?; - let entry = searcher.actions().list().zip(*entry_id).and_then(|(l,i)| l.get_cloned(i)); - let is_example = entry.map_or(false, |e| matches!(e.action,Example(_))); - let result = if let Some(id) = entry_id { - searcher.execute_action_by_index(*id) - } else { - searcher.commit_node().map(Some) - }; - match result { - Ok(Some(node_id)) => { - self.node_views.borrow_mut().insert(node_id,*displayed_id); - if is_example { - self.view.graph().frp.enable_visualization(displayed_id); } - Ok(()) - } - Ok(None) => Ok(()), - Err(err) => { - self.view.graph().frp.remove_node.emit(displayed_id); - Err(err) - } - } - } - - fn connection_created_in_ui(&self, edge_id:&graph_editor::EdgeId) -> FallibleResult { - debug!(self.logger, "Creating connection."); - let displayed = self.view.graph().model.edges.get_cloned(&edge_id).ok_or(GraphEditorInconsistency)?; - let con = self.controller_connection_from_displayed(&displayed)?; - let inserting = self.connection_views.borrow_mut().insert(con.clone(), *edge_id); - if inserting.did_overwrite() { - warning!(self.logger,"Created connection {edge_id} overwrite some old mappings in \ - GraphEditorIntegration.") - } - self.graph.connect(&con)?; - Ok(()) - } - - fn connection_removed_in_ui(&self, edge_id:&graph_editor::EdgeId) -> FallibleResult { - debug!(self.logger, "Removing connection."); - let connection = self.get_controller_connection(*edge_id)?; - self.connection_views.borrow_mut().remove_by_left(&connection); - self.graph.disconnect(&connection)?; - Ok(()) - } - - fn visualization_enabled_in_ui - (&self, (node_id,vis_metadata):&(graph_editor::NodeId,visualization::Metadata)) - -> FallibleResult { - 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(()) - } - - fn visualization_disabled_in_ui(&self, node_id:&graph_editor::NodeId) -> FallibleResult { - self.detach_visualization(*node_id,self.visualizations.clone_ref()) - } - - fn expression_entered_in_ui - (&self, local_call:&Option) -> FallibleResult { - if let Some(local_call) = local_call { - let local_call = local_call.clone(); - let controller = self.graph.clone_ref(); - let logger = self.logger.clone_ref(); - let enter_action = async move { - info!(logger,"Entering node."); - if let Err(e) = controller.enter_method_pointer(&local_call).await { - error!(logger,"Entering node failed: {e}."); - - let event = "integration::entering_node_failed"; - let field = "error"; - let data = analytics::AnonymousData(|| e.to_string()); - analytics::remote_log_value(event,field,data) - } - }; - executor::global::spawn(enter_action); - } - Ok(()) - } - - fn node_entered_in_ui(&self, node_id:&graph_editor::NodeId) -> FallibleResult { - debug!(self.logger,"Requesting entering the node {node_id}."); - let call = self.get_controller_node_id(*node_id)?; - let method_pointer = self.graph.node_method_pointer(call)?; - let definition = (*method_pointer).clone(); - let local_call = LocalCall{call,definition}; - self.expression_entered_in_ui(&Some(local_call)) - } - - fn node_exited_in_ui(&self, _:&()) -> FallibleResult { - debug!(self.logger,"Requesting exiting the current node."); - let controller = self.graph.clone_ref(); - let logger = self.logger.clone_ref(); - let exit_node_action = async move { - info!(logger,"Exiting node."); - if let Err(e) = controller.exit_node().await { - debug!(logger, "Exiting node failed: {e}."); - - let event = "integration::exiting_node_failed"; - let field = "error"; - let data = analytics::AnonymousData(|| e.to_string()); - analytics::remote_log_value(event,field,data) - } - }; - executor::global::spawn(exit_node_action); - Ok(()) - } - - fn code_changed_in_ui(&self, changes:&Vec) -> FallibleResult { - for change in changes { - let range_start = data::text::Index::new(change.range.start.value as usize); - let range_end = data::text::Index::new(change.range.end.value as usize); - let converted = TextChange::replace(range_start..range_end,change.text.to_string()); - self.text.apply_text_change(converted)?; - } - Ok(()) - } - - fn module_saved_in_ui(&self) { - 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 { - if let Err(err) = controller.store_content(content).await { - error!(logger, "Error while saving file: {err:?}"); - } - }); - } - - fn resolve_visualization_context - (&self, context:&visualization::instance::ContextModule) - -> FallibleResult { - use visualization::instance::ContextModule::*; - match context { - ProjectMain => 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 - ) -> FallibleResult { - if let Some(visualization) = self.visualizations.get_copied(&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)?; - executor::global::spawn(async move { - let result = controller.set_visualization_preprocessor(visualization,code,module); - if let Err(err) = result.await { - error!(logger, "Error when setting visualization preprocessor: {err}"); - } - }); - Ok(()) - } else { - Err(MissingMappingFor::DisplayedVisualization(node_id).into()) - } - } -} - - -// === Utilities === - -impl Model { - fn get_controller_node_id - (&self, displayed_id:graph_editor::NodeId) -> Result { - let err = MissingMappingFor::DisplayedNode(displayed_id); - self.node_views.borrow().get_by_right(&displayed_id).cloned().ok_or(err) - } - - fn get_controller_node_ids - (&self, displayed_ids:impl IntoIterator>) - -> Result, MissingMappingFor> { - use std::borrow::Borrow; - displayed_ids.into_iter().map(|id| { - let id = id.borrow(); - self.get_controller_node_id(*id) - }).collect() - } - - fn get_displayed_node_id - (&self, node_id:ast::Id) -> Result { - let err = MissingMappingFor::ControllerNode(node_id); - self.node_views.borrow().get_by_left(&node_id).cloned().ok_or(err) - } - - fn get_controller_connection - (&self, displayed_id:graph_editor::EdgeId) - -> Result { - let err = MissingMappingFor::DisplayedConnection(displayed_id); - self.connection_views.borrow().get_by_right(&displayed_id).cloned().ok_or(err) - } - - fn controller_connection_from_displayed - (&self, connection:&graph_editor::Edge) -> FallibleResult { - let src = connection.source().ok_or(GraphEditorInconsistency {})?; - let dst = connection.target().ok_or(GraphEditorInconsistency {})?; - let src_node = self.get_controller_node_id(src.node_id)?; - let dst_node = self.get_controller_node_id(dst.node_id)?; - Ok(controller::graph::Connection { - source : controller::graph::Endpoint::new(src_node,&src.port), - destination : controller::graph::Endpoint::new(dst_node,&dst.port), - }) - } - - fn lookup_computed_info(&self, id:&ExpressionId) -> Option> { - let registry = self.graph.computed_value_info_registry(); - 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 : SharedHashMap - ) -> FallibleResult { - // Do nothing if there is already a visualization attached. - let err = || VisualizationAlreadyAttached(node_id); - (!visualizations_map.contains_key(&node_id)).ok_or_else(err)?; - - debug!(self.logger, "Attaching visualization on node {node_id}."); - let visualization = self.prepare_visualization(node_id,vis_metadata)?; - let id = visualization.id; - let update_handler = self.visualization_update_handler(receive_data_endpoint,node_id); - let logger = self.logger.clone_ref(); - let controller = self.graph.clone_ref(); - - // We cannot do this in the async block, as the user may decide to detach before server - // confirms that we actually have attached. - visualizations_map.insert(node_id,id); - - executor::global::spawn(async move { - if let Ok(stream) = controller.attach_visualization(visualization).await { - debug!(logger, "Successfully attached visualization {id} for node {node_id}."); - let updates_handler = stream.for_each(update_handler); - executor::global::spawn(updates_handler); - } else { - visualizations_map.remove(&node_id); - } - }); - Ok(id) - } - - /// Return an asynchronous event processor that routes visualization update to the given's - /// visualization respective FRP endpoint. - fn visualization_update_handler - ( &self - , endpoint : frp::Any<(graph_editor::NodeId,visualization::Data)> - , node_id : graph_editor::NodeId - ) -> impl FnMut(VisualizationUpdateData) -> futures::future::Ready<()> { - // TODO [mwu] - // For now only JSON visualizations are supported, so we can just assume JSON data in the - // binary package. - let logger = self.logger.clone_ref(); - move |update| { - match Self::deserialize_visualization_data(update) { - Ok (data) => endpoint.emit((node_id,data)), - Err(error) => - // TODO [mwu] - // We should consider having the visualization also accept error input. - error!(logger, "Failed to deserialize visualization update. {error}"), } futures::future::ready(()) - } - } - - /// Create a controller-compatible description of the visualization based on the input received - /// from the graph editor endpoints. - fn prepare_visualization - (&self, node_id:graph_editor::NodeId, metadata:&visualization::Metadata) - -> FallibleResult { - let module_designation = &metadata.preprocessor.module; - let visualisation_module = self.resolve_visualization_context(module_designation)?; - let id = VisualizationId::new_v4(); - let expression = metadata.preprocessor.code.to_string(); - let ast_id = self.get_controller_node_id(node_id)?; - Ok(Visualization{id,ast_id,expression,visualisation_module}) - } - - fn detach_visualization - ( &self - , node_id : graph_editor::NodeId - , visualizations_map : SharedHashMap - ) -> FallibleResult { - debug!(self.logger,"Node editor wants to detach visualization on {node_id}."); - let err = || NoSuchVisualization(node_id); - let id = visualizations_map.get_copied(&node_id).ok_or_else(err)?; - 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(()) - } -} - - -// =========================== -// === DataProviderForView === -// =========================== - -#[derive(Clone,Debug)] -struct DataProviderForView { - actions : Rc, - user_action : controller::searcher::UserAction, - intended_function : Option, -} - -impl DataProviderForView { - fn doc_placeholder_for(suggestion:&controller::searcher::action::Suggestion) -> String { - let title = match suggestion.kind { - suggestion_database::entry::Kind::Atom => "Atom", - suggestion_database::entry::Kind::Function => "Function", - suggestion_database::entry::Kind::Local => "Local variable", - suggestion_database::entry::Kind::Method => "Method", - }; - let code = suggestion.code_to_insert(None,true).code; - format!("{} `{}`\n\nNo documentation available", title,code) + executor::global::spawn(status_notif_updates) } -} -impl list_view::entry::ModelProvider for DataProviderForView { - fn entry_count(&self) -> usize { - self.actions.matching_count() - } - - fn get(&self, id: usize) -> Option { - let action = self.actions.get_cloned(id)?; - if let MatchInfo::Matches {subsequence} = action.match_info { - let caption = action.action.caption(); - let model = list_view::entry::Model::new(caption.clone()); - let mut char_iter = caption.char_indices().enumerate(); - let highlighted_iter = subsequence.indices.iter().filter_map(|idx| loop { - if let Some(char) = char_iter.next() { - let (char_idx,(byte_id,char)) = char; - if char_idx == *idx { - let start = ensogl_text::Bytes(byte_id as i32); - let end = ensogl_text::Bytes((byte_id + char.len_utf8()) as i32); - break Some(ensogl_text::Range::new(start,end)) + fn initialize_controller_integration(&self) { + let stream = self.model.controller.subscribe(); + let weak = Rc::downgrade(&self.model); + executor::global::spawn(stream.for_each(move |notification| { + if let Some(model) = weak.upgrade() { + match notification { + controller::ide::Notification::NewProjectCreated => { + model.setup_and_display_new_project() } - } else { - break None; } - }); - let model = model.highlight(highlighted_iter); - Some(model) - } else { - None - } - } -} - -impl ide_view::searcher::DocumentationProvider for DataProviderForView { - fn get(&self) -> Option { - use controller::searcher::UserAction::*; - self.intended_function.as_ref().and_then(|function| match self.user_action { - StartingTypingArgument => function.documentation.clone(), - _ => None - }) - } - - fn get_for_entry(&self, id:usize) -> Option { - use controller::searcher::action::Action; - match self.actions.get_cloned(id)?.action { - Action::Suggestion(suggestion) => { - let doc = suggestion.documentation.clone(); - Some(doc.unwrap_or_else(|| Self::doc_placeholder_for(&suggestion))) } - Action::Example(example) => Some(example.documentation.clone()) - } + futures::future::ready(()) + })); } } diff --git a/ide/src/rust/ide/src/ide/integration/project.rs b/ide/src/rust/ide/src/ide/integration/project.rs new file mode 100644 index 000000000000..147a2a71ca55 --- /dev/null +++ b/ide/src/rust/ide/src/ide/integration/project.rs @@ -0,0 +1,1446 @@ +//! The structure integrating the controllers related to currently opened project with the project +//! view. +// TODO[ao] this module should be completely reworked when doing the +// https://github.com/enso-org/ide/issues/597 +// There should be a wrapper for each view which "fences" the input : emitting events in this +// wrapper should not notify the outputs. + +use crate::prelude::*; + +use crate::controller::graph::Connections; +use crate::controller::graph::NodeTrees; +use crate::controller::searcher::action::MatchInfo; +use crate::controller::searcher::Actions; +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::suggestion_database; + +use analytics; +use bimap::BiMap; +use enso_data::text::TextChange; +use enso_frp as frp; +use ensogl::display::traits::*; +use ensogl_gui_components::list_view; +use enso_protocol::language_server::ExpressionUpdatePayload; +use ide_view::graph_editor; +use ide_view::graph_editor::component::node; +use ide_view::graph_editor::component::visualization; +use ide_view::graph_editor::EdgeEndpoint; +use ide_view::graph_editor::GraphEditor; +use ide_view::graph_editor::SharedHashMap; +use utils::iter::split_by_predicate; + + + +// ============== +// === Errors === +// ============== + +/// Error returned by various function inside GraphIntegration, when our mappings from controller +/// items (node or connections) to displayed items are missing some information. +#[derive(Copy,Clone,Debug,Fail)] +enum MissingMappingFor { + #[fail(display="Displayed node {:?} is not bound to any controller node.",_0)] + DisplayedNode(graph_editor::NodeId), + #[fail(display="Controller node {:?} is not bound to any displayed node.",_0)] + 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. +#[derive(Copy,Clone,Debug,Fail)] +#[fail(display="Discrepancy in a GraphEditor component")] +struct GraphEditorInconsistency; + +#[derive(Copy,Clone,Debug,Fail)] +#[fail(display="No visualization associated with view node {} found.", _0)] +struct NoSuchVisualization(graph_editor::NodeId); + +#[derive(Copy,Clone,Debug,Fail)] +#[fail(display="Graph node {} already has visualization attached.", _0)] +struct VisualizationAlreadyAttached(graph_editor::NodeId); + +#[derive(Copy,Clone,Debug,Fail)] +#[fail(display="The Graph Integration hsd no SearcherController.")] +struct MissingSearcherController; + + + +// ==================== +// === FencedAction === +// ==================== + +/// An utility to FRP network. It is wrapped closure in a set of FRP nodes. The closure is called +/// on `trigger`, and `is_running` contains information if we are still inside closure call. It +/// allows us to block some execution path to avoid infinite loops. +/// +/// ### Example +/// +/// Here we want to do some updates when node was added to graph, but not during set up. +/// ```rust,compile_fail +/// frp::new_network! { network +/// let set_up = FencedAction::fence(&network, |()| { +/// frp.add_node.emit(()); +/// // other things. +/// }); +/// def _update = frp.node_added.map2(&set_up.is_running, |id,is_set_up| { +/// if !is_set_up { +/// update_something(id) +/// } +/// }); +/// } +/// // This will run the set up closure, but without calling update_something. +/// set_up.trigger.emit(()); +/// ``` +#[derive(CloneRef)] +struct FencedAction { + trigger : frp::Source, + is_running : frp::Stream, +} + +impl FencedAction { + /// Wrap the `action` in `FencedAction`. + fn fence(network:&frp::Network, action:impl Fn(&Parameter) + 'static) -> Self { + frp::extend! { network + trigger <- source::(); + triggered <- trigger.constant(()); + switch <- any(...); + switch <+ triggered; + performed <- trigger.map(move |param| action(param)); + switch <+ performed; + is_running <- switch.toggle(); + } + Self {trigger,is_running} + } +} + + + +// ============================== +// === GraphEditorIntegration === +// ============================== + +/// The identifier base that will be used to name the methods introduced by "collapse nodes" +/// refactoring. Names are typically generated by taking base and appending subsequent integers, +/// until the generated name does not collide with any known identifier. +const COLLAPSED_FUNCTION_NAME:&str = "func"; + +/// The default X position of the node when user did not set any position of node - possibly when +/// node was added by editing text. +const DEFAULT_NODE_X_POSITION : f32 = -100.0; +/// The default Y position of the node when user did not set any position of node - possibly when +/// node was added by editing text. +const DEFAULT_NODE_Y_POSITION : f32 = 200.0; + +/// Default node position -- acts as a starting points for laying out nodes with no position defined +/// in the metadata. +pub fn default_node_position() -> Vector2 { + Vector2::new(DEFAULT_NODE_X_POSITION,DEFAULT_NODE_Y_POSITION) +} + +/// A structure which handles integration between controller and graph_editor EnsoGl control. +/// All changes made by user in view are reflected in controller, and all controller notifications +/// update view accordingly. +//TODO[ao] soon we should rearrange modules and crates to avoid such long names. +#[allow(missing_docs)] +#[derive(Clone,CloneRef,Debug)] +pub struct Integration { + model : Rc, + network : frp::Network, +} + +impl Integration { + /// Get GraphEditor. + pub fn graph_editor(&self) -> GraphEditor { + self.model.view.graph().clone_ref() + } + + /// Get the controller associated with this graph editor. + pub fn graph_controller(&self) -> &controller::ExecutedGraph { + &self.model.graph + } +} + +#[derive(Debug)] +struct Model { + logger : Logger, + view : ide_view::project::View, + graph : controller::ExecutedGraph, + text : controller::Text, + ide : controller::Ide, + searcher : RefCell>, + project : model::Project, + node_views : RefCell>, + node_view_by_expression : RefCell>, + expression_views : RefCell>, + expression_types : SharedHashMap>, + connection_views : RefCell>, + code_view : CloneRefCell, + visualizations : SharedHashMap, + error_visualizations : SharedHashMap, +} + + +// === Construction And Setup === + +impl Integration { + /// Constructor. It creates GraphEditor and integrates it with given controller handle. + pub fn new + ( view : ide_view::project::View + , graph : controller::ExecutedGraph + , text : controller::Text + , ide : controller::Ide + , project : model::Project + ) -> Self { + let logger = Logger::new("ViewIntegration"); + let model = Model::new(logger,view,graph,text,ide,project); + 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; + let project_frp = &model.view.frp; + frp::new_network! {network + let invalidate = FencedAction::fence(&network,f!([model](()) { + let result = model.refresh_graph_view(); + if let Err(err) = result { + error!(model.logger,"Error while invalidating graph: {err}"); + } + })); + } + + + // === Breadcrumb Selection === + + let breadcrumbs = &model.view.graph().model.breadcrumbs; + frp::extend! {network + eval_ breadcrumbs.output.breadcrumb_pop(model.node_exited_in_ui(&()).ok()); + eval breadcrumbs.output.breadcrumb_push((local_call) { + model.expression_entered_in_ui(&local_call.as_ref().map(|local_call| { + let definition = (**local_call.definition).clone(); + let call = local_call.call; + LocalCall{call,definition} + })).ok() + }); + } + + + // === Project Renaming === + + let breadcrumbs = &model.view.graph().model.breadcrumbs; + frp::extend! {network + eval breadcrumbs.output.project_name((name) { + model.rename_project(name); + }); + } + + + // === Setting Visualization Preprocessor === + + frp::extend! { network + eval editor_outs.visualization_preprocessor_changed ([model]((node_id,preprocessor)) { + if let Err(err) = model.visualization_preprocessor_changed(*node_id,preprocessor) { + error!(model.logger, "Error when handling request for setting new \ + visualization's preprocessor code: {err}"); + } + }); + } + + + // === Visualization Reload === + + frp::extend! { network + eval editor_outs.visualization_registry_reload_requested ([model](()) { + model.view.graph().reset_visualization_registry(); + model.load_visualizations(); + }); + } + + + // === UI Actions === + + let inv = &invalidate.trigger; + let node_editing_in_ui = Model::node_editing_in_ui(Rc::downgrade(&model)); + let code_changed = Self::ui_action(&model,Model::code_changed_in_ui ,inv); + let node_removed = Self::ui_action(&model,Model::node_removed_in_ui ,inv); + let nodes_collapsed = Self::ui_action(&model,Model::nodes_collapsed_in_ui ,inv); + let node_entered = Self::ui_action(&model,Model::node_entered_in_ui ,inv); + let node_exited = Self::ui_action(&model,Model::node_exited_in_ui ,inv); + let connection_created = Self::ui_action(&model,Model::connection_created_in_ui ,inv); + let connection_removed = Self::ui_action(&model,Model::connection_removed_in_ui ,inv); + let node_moved = Self::ui_action(&model,Model::node_moved_in_ui ,inv); + let node_editing = Self::ui_action(&model,node_editing_in_ui ,inv); + let node_expression_set = Self::ui_action(&model,Model::node_expression_set_in_ui ,inv); + let used_as_suggestion = Self::ui_action(&model,Model::used_as_suggestion_in_ui ,inv); + let node_editing_committed = Self::ui_action(&model,Model::node_editing_committed_in_ui,inv); + let visualization_enabled = Self::ui_action(&model,Model::visualization_enabled_in_ui ,inv); + let visualization_disabled = Self::ui_action(&model,Model::visualization_disabled_in_ui,inv); + frp::extend! {network + eval code_editor.content ((content) model.code_view.set(content.clone_ref())); + + // Notifications from graph controller + let handle_graph_notification = FencedAction::fence(&network, + f!((notification:&Option) + model.handle_graph_notification(notification); + )); + + // Notifications from graph controller + let handle_text_notification = FencedAction::fence(&network, + f!((notification:&Option) + model.handle_text_notification(*notification); + )); + + // Changes in Graph Editor + is_handling_notification <- handle_graph_notification.is_running + || handle_text_notification.is_running; + is_hold <- is_handling_notification || invalidate.is_running; + on_connection_removed <- editor_outs.on_edge_endpoint_unset._0(); + _action <- code_editor.changed .map2(&is_hold,code_changed); + _action <- editor_outs.node_removed .map2(&is_hold,node_removed); + _action <- editor_outs.nodes_collapsed .map2(&is_hold,nodes_collapsed); + _action <- editor_outs.node_entered .map2(&is_hold,node_entered); + _action <- editor_outs.node_exited .map2(&is_hold,node_exited); + _action <- editor_outs.on_edge_endpoints_set .map2(&is_hold,connection_created); + _action <- editor_outs.visualization_enabled .map2(&is_hold,visualization_enabled); + _action <- editor_outs.visualization_disabled .map2(&is_hold,visualization_disabled); + _action <- on_connection_removed .map2(&is_hold,connection_removed); + _action <- editor_outs.node_position_set_batched.map2(&is_hold,node_moved); + _action <- editor_outs.node_being_edited .map2(&is_hold,node_editing); + _action <- editor_outs.node_expression_set .map2(&is_hold,node_expression_set); + _action <- searcher_frp.used_as_suggestion .map2(&is_hold,used_as_suggestion); + _action <- project_frp.editing_committed .map2(&is_hold,node_editing_committed); + + eval_ project_frp.editing_committed (invalidate.trigger.emit(())); + eval_ project_frp.editing_aborted (invalidate.trigger.emit(())); + eval_ project_frp.save_module (model.module_saved_in_ui()); + } + + frp::extend! { network + eval_ editor_outs.node_editing_started([]analytics::remote_log_event("graph_editor::node_editing_started")); + eval_ editor_outs.node_editing_finished([]analytics::remote_log_event("graph_editor::node_editing_finished")); + eval_ editor_outs.node_added([]analytics::remote_log_event("graph_editor::node_added")); + eval_ editor_outs.node_removed([]analytics::remote_log_event("graph_editor::node_removed")); + eval_ editor_outs.nodes_collapsed([]analytics::remote_log_event("graph_editor::nodes_collapsed")); + eval_ editor_outs.node_entered([]analytics::remote_log_event("graph_editor::node_enter_request")); + eval_ editor_outs.node_exited([]analytics::remote_log_event("graph_editor::node_exit_request")); + eval_ editor_outs.on_edge_endpoints_set([]analytics::remote_log_event("graph_editor::edge_endpoints_set")); + eval_ editor_outs.visualization_enabled([]analytics::remote_log_event("graph_editor::visualization_enabled")); + eval_ editor_outs.visualization_disabled([]analytics::remote_log_event("graph_editor::visualization_disabled")); + eval_ on_connection_removed([]analytics::remote_log_event("graph_editor::connection_removed")); + eval_ searcher_frp.used_as_suggestion([]analytics::remote_log_event("searcher::used_as_suggestion")); + eval_ project_frp.editing_committed([]analytics::remote_log_event("project::editing_committed")); + } + + + let ret = Self {model,network}; + ret.connect_frp_to_graph_controller_notifications(handle_graph_notification.trigger); + ret.connect_frp_text_controller_notifications(handle_text_notification.trigger); + ret.setup_handling_project_notifications(); + ret + } + + fn spawn_sync_stream_handler(&self, stream:Stream, handler:Function) + where Stream : StreamExt + Unpin + 'static, + Function : Fn(Stream::Item,Rc) + 'static { + let model = Rc::downgrade(&self.model); + executor::global::spawn_stream_handler(model,stream,move |item,model| { + handler(item,model); + futures::future::ready(()) + }) + } + + fn setup_handling_project_notifications(&self) { + let stream = self.model.project.subscribe(); + let logger = self.model.logger.clone_ref(); + let status_bar = self.model.view.status_bar().clone_ref(); + self.spawn_sync_stream_handler(stream, move |notification,_| { + info!(logger,"Processing notification {notification:?}"); + let message = match notification { + model::project::Notification::ConnectionLost(_) => + crate::BACKEND_DISCONNECTED_MESSAGE, + }; + let message = ide_view::status_bar::event::Label::from(message); + status_bar.add_event(message); + }) + } + + fn connect_frp_to_graph_controller_notifications + (&self, frp_endpoint : frp::Source>) { + let stream = self.model.graph.subscribe(); + let logger = self.model.logger.clone_ref(); + self.spawn_sync_stream_handler(stream, move |notification,_model| { + info!(logger,"Processing notification {notification:?}"); + frp_endpoint.emit(&Some(notification)); + }) + } + + fn connect_frp_text_controller_notifications + (&self, frp_endpoint : frp::Source>) { + let stream = self.model.text.subscribe(); + let logger = self.model.logger.clone_ref(); + self.spawn_sync_stream_handler(stream, move |notification,_model| { + info!(logger,"Processing notification {notification:?}"); + frp_endpoint.emit(&Some(notification)); + }); + } + + /// Convert a function being a method of GraphEditorIntegratedWithControllerModel to a closure + /// suitable for connecting to GraphEditor frp network. Returned lambda takes `Parameter` and a + /// bool, which indicates if this action is currently on hold (e.g. due to performing + /// invalidation). + fn ui_action + ( model : &Rc + , action : Action + , invalidate : &frp::Source<()> + ) -> impl Fn(&Parameter,&bool) + where Action : Fn(&Model,&Parameter) + -> FallibleResult + 'static { + f!([model,invalidate] (parameter,is_hold) { + if !*is_hold { + let result = action(&*model,parameter); + if let Err(err) = result { + error!(model.logger,"Error while performing UI action on controllers: {err}"); + info!(model.logger,"Invalidating displayed graph"); + invalidate.emit(()); + } + } + }) + } +} + +impl Model { + fn new + ( logger : Logger + , view : ide_view::project::View + , graph : controller::ExecutedGraph + , text : controller::Text + , ide : controller::Ide + , project : model::Project) -> 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 this = Model + {logger,view,graph,text,ide,searcher,project,node_views,node_view_by_expression + ,expression_views,expression_types,connection_views,code_view,visualizations + ,error_visualizations}; + + this.view.graph().frp.remove_all_nodes(); + this.view.status_bar().clear_all(); + this.init_project_name(); + this.load_visualizations(); + if let Err(err) = this.refresh_graph_view() { + error!(this.logger,"Error while initializing graph editor: {err}."); + } + if let Err(err) = this.refresh_code_editor() { + error!(this.logger,"Error while initializing code editor: {err}."); + } + this + } + + 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 { + let identifiers = controller.list_visualizations().await; + let identifiers = identifiers.unwrap_or_default(); + for identifier in identifiers { + match controller.load_visualization(&identifier).await { + Ok(visualization) => { + graph_editor.frp.register_visualization.emit(Some(visualization)); + } + Err(err) => { + error!(logger, "Error while loading visualization {identifier}: {err:?}"); + } + } + } + info!(logger, "Visualizations Initialized."); + }); + } +} + + +// === Project renaming === + +impl Model { + fn init_project_name(&self) { + let project_name = self.project.name().to_string(); + self.view.graph().model.breadcrumbs.input.project_name.emit(project_name); + } + + fn rename_project(&self, name:impl Str) { + if self.project.name() != name.as_ref() { + let project = self.project.clone_ref(); + let breadcrumbs = self.view.graph().model.breadcrumbs.clone_ref(); + let logger = self.logger.clone_ref(); + let name = name.into(); + executor::global::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(()); + } + }); + } + } +} + + +// === Updating Graph View === + +impl Model { + /// Refresh displayed code to be up to date with module state. + pub fn refresh_code_editor(&self) -> FallibleResult { + let current_code = self.code_view.get().to_string(); + let new_code = self.graph.graph().module.ast().repr(); + if new_code != current_code { + self.code_view.set(new_code.as_str().into()); + self.view.code_editor().text_area().set_content(new_code); + } + Ok(()) + } + + /// Reload whole displayed content to be up to date with module state. + pub fn refresh_graph_view(&self) -> FallibleResult { + info!(self.logger, "Refreshing the graph view."); + let connections_info = self.graph.connections()?; + self.refresh_node_views(&connections_info, true)?; + self.refresh_connection_views(connections_info.connections)?; + Ok(()) + } + + fn refresh_node_views + (&self, connections_info:&Connections, update_position:bool) -> FallibleResult { + let base_default_position = default_node_position(); + let mut trees = connections_info.trees.clone(); + let nodes = self.graph.graph().nodes()?; + let (without_pos,with_pos) = split_by_predicate(&nodes, |n| n.has_position()); + let bottommost_node_pos = with_pos.iter() + .filter_map(|node| node.metadata.as_ref()?.position) + .min_by(model::module::Position::ord_by_y) + .map(|pos| pos.vector) + .unwrap_or(base_default_position); + + let default_positions : HashMap<_,_> = (1..).zip(&without_pos).map(|(i,node)| { + let dy = i as f32 * self.view.default_gap_between_nodes.value(); + let pos = Vector2::new(bottommost_node_pos.x, bottommost_node_pos.y - dy); + (node.info.id(), pos) + }).collect(); + + let ids = nodes.iter().map(|node| node.info.id()).collect(); + self.retain_node_views(&ids); + for node_info in &nodes { + let id = node_info.info.id(); + let node_trees = trees.remove(&id).unwrap_or_else(default); + let default_pos = default_positions.get(&id).unwrap_or(&base_default_position); + let displayed = self.node_views.borrow_mut().get_by_left(&id).cloned(); + match displayed { + Some(displayed) => { + if update_position { + self.refresh_node_position(displayed,node_info) + }; + self.refresh_node_expression(displayed,node_info,node_trees); + }, + None => self.create_node_view(node_info,node_trees,*default_pos), + } + } + Ok(()) + } + + /// Refresh the expressions (e.g., types, ports) for all nodes. + fn refresh_graph_expressions(&self) -> FallibleResult { + info!(self.logger, "Refreshing the graph expressions."); + let connections = self.graph.connections()?; + self.refresh_node_views(&connections, false)?; + self.refresh_connection_views(connections.connections) + } + + /// Retain only given nodes in displayed graph. + fn retain_node_views(&self, ids:&HashSet) { + let to_remove = { + let borrowed = self.node_views.borrow(); + let filtered = borrowed.iter().filter(|(id,_)| !ids.contains(id)); + filtered.map(|(k,v)| (*k,*v)).collect_vec() + }; + for (id,displayed_id) in to_remove { + self.view.graph().frp.input .remove_node.emit(&displayed_id); + self.node_views.borrow_mut().remove_by_left(&id); + } + } + + fn create_node_view + (&self, info:&controller::graph::Node, trees:NodeTrees, default_pos:Vector2) { + let id = info.info.id(); + let displayed_id = self.view.graph().add_node(); + self.refresh_node_view(displayed_id, info, trees); + if info.metadata.as_ref().and_then(|md| md.position).is_none() { + self.view.graph().frp.input.set_node_position.emit(&(displayed_id, default_pos)); + // If position wasn't present in metadata, we initialize it. + if let Err(err) = self.set_node_metadata_position(id,default_pos) { + debug!(self.logger, "Failed to set default position information IDs: {id:?} because \ + of error {err:?}"); + } + } + self.node_views.borrow_mut().insert(id, displayed_id); + } + + fn deserialize_visualization_data + (data:VisualizationUpdateData) -> FallibleResult { + let binary = data.as_ref(); + let as_text = std::str::from_utf8(binary)?; + let as_json : serde_json::Value = serde_json::from_str(as_text)?; + Ok(visualization::Data::from(as_json)) + } + + fn refresh_node_view + (&self, id:graph_editor::NodeId, node:&controller::graph::Node, trees:NodeTrees) { + self.refresh_node_position(id,node); + self.refresh_node_expression(id, node, trees) + } + + /// Update the position of the node based on its current metadata. + fn refresh_node_position(&self, id:graph_editor::NodeId, node:&controller::graph::Node) { + let position = node.metadata.as_ref().and_then(|md| md.position); + if let Some(position) = position { + self.view.graph().frp.input.set_node_position.emit(&(id, position.vector)); + } + } + + /// Update the expression of the node and all related properties e.g., types, ports). + fn refresh_node_expression(&self, id:graph_editor::NodeId, node:&controller::graph::Node, trees:NodeTrees) { + let code_and_trees = graph_editor::component::node::Expression { + pattern : node.info.pattern().map(|t|t.repr()), + code : node.info.expression().repr(), + whole_expression_id : node.info.expression().id , + input_span_tree : trees.inputs, + output_span_tree : trees.outputs.unwrap_or_else(default) + }; + let expression_changed = + !self.expression_views.borrow().get(&id).contains(&&code_and_trees); + if expression_changed { + for sub_expression in node.info.ast().iter_recursive() { + if let Some(expr_id) = sub_expression.id { + self.node_view_by_expression.borrow_mut().insert(expr_id,id); + } + } + self.view.graph().frp.input.set_node_expression.emit(&(id,code_and_trees.clone())); + self.expression_views.borrow_mut().insert(id,code_and_trees); + } + + // Set initially available type information on ports (identifiable expression's sub-parts). + for expression_part in node.info.expression().iter_recursive() { + if let Some(id) = expression_part.id { + self.refresh_computed_info(id,expression_changed); + } + } + } + + /// Like `refresh_computed_info` but for multiple expressions. + fn refresh_computed_infos(&self, expressions_to_refresh:&[ExpressionId]) -> FallibleResult { + debug!(self.logger, "Refreshing type information for IDs: {expressions_to_refresh:?}."); + for id in expressions_to_refresh { + self.refresh_computed_info(*id,false) + } + Ok(()) + } + + /// Look up the computed information for a given expression and pass the information to the + /// graph editor view. + /// + /// The computed value information includes the expression type and the target method pointer. + fn refresh_computed_info(&self, id:ExpressionId, force_type_info_refresh:bool) { + let info = self.lookup_computed_info(&id); + let info = info.as_ref(); + let typename = info.and_then(|info| info.typename.clone().map(graph_editor::Type)); + if let Some(node_id) = self.node_view_by_expression.borrow().get(&id).cloned() { + self.set_type(node_id,id,typename, force_type_info_refresh); + let method_pointer = info.and_then(|info| { + info.method_call.and_then(|entry_id| { + let opt_method = self.project.suggestion_db().lookup_method_ptr(entry_id).ok(); + opt_method.map(|method| graph_editor::MethodPointer(Rc::new(method))) + }) + }); + self.set_method_pointer(id,method_pointer); + if self.node_views.borrow().get_by_left(&id).contains(&&node_id) { + let set_error_result = self.set_error(node_id,info.map(|info| &info.payload)); + if let Err(error) = set_error_result { + error!(self.logger, "Error when setting error on node: {error}"); + } + } + } + } + + /// Set given type (or lack of such) on the given sub-expression. + fn set_type + ( &self + , node_id : graph_editor::NodeId + , id : ExpressionId + , typename : Option + , force_refresh : bool + ) { + // We suppress spurious type information updates here, as they were causing performance + // issues. See: https://github.com/enso-org/ide/issues/952 + let previous_type_opt = self.expression_types.insert(id,typename.clone()); + if force_refresh || previous_type_opt.as_ref() != Some(&typename) { + let event = (node_id,id,typename); + self.view.graph().frp.input.set_expression_usage_type.emit(&event); + } + } + + /// Set given method pointer (or lack of such) on the given sub-expression. + fn set_method_pointer(&self, id:ExpressionId, method:Option) { + let event = (id,method); + self.view.graph().frp.input.set_method_pointer.emit(&event); + } + + /// 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()); + if error.is_some() && !self.error_visualizations.contains_key(&node_id) { + use graph_editor::builtin::visualization::native::error; + let endpoint = self.view.graph().frp.set_error_visualization_data.clone_ref(); + let metadata = error::metadata(); + let vis_map = self.error_visualizations.clone_ref(); + self.attach_visualization(node_id,&metadata,endpoint,vis_map)?; + } else if error.is_none() && self.error_visualizations.contains_key(&node_id) { + self.detach_visualization(node_id,self.error_visualizations.clone_ref())?; + } + Ok(()) + } + + fn convert_payload_to_error_view + (&self, payload:Option<&ExpressionUpdatePayload>, node_id:graph_editor::NodeId) + -> Option { + use ExpressionUpdatePayload::*; + use node::error::Kind; + let (kind,message,trace) = match payload { + None | + Some(Value ) => None, + Some(DataflowError { trace }) => Some((Kind::Dataflow, None ,trace)), + Some(Panic { message,trace }) => Some((Kind::Panic , Some(message),trace)), + }?; + let propagated = if kind == Kind::Panic { + let root_cause = self.get_node_causing_error_on_current_graph(&trace); + !root_cause.contains(&node_id) + } else { + // TODO[ao]: traces are not available for Dataflow errors. + false + }; + + let kind = Immutable(kind); + let message = Rc::new(message.cloned()); + let propagated = Immutable(propagated); + Some(node::error::Error {kind,message,propagated}) + } + + /// Get the node being a main cause of some error from the current nodes on the scene. Returns + /// [`None`] if the error is not present on the scene at all. + fn get_node_causing_error_on_current_graph + (&self, trace:&[ExpressionId]) -> Option { + let node_view_by_expression = self.node_view_by_expression.borrow(); + trace.iter().find_map(|expr_id| node_view_by_expression.get(&expr_id).copied()) + } + + /// Set the position in the node's metadata. + fn set_node_metadata_position(&self, node_id:ast::Id,pos:Vector2) -> FallibleResult { + self.graph.graph().module.with_node_metadata(node_id, Box::new(|md| { + md.position = Some(model::module::Position::new(pos.x,pos.y)); + })) + } + + fn refresh_connection_views + (&self, connections:Vec) -> FallibleResult { + self.retain_connection_views(&connections); + for con in connections { + if !self.connection_views.borrow().contains_left(&con) { + let targets = self.edge_targets_from_controller_connection(con.clone())?; + self.view.graph().frp.input.connect_nodes.emit(&targets); + let edge_id = self.view.graph().frp.output.on_edge_add.value(); + self.connection_views.borrow_mut().insert(con, edge_id); + } + } + Ok(()) + } + + fn edge_targets_from_controller_connection + (&self, connection:controller::graph::Connection) -> FallibleResult<(EdgeEndpoint,EdgeEndpoint)> { + let src_node = self.get_displayed_node_id(connection.source.node)?; + let dst_node = self.get_displayed_node_id(connection.destination.node)?; + let src = EdgeEndpoint::new(src_node,connection.source.port); + let data = EdgeEndpoint::new(dst_node,connection.destination.port); + Ok((src,data)) + } + + /// Retain only given connections in displayed graph. + fn retain_connection_views(&self, connections:&[controller::graph::Connection]) { + let to_remove = { + let borrowed = self.connection_views.borrow(); + let filtered = borrowed.iter().filter(|(con,_)| !connections.contains(con)); + filtered.map(|(_,edge_id)| *edge_id).collect_vec() + }; + for edge_id in to_remove { + self.view.graph().frp.input.remove_edge.emit(&edge_id); + self.connection_views.borrow_mut().remove_by_right(&edge_id); + } + } +} + + +// === Handling Controller Notifications === + +impl Model { + /// Handle notification received from controller about the whole graph being invalidated. + pub fn on_graph_invalidated(&self) -> FallibleResult { + self.refresh_graph_view() + } + + /// Handle notification received from controller about the graph receiving new type information. + pub fn on_graph_expression_update(&self) -> FallibleResult { + self.refresh_graph_expressions() + } + + /// Handle notification received from controller about the whole graph being invalidated. + pub fn on_text_invalidated(&self) -> FallibleResult { + self.refresh_code_editor() + } + + /// Handle notification received from controller about values having been entered. + pub fn on_node_entered(&self, local_call:&LocalCall) -> FallibleResult { + analytics::remote_log_event("integration::node_entered"); + let definition = local_call.definition.clone().into(); + let call = local_call.call; + let local_call = graph_editor::LocalCall{call,definition}; + self.view.graph().frp.deselect_all_nodes.emit(&()); + self.view.graph().model.breadcrumbs.push_breadcrumb.emit(&Some(local_call)); + self.request_detaching_all_visualizations(); + self.refresh_graph_view() + } + + /// Handle notification received from controller about node having been exited. + 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.view.graph().model.breadcrumbs.pop_breadcrumb.emit(()); + let id = self.get_displayed_node_id(id)?; + self.view.graph().frp.select_node.emit(&id); + Ok(()) + } + + /// Handle notification received from controller about values having been computed. + pub fn on_values_computed(&self, expressions:&[ExpressionId]) -> FallibleResult { + 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) { + use controller::graph::executed::Notification; + use controller::graph::Notification::*; + + debug!(self.logger, "Received graph notification {notification:?}"); + let result = match notification { + Some(Notification::Graph(Invalidate)) => self.on_graph_invalidated(), + Some(Notification::Graph(PortsUpdate)) => self.on_graph_expression_update(), + Some(Notification::ComputedValueInfo(update)) => self.on_values_computed(update), + Some(Notification::SteppedOutOfNode(id)) => self.on_node_exited(*id), + Some(Notification::EnteredNode(local_call)) => self.on_node_entered(local_call), + None => { + warning!(self.logger,"Handling `None` notification is not implemented; \ + performing full invalidation"); + self.refresh_graph_view() + } + }; + if let Err(err) = result { + error!(self.logger,"Error while updating graph after receiving {notification:?} from \ + controller: {err}"); + } + } + + /// Handle notification received from Text Controller. + pub fn handle_text_notification(&self, notification:Option) { + use controller::text::Notification; + + debug!(self.logger, "Received text notification {notification:?}"); + let result = match notification { + Some(Notification::Invalidate) => self.on_text_invalidated(), + other => { + warning!(self.logger,"Handling notification {other:?} is not implemented; \ + performing full invalidation"); + self.refresh_code_editor() + } + }; + if let Err(err) = result { + error!(self.logger,"Error while updating graph after receiving {notification:?} from \ + controller: {err}"); + } + } + + pub fn handle_searcher_notification(&self, notification:controller::searcher::Notification) { + use controller::searcher::Notification; + use controller::searcher::UserAction; + debug!(self.logger, "Received searcher notification {notification:?}"); + match notification { + Notification::NewActionList => with(self.searcher.borrow(), |searcher| { + if let Some(searcher) = &*searcher { + match searcher.actions() { + Actions::Loading => self.view.searcher().clear_actions(), + Actions::Loaded {list:actions} => { + let list_is_empty = actions.matching_count() == 0; + let user_action = searcher.current_user_action(); + let intended_function = searcher.intended_function_suggestion(); + let provider = DataProviderForView + { actions,user_action,intended_function}; + self.view.searcher().set_actions(Rc::new(provider)); + + // Usually we want to select first entry and display docs for it + // But not when user finished typing function or argument. + let starting_typing = user_action == UserAction::StartingTypingArgument; + if !starting_typing && !list_is_empty { + self.view.searcher().select_action(0); + } + } + Actions::Error(err) => { + error!(self.logger, "Error while obtaining list from searcher: {err}"); + self.view.searcher().clear_actions(); + }, + }; + } + }) + } + } +} + + +// === Passing UI Actions To Controllers === + +// These functions are called with FRP event values as arguments. The FRP values are always provided +// by reference, including "trivially-copy" types and Vecs, To keep code cleaner we take +// all parameters by reference. +#[allow(clippy::trivially_copy_pass_by_ref)] +#[allow(clippy::ptr_arg)] +impl Model { + fn node_removed_in_ui(&self, node:&graph_editor::NodeId) -> FallibleResult { + debug!(self.logger, "Removing node."); + let id = self.get_controller_node_id(*node)?; + self.node_views.borrow_mut().remove_by_left(&id); + self.graph.graph().remove_node(id)?; + Ok(()) + } + + fn node_moved_in_ui + (&self, (displayed_id,pos):&(graph_editor::NodeId,Vector2)) -> FallibleResult { + debug!(self.logger, "Moving node."); + if let Ok(id) = self.get_controller_node_id(*displayed_id) { + self.set_node_metadata_position(id,*pos)?; + } + Ok(()) + } + + fn nodes_collapsed_in_ui + (&self, (collapsed,_new_node_view_id):&(Vec,graph_editor::NodeId)) + -> FallibleResult { + debug!(self.logger, "Collapsing node."); + let ids = self.get_controller_node_ids(collapsed)?; + let _new_node_id = self.graph.graph().collapse(ids,COLLAPSED_FUNCTION_NAME)?; + // TODO [mwu] https://github.com/enso-org/ide/issues/760 + // As part of this issue, storing relation between new node's controller and view ids will + // be necessary. + Ok(()) + } + + fn node_expression_set_in_ui + (&self, (displayed_id,expression):&(graph_editor::NodeId,String)) -> FallibleResult { + debug!(self.logger, "Setting node expression."); + let searcher = self.searcher.borrow(); + let code_and_trees = graph_editor::component::node::Expression::new_plain(expression); + self.expression_views.borrow_mut().insert(*displayed_id,code_and_trees); + if let Some(searcher) = searcher.as_ref() { + searcher.set_input(expression.clone())?; + } + Ok(()) + } + + fn node_editing_in_ui(weak_self:Weak) + -> impl Fn(&Self,&Option) -> FallibleResult { + move |this,displayed_id| { + if let Some(displayed_id) = displayed_id { + debug!(this.logger, "Starting node editing."); + let id = this.get_controller_node_id(*displayed_id); + let mode = match id { + Ok(node_id) => controller::searcher::Mode::EditNode {node_id}, + Err(MissingMappingFor::DisplayedNode(id)) => { + let node_view = this.view.graph().model.nodes.get_cloned_ref(&id); + let position = node_view.map(|node| node.position().xy()); + let position = position.map(|vector| model::module::Position{vector}); + controller::searcher::Mode::NewNode {position} + }, + Err(other) => return Err(other.into()), + }; + let selected_nodes = this.view.graph().model.selected_nodes().iter().filter_map(|id| { + this.get_controller_node_id(*id).ok() + }).collect_vec(); + let controller = this.graph.clone_ref(); + let ide = this.ide.clone_ref(); + let searcher = controller::Searcher::new_from_graph_controller + (&this.logger,ide,&this.project,controller,mode,selected_nodes)?; + executor::global::spawn(searcher.subscribe().for_each(f!([weak_self](notification) { + if let Some(this) = weak_self.upgrade() { + this.handle_searcher_notification(notification); + } + futures::future::ready(()) + }))); + *this.searcher.borrow_mut() = Some(searcher); + } else { + debug!(this.logger, "Finishing node editing."); + } + Ok(()) + } + } + + fn used_as_suggestion_in_ui + (&self, entry:&Option) -> FallibleResult { + debug!(self.logger, "Using as suggestion."); + if let Some(entry) = entry { + let graph_frp = &self.view.graph().frp; + let error = || MissingSearcherController; + let searcher = self.searcher.borrow().clone().ok_or_else(error)?; + let error = || GraphEditorInconsistency; + let edited_node = graph_frp.output.node_being_edited.value().ok_or_else(error)?; + let code = searcher.use_as_suggestion(*entry)?; + let code_and_trees = node::Expression::new_plain(code); + graph_frp.input.set_node_expression.emit(&(edited_node,code_and_trees)); + } + Ok(()) + } + + fn node_editing_committed_in_ui + (&self, (displayed_id,entry_id):&(graph_editor::NodeId,Option)) + -> FallibleResult { + use crate::controller::searcher::action::Action::Example; + debug!(self.logger, "Committing node expression."); + let error = || MissingSearcherController; + let searcher = self.searcher.replace(None).ok_or_else(error)?; + let entry = searcher.actions().list().zip(*entry_id).and_then(|(l,i)| l.get_cloned(i)); + let is_example = entry.map_or(false, |e| matches!(e.action,Example(_))); + let result = if let Some(id) = entry_id { + searcher.execute_action_by_index(*id) + } else { + searcher.commit_node().map(Some) + }; + match result { + Ok(Some(node_id)) => { + self.node_views.borrow_mut().insert(node_id,*displayed_id); + if is_example { + self.view.graph().frp.enable_visualization(displayed_id); + } + Ok(()) + } + Ok(None) => { + self.view.graph().frp.remove_node(displayed_id); + Ok(()) + }, + Err(err) => { + self.view.graph().frp.remove_node.emit(displayed_id); + Err(err) + } + } + } + + fn connection_created_in_ui(&self, edge_id:&graph_editor::EdgeId) -> FallibleResult { + debug!(self.logger, "Creating connection."); + let displayed = self.view.graph().model.edges.get_cloned(&edge_id).ok_or(GraphEditorInconsistency)?; + let con = self.controller_connection_from_displayed(&displayed)?; + let inserting = self.connection_views.borrow_mut().insert(con.clone(), *edge_id); + if inserting.did_overwrite() { + warning!(self.logger,"Created connection {edge_id} overwrite some old mappings in \ + GraphEditorIntegration.") + } + self.graph.connect(&con)?; + Ok(()) + } + + fn connection_removed_in_ui(&self, edge_id:&graph_editor::EdgeId) -> FallibleResult { + debug!(self.logger, "Removing connection."); + let connection = self.get_controller_connection(*edge_id)?; + self.connection_views.borrow_mut().remove_by_left(&connection); + self.graph.disconnect(&connection)?; + Ok(()) + } + + fn visualization_enabled_in_ui + (&self, (node_id,vis_metadata):&(graph_editor::NodeId,visualization::Metadata)) + -> FallibleResult { + 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(()) + } + + fn visualization_disabled_in_ui(&self, node_id:&graph_editor::NodeId) -> FallibleResult { + self.detach_visualization(*node_id,self.visualizations.clone_ref()) + } + + fn expression_entered_in_ui + (&self, local_call:&Option) -> FallibleResult { + if let Some(local_call) = local_call { + let local_call = local_call.clone(); + let controller = self.graph.clone_ref(); + let logger = self.logger.clone_ref(); + let enter_action = async move { + info!(logger,"Entering node."); + if let Err(e) = controller.enter_method_pointer(&local_call).await { + error!(logger,"Entering node failed: {e}."); + + let event = "integration::entering_node_failed"; + let field = "error"; + let data = analytics::AnonymousData(|| e.to_string()); + analytics::remote_log_value(event,field,data) + } + }; + executor::global::spawn(enter_action); + } + Ok(()) + } + + fn node_entered_in_ui(&self, node_id:&graph_editor::NodeId) -> FallibleResult { + debug!(self.logger,"Requesting entering the node {node_id}."); + let call = self.get_controller_node_id(*node_id)?; + let method_pointer = self.graph.node_method_pointer(call)?; + let definition = (*method_pointer).clone(); + let local_call = LocalCall{call,definition}; + self.expression_entered_in_ui(&Some(local_call)) + } + + fn node_exited_in_ui(&self, _:&()) -> FallibleResult { + debug!(self.logger,"Requesting exiting the current node."); + let controller = self.graph.clone_ref(); + let logger = self.logger.clone_ref(); + let exit_node_action = async move { + info!(logger,"Exiting node."); + if let Err(e) = controller.exit_node().await { + debug!(logger, "Exiting node failed: {e}."); + + let event = "integration::exiting_node_failed"; + let field = "error"; + let data = analytics::AnonymousData(|| e.to_string()); + analytics::remote_log_value(event,field,data) + } + }; + executor::global::spawn(exit_node_action); + Ok(()) + } + + fn code_changed_in_ui(&self, changes:&Vec) -> FallibleResult { + for change in changes { + let range_start = data::text::Index::new(change.range.start.value as usize); + let range_end = data::text::Index::new(change.range.end.value as usize); + let converted = TextChange::replace(range_start..range_end,change.text.to_string()); + self.text.apply_text_change(converted)?; + } + Ok(()) + } + + fn module_saved_in_ui(&self) { + 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 { + if let Err(err) = controller.store_content(content).await { + error!(logger, "Error while saving file: {err:?}"); + } + }); + } + + fn resolve_visualization_context + (&self, context:&visualization::instance::ContextModule) + -> FallibleResult { + use visualization::instance::ContextModule::*; + match context { + ProjectMain => 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 + ) -> FallibleResult { + if let Some(visualization) = self.visualizations.get_copied(&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)?; + executor::global::spawn(async move { + let result = controller.set_visualization_preprocessor(visualization,code,module); + if let Err(err) = result.await { + error!(logger, "Error when setting visualization preprocessor: {err}"); + } + }); + Ok(()) + } else { + Err(MissingMappingFor::DisplayedVisualization(node_id).into()) + } + } +} + + +// === Utilities === + +impl Model { + fn get_controller_node_id + (&self, displayed_id:graph_editor::NodeId) -> Result { + let err = MissingMappingFor::DisplayedNode(displayed_id); + self.node_views.borrow().get_by_right(&displayed_id).cloned().ok_or(err) + } + + fn get_controller_node_ids + (&self, displayed_ids:impl IntoIterator>) + -> Result, MissingMappingFor> { + use std::borrow::Borrow; + displayed_ids.into_iter().map(|id| { + let id = id.borrow(); + self.get_controller_node_id(*id) + }).collect() + } + + fn get_displayed_node_id + (&self, node_id:ast::Id) -> Result { + let err = MissingMappingFor::ControllerNode(node_id); + self.node_views.borrow().get_by_left(&node_id).cloned().ok_or(err) + } + + fn get_controller_connection + (&self, displayed_id:graph_editor::EdgeId) + -> Result { + let err = MissingMappingFor::DisplayedConnection(displayed_id); + self.connection_views.borrow().get_by_right(&displayed_id).cloned().ok_or(err) + } + + fn controller_connection_from_displayed + (&self, connection:&graph_editor::Edge) -> FallibleResult { + let src = connection.source().ok_or(GraphEditorInconsistency {})?; + let dst = connection.target().ok_or(GraphEditorInconsistency {})?; + let src_node = self.get_controller_node_id(src.node_id)?; + let dst_node = self.get_controller_node_id(dst.node_id)?; + Ok(controller::graph::Connection { + source : controller::graph::Endpoint::new(src_node,&src.port), + destination : controller::graph::Endpoint::new(dst_node,&dst.port), + }) + } + + fn lookup_computed_info(&self, id:&ExpressionId) -> Option> { + let registry = self.graph.computed_value_info_registry(); + 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 : SharedHashMap + ) -> FallibleResult { + // Do nothing if there is already a visualization attached. + let err = || VisualizationAlreadyAttached(node_id); + (!visualizations_map.contains_key(&node_id)).ok_or_else(err)?; + + debug!(self.logger, "Attaching visualization on node {node_id}."); + let visualization = self.prepare_visualization(node_id,vis_metadata)?; + let id = visualization.id; + let update_handler = self.visualization_update_handler(receive_data_endpoint,node_id); + let logger = self.logger.clone_ref(); + let controller = self.graph.clone_ref(); + + // We cannot do this in the async block, as the user may decide to detach before server + // confirms that we actually have attached. + visualizations_map.insert(node_id,id); + + executor::global::spawn(async move { + if let Ok(stream) = controller.attach_visualization(visualization).await { + debug!(logger, "Successfully attached visualization {id} for node {node_id}."); + let updates_handler = stream.for_each(update_handler); + executor::global::spawn(updates_handler); + } else { + visualizations_map.remove(&node_id); + } + }); + Ok(id) + } + + /// Return an asynchronous event processor that routes visualization update to the given's + /// visualization respective FRP endpoint. + fn visualization_update_handler + ( &self + , endpoint : frp::Any<(graph_editor::NodeId,visualization::Data)> + , node_id : graph_editor::NodeId + ) -> impl FnMut(VisualizationUpdateData) -> futures::future::Ready<()> { + // TODO [mwu] + // For now only JSON visualizations are supported, so we can just assume JSON data in the + // binary package. + let logger = self.logger.clone_ref(); + move |update| { + match Self::deserialize_visualization_data(update) { + Ok (data) => endpoint.emit((node_id,data)), + Err(error) => + // TODO [mwu] + // We should consider having the visualization also accept error input. + error!(logger, "Failed to deserialize visualization update. {error}"), + } + futures::future::ready(()) + } + } + + /// Create a controller-compatible description of the visualization based on the input received + /// from the graph editor endpoints. + fn prepare_visualization + (&self, node_id:graph_editor::NodeId, metadata:&visualization::Metadata) + -> FallibleResult { + let module_designation = &metadata.preprocessor.module; + let visualisation_module = self.resolve_visualization_context(module_designation)?; + let id = VisualizationId::new_v4(); + let expression = metadata.preprocessor.code.to_string(); + let ast_id = self.get_controller_node_id(node_id)?; + Ok(Visualization{id,ast_id,expression,visualisation_module}) + } + + fn detach_visualization + ( &self + , node_id : graph_editor::NodeId + , visualizations_map : SharedHashMap + ) -> FallibleResult { + debug!(self.logger,"Node editor wants to detach visualization on {node_id}."); + let err = || NoSuchVisualization(node_id); + let id = visualizations_map.get_copied(&node_id).ok_or_else(err)?; + 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(()) + } +} + + + +// =========================== +// === DataProviderForView === +// =========================== + +#[derive(Clone,Debug)] +struct DataProviderForView { + actions : Rc, + user_action : controller::searcher::UserAction, + intended_function : Option, +} + +impl DataProviderForView { + fn doc_placeholder_for(suggestion:&controller::searcher::action::Suggestion) -> String { + let title = match suggestion.kind { + suggestion_database::entry::Kind::Atom => "Atom", + suggestion_database::entry::Kind::Function => "Function", + suggestion_database::entry::Kind::Local => "Local variable", + suggestion_database::entry::Kind::Method => "Method", + }; + let code = suggestion.code_to_insert(None,true).code; + format!("{} `{}`\n\nNo documentation available", title,code) + } +} + +impl list_view::entry::ModelProvider for DataProviderForView { + fn entry_count(&self) -> usize { + self.actions.matching_count() + } + + fn get(&self, id: usize) -> Option { + let action = self.actions.get_cloned(id)?; + if let MatchInfo::Matches {subsequence} = action.match_info { + let caption = action.action.to_string(); + let model = list_view::entry::Model::new(caption.clone()); + let mut char_iter = caption.char_indices().enumerate(); + let highlighted_iter = subsequence.indices.iter().filter_map(|idx| loop { + if let Some(char) = char_iter.next() { + let (char_idx,(byte_id,char)) = char; + if char_idx == *idx { + let start = ensogl_text::Bytes(byte_id as i32); + let end = ensogl_text::Bytes((byte_id + char.len_utf8()) as i32); + break Some(ensogl_text::Range::new(start,end)) + } + } else { + break None; + } + }); + let model = model.highlight(highlighted_iter); + Some(model) + } else { + None + } + } +} + +impl ide_view::searcher::DocumentationProvider for DataProviderForView { + fn get(&self) -> Option { + use controller::searcher::UserAction::*; + self.intended_function.as_ref().and_then(|function| match self.user_action { + StartingTypingArgument => function.documentation.clone(), + _ => None + }) + } + + fn get_for_entry(&self, id:usize) -> Option { + use controller::searcher::action::Action; + match self.actions.get_cloned(id)?.action { + Action::Suggestion(suggestion) => { + let doc = suggestion.documentation.clone(); + Some(doc.unwrap_or_else(|| Self::doc_placeholder_for(&suggestion))) + } + Action::Example(example) => Some(example.documentation.clone()), + Action::CreateNewProject => None, + } + } +} diff --git a/ide/src/rust/ide/src/model/project.rs b/ide/src/rust/ide/src/model/project.rs index d36943e53011..a14802bc3b65 100644 --- a/ide/src/rust/ide/src/model/project.rs +++ b/ide/src/rust/ide/src/model/project.rs @@ -77,7 +77,7 @@ pub trait API:Debug { /// /// This module is special, as it needs to be referred by the project name itself. fn main_module(&self) -> FallibleResult { - let main = std::iter::once(crate::ide::INITIAL_MODULE_NAME); + let main = std::iter::once(controller::project::INITIAL_MODULE_NAME); model::module::QualifiedName::from_segments(self.name(),main) // TODO [mwu] The code below likely should be preferred but does not work diff --git a/ide/src/rust/ide/src/model/project/synchronized.rs b/ide/src/rust/ide/src/model/project/synchronized.rs index 45c820aef74c..1ae515b1dfbf 100644 --- a/ide/src/rust/ide/src/model/project/synchronized.rs +++ b/ide/src/rust/ide/src/model/project/synchronized.rs @@ -8,6 +8,7 @@ use crate::model::module; use crate::model::SuggestionDatabase; use crate::model::traits::*; use crate::notification; +use crate::transport::web::WebSocket; use enso_protocol::binary; use enso_protocol::binary::message::VisualisationContext; @@ -15,6 +16,7 @@ use enso_protocol::language_server; use enso_protocol::language_server::CapabilityRegistration; use enso_protocol::language_server::MethodPointer; use enso_protocol::project_manager; +use enso_protocol::project_manager::MissingComponentAction; use flo_stream::Subscriber; use parser::Parser; @@ -98,40 +100,31 @@ impl ExecutionContextsRegistry { pub struct ProjectManagerUnavailable; - // === Data === -/// A structure containing the project's unique ID and name. +/// A structure containing the project's properties. +#[allow(missing_docs)] #[derive(Debug,Clone)] -pub struct Data { +pub struct Properties { /// ID of the project, as used by the Project Manager service. - pub id : Uuid, - name : RefCell, + pub id : Uuid, + pub name : CloneRefCell, + pub engine_version : semver::Version, } -impl Data { - /// Set project name. - pub fn set_name(&self, name:impl Str) { - *self.name.borrow_mut() = ImString::new(name); - } - /// Get project name. - pub fn name(&self) -> ImString { - self.name.borrow().clone_ref() - } -} +// == Model == /// Project Model. #[allow(missing_docs)] #[derive(Derivative)] #[derivative(Debug)] pub struct Project { - pub data : Rc, + pub properties : Rc, #[derivative(Debug = "ignore")] pub project_manager : Option>, pub language_server_rpc : Rc, pub language_server_bin : Rc, - pub engine_version : Rc, pub module_registry : Rc>, pub execution_contexts : Rc, pub visualization : controller::Visualization, @@ -158,22 +151,21 @@ impl Project { let json_rpc_events = language_server_rpc.events(); let embedded_visualizations = default(); let language_server = language_server_rpc.clone(); - let engine_version = Rc::new(engine_version); let module_registry = default(); let execution_contexts = default(); let visualization = controller::Visualization::new(language_server,embedded_visualizations); - let name = RefCell::new(ImString::new(name.into())); + let name = CloneRefCell::new(ImString::new(name.into())); let parser = Parser::new_or_panic(); let language_server = &*language_server_rpc; let suggestion_db = SuggestionDatabase::create_synchronized(language_server); let suggestion_db = Rc::new(suggestion_db.await?); let notifications = notification::Publisher::default(); - let data = Rc::new(Data {id,name}); + let properties = Rc::new(Properties {id,name,engine_version}); - let ret = Project {data,project_manager,language_server_rpc,language_server_bin - ,engine_version,module_registry,execution_contexts,visualization,suggestion_db - ,parser,logger,notifications}; + let ret = Project + {properties,project_manager,language_server_rpc,language_server_bin,module_registry + ,execution_contexts,visualization,suggestion_db,parser,logger,notifications}; let binary_handler = ret.binary_event_handler(); crate::executor::global::spawn(binary_protocol_events.for_each(binary_handler)); @@ -185,20 +177,47 @@ impl Project { Ok(ret) } - /// Create a project model from owned LS connections. - pub fn from_connections + /// Initializes the json and binary connection to Language Server, and creates a Project Model + pub async fn new_connected ( parent : impl AnyLogger , project_manager : Option> - , language_server_rpc : language_server::Connection - , language_server_bin : binary::Connection + , language_server_rpc : String + , language_server_bin : String , engine_version : semver::Version , id : Uuid , name : impl Str - ) -> impl Future> { - let language_server_rpc = Rc::new(language_server_rpc); - let language_server_bin = Rc::new(language_server_bin); - Self::new(parent,project_manager,language_server_rpc,language_server_bin,engine_version,id - ,name) + ) -> FallibleResult { + let client_id = Uuid::new_v4(); + let json_ws = WebSocket::new_opened(&parent,&language_server_rpc).await?; + let binary_ws = WebSocket::new_opened(&parent,&language_server_bin).await?; + let client_json = language_server::Client::new(json_ws); + let client_binary = binary::Client::new(&parent,binary_ws); + crate::executor::global::spawn(client_json.runner()); + crate::executor::global::spawn(client_binary.runner()); + let connection_json = language_server::Connection::new(client_json,client_id).await?; + let connection_binary = binary::Connection::new(client_binary,client_id).await?; + let language_server_rpc = Rc::new(connection_json); + let language_server_bin = Rc::new(connection_binary); + let model = Self::new(parent,project_manager,language_server_rpc + ,language_server_bin,engine_version,id,name).await?; + Ok(Rc::new(model)) + } + + /// Creates a project model by opening a given project in project_manager, and initializing + /// the received json and binary connections. + pub async fn new_opened + ( parent : &Logger + , project_manager : Rc + , id : Uuid + , name : impl Str + ) -> FallibleResult { + let action = MissingComponentAction::Install; + let opened = project_manager.open_project(&id,&action).await?; + let project_manager = Some(project_manager); + let json_endpoint = opened.language_server_json_address.to_string(); + let binary_endpoint = opened.language_server_binary_address.to_string(); + let version = semver::Version::parse(&opened.engine_version)?; + Self::new_connected(parent,project_manager,json_endpoint,binary_endpoint,version,id,name).await } /// Returns a handling function capable of processing updates from the binary protocol. @@ -320,7 +339,7 @@ impl Project { impl model::project::API for Project { fn name(&self) -> ImString { - self.data.name() + self.properties.name.get() } fn json_rpc(&self) -> Rc { @@ -331,7 +350,7 @@ impl model::project::API for Project { self.language_server_bin.clone_ref() } - fn engine_version(&self) -> &semver::Version { &*self.engine_version } + fn engine_version(&self) -> &semver::Version { &self.properties.engine_version } fn parser(&self) -> Parser { self.parser.clone_ref() @@ -370,8 +389,8 @@ impl model::project::API for Project { fn rename_project(&self, name:String) -> BoxFuture { async move { let project_manager = self.project_manager.as_ref().ok_or(ProjectManagerUnavailable)?; - project_manager.rename_project(&self.data.id,&name).await?; - self.data.set_name(name); + project_manager.rename_project(&self.properties.id, &name).await?; + self.properties.name.set(name.into()); Ok(()) }.boxed_local() } @@ -442,13 +461,13 @@ mod test { setup_mock_json(&mut json_client); setup_mock_binary(&mut binary_client); - let json_connection = language_server::Connection::new_mock(json_client); - let binary_connection = binary::Connection::new_mock(binary_client); + let json_connection = Rc::new(language_server::Connection::new_mock(json_client)); + let binary_connection = Rc::new(binary::Connection::new_mock(binary_client)); let project_manager = Rc::new(project_manager); let logger = Logger::new("Fixture"); let id = Uuid::new_v4(); let engine_version = semver::Version::new(0,2,1); - let project_fut = Project::from_connections(logger,Some(project_manager), + let project_fut = Project::new(logger,Some(project_manager), json_connection,binary_connection,engine_version,id,DEFAULT_PROJECT_NAME).boxed_local(); let project = test.expect_completion(project_fut).unwrap(); Fixture {test,project,binary_events_sender,json_events_sender} diff --git a/ide/src/rust/ide/src/test.rs b/ide/src/rust/ide/src/test.rs index 184c1d4013af..3cf0126d2b96 100644 --- a/ide/src/rust/ide/src/test.rs +++ b/ide/src/rust/ide/src/test.rs @@ -222,6 +222,10 @@ pub mod mock { Rc::new(project) } + pub fn ide(&self, project:&model::Project) -> controller::Ide { + Rc::new(controller::ide::Plain::new(project.clone_ref())) + } + pub fn fixture(&self) -> Fixture { self.fixture_customize(|_,_| {}) } @@ -242,25 +246,19 @@ pub mod mock { let execution = self.execution_context(); let project = self.project(module.clone_ref(),execution.clone_ref(), suggestion_db.clone_ref(),json_client); + let ide = self.ide(&project); let executed_graph = controller::ExecutedGraph::new_internal(graph.clone_ref(), project.clone_ref(),execution.clone_ref()); let executor = TestWithLocalPoolExecutor::set_up(); let data = self.clone(); let selected_nodes = Vec::new(); let searcher_mode = controller::searcher::Mode::NewNode {position:None}; - let searcher = controller::Searcher::new_from_graph_controller(&logger,&project, - executed_graph.clone_ref(),searcher_mode,selected_nodes).unwrap(); - Fixture { - executor, - data, - module, - graph, - executed_graph, - execution, - suggestion_db, - project, - searcher, - } + let searcher = controller::Searcher::new_from_graph_controller(&logger + ,ide.clone_ref(),&project,executed_graph.clone_ref(),searcher_mode,selected_nodes + ).unwrap(); + Fixture + {executor,data,module,graph,executed_graph,execution,suggestion_db,project + ,searcher,ide} } /// Register an expectation that the module described by this mock data will be opened. @@ -298,6 +296,7 @@ pub mod mock { pub executed_graph : controller::ExecutedGraph, pub suggestion_db : Rc, pub project : model::Project, + pub ide : controller::Ide, pub searcher : controller::Searcher, } diff --git a/ide/src/rust/ide/src/tests.rs b/ide/src/rust/ide/src/tests.rs index 10e20b8272ac..80f34d4c440f 100644 --- a/ide/src/rust/ide/src/tests.rs +++ b/ide/src/rust/ide/src/tests.rs @@ -26,11 +26,10 @@ fn failure_to_open_project_is_reported() { let transport = MockTransport::new(); let mut fixture = TestWithMockedTransport::set_up(&transport); fixture.run_test(async move { - let logger = Logger::new("test"); let project_manager = Rc::new(project_manager::Client::new(transport)); executor::global::spawn(project_manager.runner()); let name = ProjectName(crate::constants::DEFAULT_PROJECT_NAME.to_owned()); - let initializer = ide::initializer::WithProjectManager::new(logger,project_manager,name); + let initializer = ide::initializer::WithProjectManager::new(project_manager,name); let result = initializer.initialize_project_model().await; result.expect_err("Error should have been reported."); }); diff --git a/ide/src/rust/ide/tests/language_server.rs b/ide/src/rust/ide/tests/language_server.rs index 5e0422686985..bc70e7945ac9 100644 --- a/ide/src/rust/ide/tests/language_server.rs +++ b/ide/src/rust/ide/tests/language_server.rs @@ -15,7 +15,6 @@ use ide::prelude::*; use enso_protocol::language_server::*; use enso_protocol::types::*; use ide::double_representation::identifier::ReferentName; -use ide::model::Project; use ide::model::module; use ide::model::execution_context::Visualization; use ide::transport::web::WebSocket; @@ -65,7 +64,8 @@ wasm_bindgen_test_configure!(run_in_browser); #[allow(dead_code)] async fn ls_text_protocol_test() { let _guard = ide::initializer::setup_global_executor(); - let project = setup_project().await; + let ide = setup_ide().await; + let project = ide.current_project(); let client = project.json_rpc(); let root_id = project.content_root_id(); let project_name = ReferentName::new(project.name()).unwrap(); @@ -279,13 +279,13 @@ async fn file_events() { /// This procedure sets up the project, testing: /// * using project picker to open (or create) a project /// * establishing a binary protocol connection with Language Server -async fn setup_project() -> Project { +async fn setup_ide() -> controller::Ide { let logger = Logger::new("Test"); let config = ide::config::Startup::default(); info!(logger,"Setting up the project."); let initializer = ide::Initializer::new(config); let error_msg = "Couldn't open project."; - initializer.initialize_project_model().await.expect(error_msg) + initializer.initialize_ide_controller().await.expect(error_msg) } //#[wasm_bindgen_test::wasm_bindgen_test(async)] @@ -293,8 +293,9 @@ async fn setup_project() -> Project { /// This integration test covers writing and reading a file using the binary protocol async fn file_operations_test() { let logger = Logger::new("Test"); - let _guard = ide::initializer::setup_global_executor(); - let project = setup_project().await; + let _guard = ide::initializer::setup_global_executor(); + let ide = setup_ide().await; + let project = ide.current_project(); info!(logger,"Got project: {project:?}"); // Edit file using the text protocol let path = Path::new(project.json_rpc().content_root(), &["test_file.txt"]); @@ -320,14 +321,15 @@ async fn file_operations_test() { /// The future that tests attaching visualization and routing its updates. async fn binary_visualization_updates_test_hlp() { - let logger = Logger::new("Test"); - let project = setup_project().await; + let logger = Logger::new("Test"); + let ide = setup_ide().await; + let project = ide.current_project(); info!(logger,"Got project: {project:?}"); let expression = "x -> x.json_serialize"; use ensogl::system::web::sleep; - use ide::MAIN_DEFINITION_NAME; + use controller::project::MAIN_DEFINITION_NAME; let logger = Logger::new("Test"); let module_path = ide::initial_module_path(&project).unwrap(); diff --git a/ide/src/rust/ide/view/src/status_bar.rs b/ide/src/rust/ide/view/src/status_bar.rs index b69f6be49be7..a969d5675ad6 100644 --- a/ide/src/rust/ide/view/src/status_bar.rs +++ b/ide/src/rust/ide/view/src/status_bar.rs @@ -126,6 +126,7 @@ ensogl::define_endpoints! { add_event (event::Label), add_process (process::Label), finish_process (process::Id), + clear_all (), } Output { last_event (event::Id), @@ -241,6 +242,11 @@ impl Model { fn last_event_message(&self) -> event::Label { self.events.borrow().last().cloned().unwrap_or_default() } + + fn clear_all(&self) { + self.events.borrow_mut().clear(); + self.processes.borrow_mut().clear(); + } } @@ -288,14 +294,18 @@ impl View { label <- any(label_after_adding_event,label_after_adding_process,label_after_finishing_process); eval label ((label) model.label.set_content(label.to_string())); + eval_ frp.clear_all (model.clear_all()); + frp.source.last_event <+ event_added; frp.source.last_process <+ process_added; frp.source.displayed_event <+ event_added.map(|id| Some(*id)); frp.source.displayed_event <+ process_added.constant(None); + frp.source.displayed_event <+ frp.clear_all.constant(None); frp.source.displayed_process <+ process_added.map(|id| Some(*id)); frp.source.displayed_process <+ event_added.constant(None); frp.source.displayed_process <+ displayed_process_finished.constant(None); + frp.source.displayed_process <+ frp.clear_all.constant(None); eval_ model.label.output.width (model.update_layout()); eval_ scene.frp.camera_changed (model.camera_changed());