diff --git a/CHANGELOG.md b/CHANGELOG.md
index fabb2107bf..9cb96fe3e5 100644
--- a/CHANGELOG.md
+++ b/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/src/rust/ide/lib/enso-protocol/src/language_server/types.rs b/src/rust/ide/lib/enso-protocol/src/language_server/types.rs
index 4ae912d000..f17d2f9195 100644
--- a/src/rust/ide/lib/enso-protocol/src/language_server/types.rs
+++ b/src/rust/ide/lib/enso-protocol/src/language_server/types.rs
@@ -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/src/rust/ide/lib/enso-protocol/src/project_manager.rs b/src/rust/ide/lib/enso-protocol/src/project_manager.rs
index 3d2fb8b410..299657a664 100644
--- a/src/rust/ide/lib/enso-protocol/src/project_manager.rs
+++ b/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/src/rust/ide/src/controller.rs b/src/rust/ide/src/controller.rs
index 72752b54a4..164c89a995 100644
--- a/src/rust/ide/src/controller.rs
+++ b/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/src/rust/ide/src/controller/ide.rs b/src/rust/ide/src/controller/ide.rs
new file mode 100644
index 0000000000..0be8807e1b
--- /dev/null
+++ b/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/src/rust/ide/src/controller/ide/desktop.rs b/src/rust/ide/src/controller/ide/desktop.rs
new file mode 100644
index 0000000000..7ddcbc9f7f
--- /dev/null
+++ b/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/src/rust/ide/src/controller/ide/plain.rs b/src/rust/ide/src/controller/ide/plain.rs
new file mode 100644
index 0000000000..66ee8a093f
--- /dev/null
+++ b/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/src/rust/ide/src/controller/project.rs b/src/rust/ide/src/controller/project.rs
new file mode 100644
index 0000000000..c6323ce1d6
--- /dev/null
+++ b/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/src/rust/ide/src/controller/searcher.rs b/src/rust/ide/src/controller/searcher.rs
index 36290c2eeb..fbbda11fb1 100644
--- a/src/rust/ide/src/controller/searcher.rs
+++ b/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/src/rust/ide/src/controller/searcher/action.rs b/src/rust/ide/src/controller/searcher/action.rs
index 1706b3cda8..2135358d2e 100644
--- a/src/rust/ide/src/controller/searcher/action.rs
+++ b/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/src/rust/ide/src/ide.rs b/src/rust/ide/src/ide.rs
index 963cdf5b3c..e84c0a2e83 100644
--- a/src/rust/ide/src/ide.rs
+++ b/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/src/rust/ide/src/ide/initializer.rs b/src/rust/ide/src/ide/initializer.rs
index d58e034225..45d6283140 100644
--- a/src/rust/ide/src/ide/initializer.rs
+++ b/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/src/rust/ide/src/ide/integration.rs b/src/rust/ide/src/ide/integration.rs
index 6da99dc3b4..55ff337f69 100644
--- a/src/rust/ide/src/ide/integration.rs
+++ b/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/src/rust/ide/src/ide/integration/project.rs b/src/rust/ide/src/ide/integration/project.rs
new file mode 100644
index 0000000000..147a2a71ca
--- /dev/null
+++ b/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/src/rust/ide/src/model/project.rs b/src/rust/ide/src/model/project.rs
index d36943e530..a14802bc3b 100644
--- a/src/rust/ide/src/model/project.rs
+++ b/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/src/rust/ide/src/model/project/synchronized.rs b/src/rust/ide/src/model/project/synchronized.rs
index 45c820aef7..1ae515b1df 100644
--- a/src/rust/ide/src/model/project/synchronized.rs
+++ b/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/src/rust/ide/src/test.rs b/src/rust/ide/src/test.rs
index 184c1d4013..3cf0126d2b 100644
--- a/src/rust/ide/src/test.rs
+++ b/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/src/rust/ide/src/tests.rs b/src/rust/ide/src/tests.rs
index 10e20b8272..80f34d4c44 100644
--- a/src/rust/ide/src/tests.rs
+++ b/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/src/rust/ide/tests/language_server.rs b/src/rust/ide/tests/language_server.rs
index 5e04226869..bc70e7945a 100644
--- a/src/rust/ide/tests/language_server.rs
+++ b/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/src/rust/ide/view/src/status_bar.rs b/src/rust/ide/view/src/status_bar.rs
index b69f6be49b..a969d5675a 100644
--- a/src/rust/ide/view/src/status_bar.rs
+++ b/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());
|