Finish integration refactoring (#3206)

All other things from the old integration layer were rewritten to some kind of presenter. The big integration module was removed.
This commit is contained in:
Adam Obuchowicz 2021-12-29 13:44:13 +01:00 committed by GitHub
parent 2676aa50a3
commit e8077253c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1762 additions and 2732 deletions

View File

@ -56,6 +56,5 @@ ensogl::read_args! {
authentication_enabled : bool,
email : String,
application_config_url : String,
rust_new_presentation_layer : bool,
}
}

View File

@ -106,6 +106,14 @@ pub enum Notification {
// === API ===
// ===========
// === Errors ===
#[allow(missing_docs)]
#[derive(Clone, Debug, Fail)]
#[fail(display = "Project with name \"{}\" not found.", 0)]
struct ProjectNotFound(String);
// === Managing API ===
/// The API of all project management operations.
@ -124,6 +132,23 @@ pub trait ManagingProjectAPI {
/// Open the project with given UUID.
fn open_project(&self, id: Uuid) -> BoxFuture<FallibleResult>;
/// Open project by name. It makes two calls to the Project Manager: one for listing projects
/// and then for the project opening.
fn open_project_by_name(&self, name: String) -> BoxFuture<FallibleResult> {
async move {
let projects = self.list_projects().await?;
let mut projects = projects.into_iter();
let project = projects.find(|project| project.name.as_ref() == name);
let uuid = project.map(|project| project.id);
if let Some(uuid) = uuid {
self.open_project(uuid).await
} else {
Err(ProjectNotFound(name).into())
}
}
.boxed_local()
}
}

View File

@ -1,6 +1,5 @@
//! This module contains the IDE object implementation.
pub mod initializer;
pub mod integration;
pub use initializer::Initializer;
@ -33,17 +32,6 @@ const ALIVE_LOG_INTERVAL_SEC: u64 = 60;
// === Ide ===
// ===========
/// One of the integration implementations.
///
/// The new, refactored integration is called "Presenter", but it is not yet fully implemented.
/// To test it, run IDE with `--rust-new-presentation-layer` option. By default, the old integration
/// is used.
#[derive(Debug)]
enum Integration {
Old(integration::Integration),
New(Presenter),
}
/// The main Ide structure.
///
/// This structure is a root of all objects in our application. It includes both layers:
@ -52,9 +40,9 @@ enum Integration {
pub struct Ide {
application: Application,
#[allow(dead_code)]
/// The integration layer is never directly accessed, but needs to be kept alive to keep
/// The presenter layer is never directly accessed, but needs to be kept alive to keep
/// performing its function.
integration: Integration,
presenter: Presenter,
network: frp::Network,
}
@ -65,13 +53,9 @@ impl Ide {
view: ide_view::root::View,
controller: controller::Ide,
) -> Self {
let integration = if enso_config::ARGS.rust_new_presentation_layer.unwrap_or(false) {
Integration::New(Presenter::new(controller, view))
} else {
Integration::Old(integration::Integration::new(controller, view))
};
let presenter = Presenter::new(controller, view);
let network = frp::Network::new("Ide");
Ide { application, integration, network }.init()
Ide { application, presenter, network }.init()
}
fn init(self) -> Self {

View File

@ -1,243 +0,0 @@
//! The integration layer between IDE controllers and the view.
pub mod file_system;
pub mod project;
pub mod visualization;
use crate::prelude::*;
use crate::controller::ide::StatusNotification;
use crate::model::undo_redo::Aware;
use enso_frp as frp;
use ide_view::graph_editor::SharedHashMap;
// =======================
// === IDE Integration ===
// =======================
// === 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,
controller: controller::Ide,
view: ide_view::root::View,
project_integration: RefCell<Option<project::Integration>>,
}
impl Model {
/// Create a new project integration
fn setup_and_display_new_project(self: Rc<Self>) {
// 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;
if let Some(project_model) = self.controller.current_project() {
// We know the name of new project before it loads. We set it right now to avoid
// displaying placeholder on the scene during loading.
let project_view = self.view.project();
let breadcrumbs = &project_view.graph().model.breadcrumbs;
breadcrumbs.project_name(project_model.name().to_string());
let status_notifications = self.controller.status_notifications().clone_ref();
let project = controller::Project::new(project_model, status_notifications.clone_ref());
executor::global::spawn(async move {
match project.initialize().await {
Ok(result) => {
let view = project_view;
let status_bar = self.view.status_bar().clone_ref();
let text = result.main_module_text;
let graph = result.main_graph;
let ide = self.controller.clone_ref();
let project = project.model;
let main = result.main_module_model;
let integration = project::Integration::new(
view, status_bar, graph, text, ide, project, main,
);
// We don't want any initialization-related changes to appear on undo stack.
integration.graph_controller().undo_redo_repository().clear_all();
*self.project_integration.borrow_mut() = Some(integration);
}
Err(err) => {
let err_msg = format!("Failed to initialize project: {}", err);
error!(self.logger, "{err_msg}");
status_notifications.publish_event(err_msg)
}
}
});
}
}
/// Open a project by name. It makes two calls to Project Manager: one for listing projects and
/// a second one for opening the project.
pub fn open_project(&self, project_name: &str) {
let logger = self.logger.clone_ref();
let controller = self.controller.clone_ref();
let name = project_name.to_owned();
crate::executor::global::spawn(async move {
if let Ok(managing_api) = controller.manage_projects() {
match managing_api.list_projects().await {
Ok(projects) => {
let mut projects = projects.into_iter();
let project = projects.find(|project| project.name.as_ref() == name);
let uuid = project.map(|project| project.id);
if let Some(uuid) = uuid {
if let Err(err) = managing_api.open_project(uuid).await {
error!(logger, "Could not open open project `{name}`: {err}.");
}
} else {
error!(logger, "Could not find project `{name}`.")
}
}
Err(err) => error!(logger, "Could not list projects: {err}."),
}
} else {
warning!(logger, "Project opening failed: no ProjectManagingAPI available.");
}
});
}
/// Create a new project. `template` is an optional name of the project template passed to the
/// Engine. It makes a call to Project Manager.
fn create_project(&self, template: Option<&str>) {
let logger = self.logger.clone_ref();
let controller = self.controller.clone_ref();
let template = template.map(ToOwned::to_owned);
crate::executor::global::spawn(async move {
if let Ok(managing_api) = controller.manage_projects() {
if let Err(err) = managing_api.create_new_project(template.clone()).await {
if let Some(template) = template {
error!(
logger,
"Could not create new project from template {template}: {err}."
);
} else {
error!(logger, "Could not create new project: {err}.");
}
}
} else {
warning!(logger, "Project creation failed: no ProjectManagingAPI available.");
}
});
}
}
// === Integration ===
/// 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<Model>,
network: frp::Network,
}
impl Integration {
/// Create the integration of given controller and view.
pub fn new(controller: controller::Ide, view: ide_view::root::View) -> Self {
let logger = Logger::new("ide::Integration");
let project_integration = default();
let model = Rc::new(Model { logger, controller, view, project_integration });
frp::new_network! { network
let welcome_view_frp = &model.view.welcome_screen().frp;
eval welcome_view_frp.open_project((name) {
model.open_project(name);
});
eval welcome_view_frp.create_project((template) {
model.create_project(template.as_deref());
});
let root_frp = &model.view.frp;
root_frp.switch_view_to_project <+ welcome_view_frp.create_project.constant(());
root_frp.switch_view_to_project <+ welcome_view_frp.open_project.constant(());
}
Self { model, network }.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.set_projects_list_on_welcome_screen();
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::<ControllerHandle, ViewHandle>::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));
}
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(())
});
executor::global::spawn(status_notif_updates)
}
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
| controller::ide::Notification::ProjectOpened =>
model.setup_and_display_new_project(),
}
}
futures::future::ready(())
}));
}
fn set_projects_list_on_welcome_screen(&self) {
let controller = self.model.controller.clone_ref();
let welcome_view_frp = self.model.view.welcome_screen().frp.clone_ref();
let logger = self.model.logger.clone_ref();
crate::executor::global::spawn(async move {
if let Ok(project_manager) = controller.manage_projects() {
match project_manager.list_projects().await {
Ok(projects) => {
let names = projects.into_iter().map(|p| p.name.into()).collect::<Vec<_>>();
welcome_view_frp.set_projects_list(names);
}
Err(err) => {
error!(logger, "Unable to get list of projects: {err}.");
}
}
}
});
}
}

View File

@ -1,291 +0,0 @@
//! The integration between EnsoGL File Browser and the Engine's file management API.
use crate::prelude::*;
use crate::controller::graph::NewNodeInfo;
use crate::controller::upload::pick_non_colliding_name;
use engine_protocol::language_server;
use engine_protocol::language_server::ContentRoot;
use engine_protocol::language_server::FileSystemObject;
use enso_frp as frp;
use ensogl_component::file_browser::model::Entry;
use ensogl_component::file_browser::model::EntryType;
use ensogl_component::file_browser::model::FolderContent;
use ensogl_component::file_browser::model::FolderType;
use json_rpc::error::RpcError;
use std::iter::once;
// =======================
// === Path Conversion ===
// =======================
// === Errors ===
#[derive(Clone, Debug, Fail)]
#[fail(display = "Invalid path received from File Browser Component: {}", path)]
struct InvalidPath {
path: String,
}
// === Functions ===
/// Translate [`language_server::Path`] to path used by File Browser.
///
/// The path will be absolute, starting with "/". The first segment will be the content root id.
pub fn to_file_browser_path(path: &language_server::Path) -> std::path::PathBuf {
let root_id_str = path.root_id.to_string();
let segments_str = path.segments.iter().map(AsRef::<str>::as_ref);
once("/").chain(once(root_id_str.as_ref())).chain(segments_str).collect()
}
/// Translate [`std::path::Path`] received from File Browser to a language server path.
///
/// The path should br absolute, starting with "/" and first segment should be a valid uuid of the
/// content root. The function returns `Err` if these conditions are not met.
///
/// Translating back path generated by [`to_file_browser_path`] must not fail.
pub fn from_file_browser_path(path: &std::path::Path) -> FallibleResult<language_server::Path> {
use std::path::Component::*;
let mut iter = path.components();
match (iter.next(), iter.next()) {
(Some(RootDir), Some(Normal(root_id))) => {
let root_id = root_id.to_string_lossy().parse()?;
Ok(language_server::Path::new(root_id, iter.map(|s| s.as_os_str().to_string_lossy())))
}
_ => {
let path = path.to_string_lossy().to_string();
Err(InvalidPath { path }.into())
}
}
}
// =================
// === Providers ===
// =================
// === FileProvider ===
/// The provider of the all content roots and their contents.
///
/// The content roots will be presented to the File Browser as a root directories. The paths
/// received from the Engine are translated accordingly - see [`to_file_browser_path`] and
/// [`from_file_browser_path`].
#[derive(Clone, Debug)]
pub struct FileProvider {
connection: Rc<language_server::Connection>,
content_roots: Vec<Rc<ContentRoot>>,
}
impl FileProvider {
/// Create provider of all content roots attached to given project.
pub fn new(project: &model::Project) -> Self {
Self { connection: project.json_rpc(), content_roots: project.content_roots() }
}
}
impl FolderContent for FileProvider {
fn request_entries(
&self,
entries_loaded: frp::Any<Rc<Vec<Entry>>>,
_error_occurred: frp::Any<ImString>,
) {
let entries = self.content_roots.iter().filter_map(|root| {
let ls_path = language_server::Path::new_root(root.id());
let path = to_file_browser_path(&ls_path);
let (name, folder_type) = match &**root {
language_server::ContentRoot::Project { .. } =>
Some(("Project".to_owned(), FolderType::Project)),
language_server::ContentRoot::FileSystemRoot { path, .. } =>
Some((path.clone(), FolderType::Root)),
language_server::ContentRoot::Home { .. } =>
Some(("Home".to_owned(), FolderType::Home)),
language_server::ContentRoot::Library { .. } => None, /* We skip libraries, as */
// they cannot be easily
// inserted.
language_server::ContentRoot::Custom { .. } => None, /* Custom content roots are
* not used. */
}?;
let type_ = EntryType::Folder {
type_: folder_type,
content: {
let connection = self.connection.clone_ref();
DirectoryView::new_from_root(connection, root.clone_ref()).into()
},
};
Some(Entry { type_, name, path })
});
entries_loaded.emit(Rc::new(entries.sorted().collect_vec()));
}
}
// === DirectoryView ===
/// A provider of the content of a specific directory.
#[derive(Clone, CloneRef, Debug)]
pub struct DirectoryView {
connection: Rc<language_server::Connection>,
content_root: Rc<ContentRoot>,
path: Rc<language_server::Path>,
}
impl DirectoryView {
/// Create a view of given Content Root content.
pub fn new_from_root(
connection: Rc<language_server::Connection>,
content_root: Rc<ContentRoot>,
) -> Self {
let path = Rc::new(language_server::Path::new_root(content_root.id()));
Self { connection, content_root, path }
}
/// Returns a new view of the content of current's view subdirectory.
pub fn sub_view(&self, sub_dir: impl Str) -> DirectoryView {
DirectoryView {
connection: self.connection.clone_ref(),
content_root: self.content_root.clone_ref(),
path: Rc::new(self.path.append_im(sub_dir)),
}
}
/// Retrieves the directory content from the Engine.
pub async fn get_entries_list(&self) -> Result<Vec<Entry>, RpcError> {
let response = self.connection.file_list(&self.path).await?;
let entries = response.paths.into_iter().map(|fs_obj| match fs_obj {
FileSystemObject::Directory { name, path }
| FileSystemObject::DirectoryTruncated { name, path }
| FileSystemObject::SymlinkLoop { name, path, .. } => {
let path = to_file_browser_path(&path).join(&name);
let sub = self.sub_view(&name);
let type_ =
EntryType::Folder { type_: FolderType::Standard, content: sub.into() };
Entry { type_, name, path }
}
FileSystemObject::File { name, path } | FileSystemObject::Other { name, path } => {
let path = to_file_browser_path(&path).join(&name);
let type_ = EntryType::File;
Entry { type_, name, path }
}
});
Ok(entries.sorted().collect())
}
}
impl FolderContent for DirectoryView {
fn request_entries(
&self,
entries_loaded: frp::Any<Rc<Vec<Entry>>>,
error_occurred: frp::Any<ImString>,
) {
let this = self.clone_ref();
executor::global::spawn(async move {
match this.get_entries_list().await {
Ok(entries) => entries_loaded.emit(Rc::new(entries)),
Err(RpcError::RemoteError(error)) =>
error_occurred.emit(ImString::new(error.message)),
Err(error) => error_occurred.emit(ImString::new(error.to_string())),
}
});
}
}
// ====================
// === User Actions ===
// ====================
// === Errors ===
#[derive(Clone, Debug, Fail)]
#[fail(display = "Invalid source file for copy/move operation: {}", path)]
// the path is a String here, because there is no Display for path_buf.
struct InvalidSourceFile {
path: String,
}
// === create_node_from_file ===
/// Create a node reading the given file.
///
/// The `path` should be a path convertible to Path from Language Server Protocol - see
/// [`from_file_browser_path`], otherwise function will return `Err`.
pub fn create_node_from_file(
project: &model::Project,
graph: &controller::Graph,
path: &std::path::Path,
) -> FallibleResult {
let ls_path = from_file_browser_path(path)?;
let path_segments = ls_path.segments.into_iter().join("/");
let content_root = project.content_root_by_id(ls_path.root_id)?;
let path = match &*content_root {
ContentRoot::Project { .. } => format!("Enso_Project.root/\"{}\"", path_segments),
ContentRoot::FileSystemRoot { path, .. } => format!("\"{}/{}\"", path, path_segments),
ContentRoot::Home { .. } => format!("File.home/\"{}\"", path_segments),
ContentRoot::Library { namespace, name, .. } =>
format!("{}.{}.Enso_Project.root / \"{}\"", namespace, name, path_segments),
ContentRoot::Custom { .. } => "Unsupported Content Root".to_owned(),
};
let expression = format!("File.read {}", path);
let node_info = NewNodeInfo::new_pushed_back(expression);
graph.add_node(node_info)?;
Ok(())
}
// === copy_or_move_file ===
/// A enum describing if we want to copy or move file.
#[allow(missing_docs)]
#[derive(Copy, Clone, Debug)]
pub enum FileOperation {
Copy,
Move,
}
impl Default for FileOperation {
fn default() -> Self {
Self::Copy
}
}
impl FileOperation {
/// Returns a verb describing the operation ("copy" or "move"), to be used in diagnostic
/// messages.
pub fn verb(&self) -> &'static str {
match self {
Self::Copy => "copy",
Self::Move => "move",
}
}
}
/// Do copy or move file operation. The destination must be a directory. If the destination already
/// contains file with the same name as source's, the moved/copy file name will be altered
/// according to algorithm described in [`pick_non_colliding_name`] docs.
pub async fn do_file_operation(
project: &model::Project,
source: &std::path::Path,
dest_dir: &std::path::Path,
operation: FileOperation,
) -> FallibleResult {
let json_rpc = project.json_rpc();
let ls_source = from_file_browser_path(source)?;
let ls_dest = from_file_browser_path(dest_dir)?;
let src_name = ls_source
.file_name()
.ok_or_else(|| InvalidSourceFile { path: source.to_string_lossy().to_string() })?;
let dest_name = pick_non_colliding_name(&*json_rpc, &ls_dest, src_name).await?;
let dest_full = ls_dest.append_im(dest_name);
match operation {
FileOperation::Copy => json_rpc.copy_file(&ls_source, &dest_full).await?,
FileOperation::Move => json_rpc.move_file(&ls_source, &dest_full).await?,
}
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,15 @@
//! The Presenter is a layer between logical part of the IDE (controllers, engine models) and the
//! views (the P letter in MVP pattern). The presenter reacts to changes in the controllers, and
//! actively updates the view. It also passes all user interactions from view to controllers.
//!
//! **The presenters are not fully implemented**. Therefore, the old integration defined in
//! [`crate::integration`] is used by default. The presenters may be tested by passing
//! `--rust-new-presentation-layer` commandline argument.
pub mod code;
pub mod graph;
pub mod project;
pub mod searcher;
pub use code::Code;
pub use graph::Graph;
pub use project::Project;
pub use searcher::Searcher;
use crate::prelude::*;
@ -18,6 +17,7 @@ use crate::controller::ide::StatusNotification;
use crate::executor::global::spawn_stream_handler;
use crate::presenter;
use enso_frp as frp;
use ide_view as view;
use ide_view::graph_editor::SharedHashMap;
@ -46,15 +46,24 @@ impl Model {
// We know the name of new project before it loads. We set it right now to avoid
// displaying placeholder on the scene during loading.
let project_view = self.view.project();
let status_bar = self.view.status_bar().clone_ref();
let breadcrumbs = &project_view.graph().model.breadcrumbs;
breadcrumbs.project_name(project_model.name().to_string());
let status_notifications = self.controller.status_notifications().clone_ref();
let ide_controller = self.controller.clone_ref();
let project_controller =
controller::Project::new(project_model, status_notifications.clone_ref());
executor::global::spawn(async move {
match presenter::Project::initialize(project_controller, project_view).await {
match presenter::Project::initialize(
ide_controller,
project_controller,
project_view,
status_bar,
)
.await
{
Ok(project) => {
*self.current_project.borrow_mut() = Some(project);
}
@ -67,6 +76,46 @@ impl Model {
});
}
}
/// Open a project by name. It makes two calls to Project Manager: one for listing projects and
/// a second one for opening the project.
pub fn open_project(&self, project_name: String) {
let logger = self.logger.clone_ref();
let controller = self.controller.clone_ref();
crate::executor::global::spawn(async move {
if let Ok(managing_api) = controller.manage_projects() {
if let Err(err) = managing_api.open_project_by_name(project_name).await {
error!(logger, "Cannot open project by name: {err}.");
}
} else {
warning!(logger, "Project opening failed: no ProjectManagingAPI available.");
}
});
}
/// Create a new project. `template` is an optional name of the project template passed to the
/// Engine. It makes a call to Project Manager.
fn create_project(&self, template: Option<&str>) {
let logger = self.logger.clone_ref();
let controller = self.controller.clone_ref();
let template = template.map(ToOwned::to_owned);
crate::executor::global::spawn(async move {
if let Ok(managing_api) = controller.manage_projects() {
if let Err(err) = managing_api.create_new_project(template.clone()).await {
if let Some(template) = template {
error!(
logger,
"Could not create new project from template {template}: {err}."
);
} else {
error!(logger, "Could not create new project: {err}.");
}
}
} else {
warning!(logger, "Project creation failed: no ProjectManagingAPI available.");
}
});
}
}
@ -80,7 +129,8 @@ impl Model {
/// See [`crate::presenter`] docs for information about presenters in general.
#[derive(Clone, CloneRef, Debug)]
pub struct Presenter {
model: Rc<Model>,
network: frp::Network,
model: Rc<Model>,
}
impl Presenter {
@ -92,12 +142,25 @@ impl Presenter {
let logger = Logger::new("Presenter");
let current_project = default();
let model = Rc::new(Model { logger, controller, view, current_project });
Self { model }.init()
frp::new_network! { network
let welcome_view_frp = &model.view.welcome_screen().frp;
eval welcome_view_frp.open_project((name) model.open_project(name.to_owned()));
eval welcome_view_frp.create_project((templ) model.create_project(templ.as_deref()));
let root_frp = &model.view.frp;
root_frp.switch_view_to_project <+ welcome_view_frp.create_project.constant(());
root_frp.switch_view_to_project <+ welcome_view_frp.open_project.constant(());
}
Self { model, network }.init()
}
fn init(self) -> Self {
self.setup_status_bar_notification_handler();
self.setup_controller_notification_handler();
self.set_projects_list_on_welcome_screen();
self.model.clone_ref().setup_and_display_new_project();
self
}
@ -145,4 +208,23 @@ impl Presenter {
futures::future::ready(())
});
}
fn set_projects_list_on_welcome_screen(&self) {
let controller = self.model.controller.clone_ref();
let welcome_view_frp = self.model.view.welcome_screen().frp.clone_ref();
let logger = self.model.logger.clone_ref();
crate::executor::global::spawn(async move {
if let Ok(project_manager) = controller.manage_projects() {
match project_manager.list_projects().await {
Ok(projects) => {
let names = projects.into_iter().map(|p| p.name.into()).collect::<Vec<_>>();
welcome_view_frp.set_projects_list(names);
}
Err(err) => {
error!(logger, "Unable to get list of projects: {err}.");
}
}
}
});
}
}

View File

@ -0,0 +1,124 @@
//! The module containing [`Code`] presenter. See [`crate::presenter`] documentation to know more
//! about presenters in general.
use crate::prelude::*;
use crate::controller::text::Notification;
use crate::executor::global::spawn_stream_handler;
use enso_frp as frp;
use ide_view as view;
// =============
// === Model ===
// =============
#[derive(Debug)]
struct Model {
logger: Logger,
controller: controller::Text,
view: view::code_editor::View,
}
impl Model {
fn new(controller: controller::Text, view: view::code_editor::View) -> Self {
let logger = Logger::new("presenter::code");
Self { logger, controller, view }
}
fn apply_change_from_view(&self, change: &enso_text::Change) {
let converted = enso_text::Change { range: change.range, text: change.text.to_string() };
if let Err(err) = self.controller.apply_text_change(converted) {
error!(self.logger, "Error while applying text change: {err}");
}
}
async fn emit_event_with_controller_code(&self, endpoint: &frp::Source<ImString>) {
match self.controller.read_content().await {
Ok(code) => endpoint.emit(ImString::new(code)),
Err(err) => {
error!(self.logger, "Error while updating code editor: {err}")
}
}
}
fn save_module(&self, content: String) {
let logger = self.logger.clone_ref();
let controller = self.controller.clone_ref();
executor::global::spawn(async move {
if let Err(err) = controller.store_content(content).await {
error!(logger, "Error while saving module: {err}");
}
})
}
}
// ============
// === Code ===
// ============
/// The presenter synchronizing Code Editor view with the module code in controllers.
#[derive(Debug)]
pub struct Code {
_network: frp::Network,
model: Rc<Model>,
}
impl Code {
/// Constructor. The returned structure works right away and does not need any initialization.
pub fn new(controller: controller::Text, project_view: &view::project::View) -> Self {
let network = frp::Network::new("presenter::code");
let view = project_view.code_editor().clone_ref();
let model = Rc::new(Model::new(controller, view));
let text_area = model.view.text_area();
frp::extend! { network
code_in_controller <- source::<ImString>();
desynchronized <- all_with(&code_in_controller, &text_area.content, |controller, view|
*controller != view.to_string()
);
text_area.set_content <+ code_in_controller.gate(&desynchronized).map(|s| s.to_string());
maybe_change_to_apply <= text_area.changed;
change_to_apply <- maybe_change_to_apply.gate(&desynchronized);
eval change_to_apply ((change) model.apply_change_from_view(change));
code_to_save <- code_in_controller.sample(&project_view.save_module);
eval code_to_save ((code) model.save_module(code.to_string()));
}
Self { _network: network, model }
.display_initial_code(code_in_controller.clone_ref())
.setup_notification_handler(code_in_controller)
}
fn setup_notification_handler(self, code_in_controller: frp::Source<ImString>) -> Self {
let weak = Rc::downgrade(&self.model);
let notifications = self.model.controller.subscribe();
spawn_stream_handler(weak, notifications, move |notification, model| {
let endpoint_clone = code_in_controller.clone_ref();
let model_clone = model.clone_ref();
async move {
match notification {
Notification::Invalidate =>
model_clone.emit_event_with_controller_code(&endpoint_clone).await,
}
}
});
self
}
fn display_initial_code(self, code_in_controller: frp::Source<ImString>) -> Self {
let model = self.model.clone_ref();
executor::global::spawn(async move {
model.emit_event_with_controller_code(&code_in_controller).await
});
self
}
}

View File

@ -1,27 +1,66 @@
//! The module with the [`Graph`] presenter. See [`crate::presenter`] documentation to know more
//! about presenters in general.
mod state;
pub mod call_stack;
pub mod state;
pub mod visualization;
pub use call_stack::CallStack;
pub use visualization::Visualization;
use crate::prelude::*;
use crate::controller::upload::NodeFromDroppedFileHandler;
use crate::executor::global::spawn_stream_handler;
use crate::presenter::graph::state::State;
use enso_frp as frp;
use futures::future::LocalBoxFuture;
use ide_view as view;
use ide_view::graph_editor::component::node as node_view;
use ide_view::graph_editor::component::visualization as visualization_view;
use ide_view::graph_editor::EdgeEndpoint;
// ===============
// === Aliases ===
// ===============
type ViewNodeId = view::graph_editor::NodeId;
type AstNodeId = ast::Id;
type ViewConnection = view::graph_editor::EdgeId;
type AstConnection = controller::graph::Connection;
/// The node identifier used by view.
pub type ViewNodeId = view::graph_editor::NodeId;
/// The node identifier used by controllers.
pub type AstNodeId = ast::Id;
/// The connection identifier used by view.
pub type ViewConnection = view::graph_editor::EdgeId;
/// The connection identifier used by controllers.
pub type AstConnection = controller::graph::Connection;
// =================
// === Constants ===
// =================
/// 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)
}
@ -29,22 +68,42 @@ type AstConnection = controller::graph::Connection;
// === Model ===
// =============
#[derive(Clone, Debug)]
#[derive(Debug)]
struct Model {
logger: Logger,
controller: controller::ExecutedGraph,
view: view::graph_editor::GraphEditor,
state: Rc<state::State>,
logger: Logger,
project: model::Project,
controller: controller::ExecutedGraph,
view: view::graph_editor::GraphEditor,
state: Rc<State>,
_visualization: Visualization,
_execution_stack: CallStack,
}
impl Model {
pub fn new(
project: model::Project,
controller: controller::ExecutedGraph,
view: view::graph_editor::GraphEditor,
) -> Self {
let logger = Logger::new("presenter::Graph");
let state = default();
Self { logger, controller, view, state }
let state: Rc<State> = default();
let visualization = Visualization::new(
project.clone_ref(),
controller.clone_ref(),
view.clone_ref(),
state.clone_ref(),
);
let execution_stack =
CallStack::new(&logger, controller.clone_ref(), view.clone_ref(), state.clone_ref());
Self {
logger,
project,
controller,
view,
state,
_visualization: visualization,
_execution_stack: execution_stack,
}
}
/// Node position was changed in view.
@ -58,6 +117,23 @@ impl Model {
);
}
fn node_visualization_changed(&self, id: ViewNodeId, path: Option<visualization_view::Path>) {
self.update_ast(
|| {
let ast_id =
self.state.update_from_view().set_node_visualization(id, path.clone())?;
let module = self.controller.graph().module;
let result = match serde_json::to_value(path) {
Ok(serialized) => module
.with_node_metadata(ast_id, Box::new(|md| md.visualization = serialized)),
Err(err) => FallibleResult::Err(err.into()),
};
Some(result)
},
"update node position",
);
}
/// Node was removed in view.
fn node_removed(&self, id: ViewNodeId) {
self.update_ast(
@ -92,6 +168,21 @@ impl Model {
);
}
fn nodes_collapsed(&self, collapsed: &[ViewNodeId]) {
self.update_ast(
|| {
debug!(self.logger, "Collapsing node.");
let ids = collapsed.iter().filter_map(|node| self.state.ast_node_id_of_view(*node));
let new_node_id = self.controller.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.
Some(new_node_id.map(|_| ()))
},
"collapse nodes",
);
}
fn update_ast<F>(&self, f: F, action: &str)
where F: FnOnce() -> Option<FallibleResult> {
if let Some(Err(err)) = f() {
@ -156,6 +247,15 @@ impl Model {
Some((id, method_pointer))
}
fn refresh_node_error(
&self,
expression: ast::Id,
) -> Option<(ViewNodeId, Option<node_view::Error>)> {
let registry = self.controller.computed_value_info_registry();
let payload = registry.get(&expression).map(|info| info.payload.clone());
self.state.update_from_controller().set_node_error_from_payload(expression, payload)
}
/// Extract the expression's current type from controllers.
fn expression_type(&self, id: ast::Id) -> Option<view::graph_editor::Type> {
let registry = self.controller.computed_value_info_registry();
@ -171,6 +271,64 @@ impl Model {
let method = suggestion_db.lookup_method_ptr(method_id).ok()?;
Some(view::graph_editor::MethodPointer(Rc::new(method)))
}
fn file_dropped(&self, file: ensogl_drop_manager::File, position: Vector2<f32>) {
let project = self.project.clone_ref();
let graph = self.controller.graph();
let to_upload = controller::upload::FileToUpload {
name: file.name.clone_ref().into(),
size: file.size,
data: file,
};
let position = model::module::Position { vector: position };
let handler = NodeFromDroppedFileHandler::new(&self.logger, project, graph);
if let Err(err) = handler.create_node_and_start_uploading(to_upload, position) {
error!(self.logger, "Error when creating node from dropped file: {err}");
}
}
/// Look through all graph's nodes in AST and set position where it is missing.
fn initialize_nodes_positions(&self, default_gap_between_nodes: f32) {
match self.controller.graph().nodes() {
Ok(nodes) => {
use model::module::Position;
let base_default_position = default_node_position();
let node_positions =
nodes.iter().filter_map(|node| node.metadata.as_ref()?.position);
let bottommost_pos = node_positions
.min_by(Position::ord_by_y)
.map(|p| p.vector)
.unwrap_or(base_default_position);
let offset = default_gap_between_nodes + node_view::HEIGHT;
let mut next_default_position =
Vector2::new(bottommost_pos.x, bottommost_pos.y - offset);
let transaction =
self.controller.get_or_open_transaction("Setting default positions.");
transaction.ignore();
for node in nodes {
if !node.has_position() {
if let Err(err) = self
.controller
.graph()
.set_node_position(node.id(), next_default_position)
{
warning!(
self.logger,
"Failed to initialize position of node {node.id()}: {err}"
);
}
next_default_position.y -= offset;
}
}
}
Err(err) => {
warning!(self.logger, "Failed to initialize nodes positions: {err}");
}
}
}
}
@ -185,7 +343,7 @@ impl Model {
/// extracted from controllers, the data are cached in this structure.
#[derive(Clone, Debug, Default)]
struct ViewUpdate {
state: Rc<state::State>,
state: Rc<State>,
nodes: Vec<controller::graph::Node>,
trees: HashMap<AstNodeId, controller::graph::NodeTrees>,
connections: HashSet<AstConnection>,
@ -194,12 +352,12 @@ struct ViewUpdate {
impl ViewUpdate {
/// Create ViewUpdate information from Graph Presenter's model.
fn new(model: &Model) -> FallibleResult<Self> {
let displayed = model.state.clone_ref();
let state = model.state.clone_ref();
let nodes = model.controller.graph().nodes()?;
let connections_and_trees = model.controller.connections()?;
let connections = connections_and_trees.connections.into_iter().collect();
let trees = connections_and_trees.trees;
Ok(Self { state: displayed, nodes, trees, connections })
Ok(Self { state, nodes, trees, connections })
}
/// Remove nodes from the state and return node views to be removed.
@ -236,7 +394,7 @@ impl ViewUpdate {
.iter()
.filter_map(|node| {
let id = node.main_line.id();
let position = node.position()?.vector;
let position = node.position().map(|p| p.vector)?;
let view_id =
self.state.update_from_controller().set_node_position(id, position)?;
Some((view_id, position))
@ -244,6 +402,16 @@ impl ViewUpdate {
.collect()
}
fn set_node_visualizations(&self) -> Vec<(ViewNodeId, Option<visualization_view::Path>)> {
self.nodes
.iter()
.filter_map(|node| {
let data = node.metadata.as_ref().map(|md| md.visualization.clone());
self.state.update_from_controller().set_node_visualization(node.id(), data)
})
.collect()
}
/// Remove connections from the state and return views to be removed.
fn remove_connections(&self) -> Vec<ViewConnection> {
self.state.update_from_controller().retain_connections(&self.connections)
@ -273,8 +441,8 @@ impl ViewUpdate {
/// The Graph Presenter, synchronizing graph state between graph controller and view.
///
/// This presenter focuses on the graph structure: nodes, their expressions and types, and
/// connections between them. It does not integrate Searcher nor Breadcrumbs - integration of
/// these is still to-be-delivered.
/// connections between them. It does not integrate Searcher nor Breadcrumbs (managed by
/// [`presenter::Searcher`] and [`presenter::CallStack`] respectively).
#[derive(Debug)]
pub struct Graph {
network: frp::Network,
@ -285,30 +453,33 @@ impl Graph {
/// Create graph presenter. The returned structure is working and does not require any
/// initialization.
pub fn new(
project: model::Project,
controller: controller::ExecutedGraph,
view: view::graph_editor::GraphEditor,
project_view: &view::project::View,
) -> Self {
let network = frp::Network::new("presenter::Graph");
let model = Rc::new(Model::new(controller, view));
Self { network, model }.init()
let view = project_view.graph().clone_ref();
let model = Rc::new(Model::new(project, controller, view));
Self { network, model }.init(project_view)
}
fn init(self) -> Self {
fn init(self, project_view: &view::project::View) -> Self {
let logger = &self.model.logger;
let network = &self.network;
let model = &self.model;
let view = &self.model.view.frp;
frp::extend! { network
update_view <- source::<()>();
update_data <- update_view.map(
f_!([logger,model] match ViewUpdate::new(&*model) {
Ok(update) => Rc::new(update),
Err(err) => {
error!(logger,"Failed to update view: {err:?}");
Rc::new(default())
}
})
);
// Position initialization should go before emitting `update_data` event.
update_with_gap <- view.default_y_gap_between_nodes.sample(&update_view);
eval update_with_gap ((gap) model.initialize_nodes_positions(*gap));
update_data <- update_view.map(f_!([logger,model] match ViewUpdate::new(&*model) {
Ok(update) => Rc::new(update),
Err(err) => {
error!(logger,"Failed to update view: {err:?}");
Rc::new(default())
}
}));
// === Refreshing Nodes ===
@ -316,9 +487,15 @@ impl Graph {
remove_node <= update_data.map(|update| update.remove_nodes());
update_node_expression <= update_data.map(|update| update.set_node_expressions());
set_node_position <= update_data.map(|update| update.set_node_positions());
set_node_visualization <= update_data.map(|update| update.set_node_visualizations());
enable_vis <- set_node_visualization.filter_map(|(id,path)| path.is_some().as_some(*id));
disable_vis <- set_node_visualization.filter_map(|(id,path)| path.is_none().as_some(*id));
view.remove_node <+ remove_node;
view.set_node_expression <+ update_node_expression;
view.set_node_position <+ set_node_position;
view.set_visualization <+ set_node_visualization;
view.enable_visualization <+ enable_vis;
view.disable_visualization <+ disable_vis;
view.add_node <+ update_data.map(|update| update.count_nodes_to_add()).repeat();
added_node_update <- view.node_added.filter_map(f!((view_id)
@ -327,6 +504,8 @@ impl Graph {
init_node_expression <- added_node_update.filter_map(|update| Some((update.view_id?, update.expression.clone())));
view.set_node_expression <+ init_node_expression;
view.set_node_position <+ added_node_update.filter_map(|update| Some((update.view_id?, update.position)));
view.set_visualization <+ added_node_update.filter_map(|update| Some((update.view_id?, Some(update.visualization.clone()?))));
view.enable_visualization <+ added_node_update.filter_map(|update| update.visualization.is_some().and_option(update.view_id));
// === Refreshing Connections ===
@ -349,6 +528,7 @@ impl Graph {
update_expression <= update_expressions;
view.set_expression_usage_type <+ update_expression.filter_map(f!((id) model.refresh_expression_type(*id)));
view.set_method_pointer <+ update_expression.filter_map(f!((id) model.refresh_expression_method_pointer(*id)));
view.set_node_error_status <+ update_expression.filter_map(f!((id) model.refresh_node_error(*id)));
// === Changes from the View ===
@ -357,8 +537,17 @@ impl Graph {
eval view.node_removed((node_id) model.node_removed(*node_id));
eval view.on_edge_endpoints_set((edge_id) model.new_connection_created(*edge_id));
eval view.on_edge_endpoint_unset(((edge_id,_)) model.connection_removed(*edge_id));
eval view.nodes_collapsed(((nodes, _)) model.nodes_collapsed(nodes));
eval view.enabled_visualization_path(((node_id, path)) model.node_visualization_changed(*node_id, path.clone()));
// === Dropping Files ===
file_upload_requested <- view.file_dropped.gate(&project_view.drop_files_enabled);
eval file_upload_requested (((file,position)) model.file_dropped(file.clone_ref(),*position));
}
view.remove_all_nodes();
update_view.emit(());
self.setup_controller_notification_handlers(update_view, update_expressions);
@ -373,7 +562,8 @@ impl Graph {
use crate::controller::graph::executed;
use crate::controller::graph::Notification;
let graph_notifications = self.model.controller.subscribe();
self.spawn_sync_stream_handler(graph_notifications, move |notification, model| {
let weak = Rc::downgrade(&self.model);
spawn_stream_handler(weak, graph_notifications, move |notification, model| {
info!(model.logger, "Received controller notification {notification:?}");
match notification {
executed::Notification::Graph(graph) => match graph {
@ -382,20 +572,43 @@ impl Graph {
},
executed::Notification::ComputedValueInfo(expressions) =>
update_expressions.emit(expressions),
executed::Notification::EnteredNode(_) => {}
executed::Notification::SteppedOutOfNode(_) => {}
executed::Notification::EnteredNode(_) => update_view.emit(()),
executed::Notification::SteppedOutOfNode(_) => update_view.emit(()),
}
})
}
fn spawn_sync_stream_handler<Stream, Function>(&self, stream: Stream, handler: Function)
where
Stream: StreamExt + Unpin + 'static,
Function: Fn(Stream::Item, Rc<Model>) + 'static, {
let model = Rc::downgrade(&self.model);
spawn_stream_handler(model, stream, move |item, model| {
handler(item, model);
futures::future::ready(())
std::future::ready(())
})
}
}
// === State Access ===
impl Graph {
/// Get the view id of given AST node.
pub fn view_id_of_ast_node(&self, id: AstNodeId) -> Option<ViewNodeId> {
self.model.state.view_id_of_ast_node(id)
}
/// Get the ast id of given node view.
pub fn ast_node_of_view(&self, id: ViewNodeId) -> Option<AstNodeId> {
self.model.state.ast_node_id_of_view(id)
}
/// Assign a node view to the given AST id. Since next update, the presenter will share the
/// node content between the controllers and the view.
pub fn assign_node_view_explicitly(&self, view_id: ViewNodeId, ast_id: AstNodeId) {
self.model.state.assign_node_view_explicitly(view_id, ast_id);
}
}
// ====================================
// === DataProvider for EnsoGL File ===
// ====================================
impl controller::upload::DataProvider for ensogl_drop_manager::File {
fn next_chunk(&mut self) -> LocalBoxFuture<FallibleResult<Option<Vec<u8>>>> {
self.read_chunk().map(|f| f.map_err(|e| e.into())).boxed_local()
}
}

View File

@ -0,0 +1,201 @@
//! The module with [`CallStack`] presenter. See [`crate::presenter`] documentation to know
//! more about presenters in general.
use crate::prelude::*;
use crate::executor::global::spawn_stream_handler;
use crate::model::execution_context::LocalCall;
use crate::presenter::graph::state::State;
use crate::presenter::graph::ViewNodeId;
use enso_frp as frp;
use ide_view as view;
// =============
// === Model ===
// =============
#[derive(Debug)]
struct Model {
logger: Logger,
controller: controller::ExecutedGraph,
view: view::graph_editor::GraphEditor,
state: Rc<State>,
}
impl Model {
fn new(
parent: impl AnyLogger,
controller: controller::ExecutedGraph,
view: view::graph_editor::GraphEditor,
state: Rc<State>,
) -> Self {
let logger = parent.sub("presenter::graph::CallStack");
Self { logger, controller, view, state }
}
fn expression_entered(&self, local_call: &view::graph_editor::LocalCall) {
let local_call = LocalCall {
definition: (**local_call.definition).clone(),
call: local_call.call,
};
self.enter_expression(local_call);
}
fn node_entered(&self, node_id: ViewNodeId) {
debug!(self.logger, "Requesting entering the node {node_id}.");
analytics::remote_log_event("integration::node_entered");
if let Some(call) = self.state.ast_node_id_of_view(node_id) {
match self.controller.node_method_pointer(call) {
Ok(method_pointer) => {
let definition = (*method_pointer).clone();
let local_call = LocalCall { call, definition };
self.enter_expression(local_call);
}
Err(_) =>
info!(self.logger, "Ignoring request to enter non-enterable node {call}."),
}
} else {
error!(self.logger, "Cannot enter {node_id:?}: no AST node bound to the view.");
}
}
fn node_exited(&self) {
debug!(self.logger, "Requesting exiting the current node.");
analytics::remote_log_event("integration::node_exited");
let controller = self.controller.clone_ref();
let logger = self.logger.clone_ref();
let store_stack = self.store_updated_stack_task();
executor::global::spawn(async move {
info!(logger, "Exiting node.");
match controller.exit_node().await {
Ok(()) =>
if let Err(err) = store_stack() {
// We cannot really do anything when updating metadata fails.
// Can happen in improbable case of serialization failure.
error!(logger, "Failed to store an updated call stack: {err}");
},
Err(err) => {
error!(logger, "Exiting node failed: {err}");
let event = "integration::exiting_node_failed";
let field = "error";
let data = analytics::AnonymousData(|| err.to_string());
analytics::remote_log_value(event, field, data)
}
}
});
}
fn enter_expression(&self, local_call: LocalCall) {
let controller = self.controller.clone_ref();
let logger = self.logger.clone_ref();
let store_stack = self.store_updated_stack_task();
executor::global::spawn(async move {
info!(logger, "Entering expression {local_call:?}.");
match controller.enter_method_pointer(&local_call).await {
Ok(()) =>
if let Err(err) = store_stack() {
// We cannot really do anything when updating metadata fails.
// Can happen in improbable case of serialization failure.
error!(logger, "Failed to store an updated call stack: {err}");
},
Err(err) => {
error!(logger, "Entering node failed: {err}.");
let event = "integration::entering_node_failed";
let field = "error";
let data = analytics::AnonymousData(|| err.to_string());
analytics::remote_log_value(event, field, data)
}
};
});
}
fn store_updated_stack_task(&self) -> impl FnOnce() -> FallibleResult + 'static {
let main_module = self.controller.graph().module.clone_ref();
let controller = self.controller.clone_ref();
move || {
let new_call_stack = controller.call_stack();
main_module.update_project_metadata(|metadata| {
metadata.call_stack = new_call_stack;
})
}
}
fn add_breadcrumb_in_view(&self, frame: LocalCall) {
let definition = frame.definition.clone().into();
let call = frame.call;
let local_call = view::graph_editor::LocalCall { call, definition };
self.view.model.breadcrumbs.push_breadcrumb(local_call);
}
}
// ======================
// === CallStack ===
// ======================
/// Call Stack presenter, synchronizing the call stack of currently displayed graph with
/// the breadcrumbs displayed above. It also handles the entering/exiting nodes requests.
#[derive(Debug)]
pub struct CallStack {
_network: frp::Network,
model: Rc<Model>,
}
impl CallStack {
/// Constructor. The returned presenter works right away.
pub fn new(
parent: impl AnyLogger,
controller: controller::ExecutedGraph,
view: view::graph_editor::GraphEditor,
state: Rc<State>,
) -> Self {
let network = frp::Network::new("presenter::graph::CallStack");
let model = Rc::new(Model::new(parent, controller, view, state));
let view = &model.view;
let breadcrumbs = &view.model.breadcrumbs;
frp::extend! { network
eval view.node_entered ((node) model.node_entered(*node));
eval_ view.node_exited (model.node_exited());
eval_ breadcrumbs.output.breadcrumb_pop(model.node_exited());
eval breadcrumbs.output.breadcrumb_push ([model](opt_local_call) {
if let Some(local_call) = opt_local_call {
model.expression_entered(local_call);
}
});
}
Self { _network: network, model }
.initialize_breadcrumbs()
.setup_controller_notification_handlers()
}
fn setup_controller_notification_handlers(self) -> Self {
use crate::controller::graph::executed::Notification;
let graph_notifications = self.model.controller.subscribe();
let weak = Rc::downgrade(&self.model);
spawn_stream_handler(weak, graph_notifications, move |notification, model| {
info!(model.logger, "Received controller notification {notification:?}");
match notification {
Notification::EnteredNode(frame) => model.add_breadcrumb_in_view(frame),
Notification::SteppedOutOfNode(_) => model.view.model.breadcrumbs.pop_breadcrumb(),
_ => {}
}
std::future::ready(())
});
self
}
fn initialize_breadcrumbs(self) -> Self {
for frame in self.model.controller.call_stack() {
self.model.add_breadcrumb_in_view(frame)
}
self
}
}

View File

@ -9,8 +9,10 @@ use crate::presenter::graph::ViewNodeId;
use bimap::BiMap;
use bimap::Overwritten;
use engine_protocol::language_server::ExpressionUpdatePayload;
use ide_view as view;
use ide_view::graph_editor::component::node as node_view;
use ide_view::graph_editor::component::visualization as visualization_view;
use ide_view::graph_editor::EdgeEndpoint;
@ -23,9 +25,11 @@ use ide_view::graph_editor::EdgeEndpoint;
#[allow(missing_docs)]
#[derive(Clone, Debug, Default)]
pub struct Node {
pub view_id: Option<ViewNodeId>,
pub position: Vector2,
pub expression: node_view::Expression,
pub view_id: Option<ViewNodeId>,
pub position: Vector2,
pub expression: node_view::Expression,
pub error: Option<node_view::Error>,
pub visualization: Option<visualization_view::Path>,
}
/// The set of node states.
@ -54,13 +58,25 @@ impl Nodes {
self.nodes.get_mut(&id)
}
/// Get id of AST corresponding with the node represented by given view.
pub fn ast_id_by_view(&self, id: ViewNodeId) -> Option<AstNodeId> {
self.ast_node_by_view_id.get(&id).copied()
}
/// Get the mutable reference, creating an default entry without view if it's missing.
///
/// The entry will be also present on the "nodes without view" list and may have view assigned
/// using [`assign_newly_created_node`] method.
pub fn get_mut_or_create(&mut self, id: AstNodeId) -> &mut Node {
let nodes_without_view = &mut self.nodes_without_view;
self.nodes.entry(id).or_insert_with(|| {
Self::get_mut_or_create_static(&mut self.nodes, &mut self.nodes_without_view, id)
}
fn get_mut_or_create_static<'a>(
nodes: &'a mut HashMap<AstNodeId, Node>,
nodes_without_view: &mut Vec<AstNodeId>,
id: AstNodeId,
) -> &'a mut Node {
nodes.entry(id).or_insert_with(|| {
nodes_without_view.push(id);
default()
})
@ -85,6 +101,25 @@ impl Nodes {
opt_displayed
}
/// Assign a node view to a concrete AST node. Returns the node state: the view must be
/// refreshed with the data from the state.
pub fn assign_node_view_explicitly(
&mut self,
view_id: ViewNodeId,
ast_id: AstNodeId,
) -> &mut Node {
let mut displayed =
Self::get_mut_or_create_static(&mut self.nodes, &mut self.nodes_without_view, ast_id);
if let Some(old_view) = displayed.view_id {
self.ast_node_by_view_id.remove(&old_view);
} else {
self.nodes_without_view.remove_item(&ast_id);
}
displayed.view_id = Some(view_id);
self.ast_node_by_view_id.insert(view_id, ast_id);
displayed
}
/// Update the state retaining given set of nodes. Returns the list of removed nodes' views.
pub fn retain_nodes(&mut self, nodes: &HashSet<AstNodeId>) -> Vec<ViewNodeId> {
self.nodes_without_view.drain_filter(|id| !nodes.contains(id));
@ -179,8 +214,11 @@ impl Connections {
/// A single expression data.
#[derive(Clone, Debug, Default)]
pub struct Expression {
/// A node whose line contains this expression.
pub node: AstNodeId,
/// The known type of the expression.
pub expression_type: Option<view::graph_editor::Type>,
/// A pointer to the method called by this expression.
pub method_pointer: Option<view::graph_editor::MethodPointer>,
}
@ -258,6 +296,11 @@ impl State {
self.nodes.borrow().get(node).and_then(|n| n.view_id)
}
/// Get node's AST id by the view id.
pub fn ast_node_id_of_view(&self, node: ViewNodeId) -> Option<AstNodeId> {
self.nodes.borrow().ast_id_of_view(node)
}
/// Convert the AST connection to pair of [`EdgeEndpoint`]s.
pub fn view_edge_targets_of_ast_connection(
&self,
@ -308,6 +351,12 @@ impl State {
pub fn assign_node_view(&self, view_id: ViewNodeId) -> Option<Node> {
self.nodes.borrow_mut().assign_newly_created_node(view_id).cloned()
}
/// Assign a node view to a concrete AST node. Returns the node state: the view must be
/// refreshed with the data from the state.
pub fn assign_node_view_explicitly(&self, view_id: ViewNodeId, ast_id: AstNodeId) -> Node {
self.nodes.borrow_mut().assign_node_view_explicitly(view_id, ast_id).clone()
}
}
// ========================
@ -378,6 +427,77 @@ impl<'a> ControllerChange<'a> {
None
}
}
/// Set the node error basing of the given expression's payload. If the error is actually
/// changed, the to-be-updated node view is returned with the proper error description. If the
/// expression is not a whole expression of any node, nothing is updated and `None` is returned.
pub fn set_node_error_from_payload(
&self,
expression: ast::Id,
payload: Option<ExpressionUpdatePayload>,
) -> Option<(ViewNodeId, Option<node_view::Error>)> {
let node_id = self.state.nodes.borrow().get(expression).is_some().as_some(expression)?;
let new_error = self.convert_payload_to_error(node_id, payload);
let mut nodes = self.nodes.borrow_mut();
let displayed = nodes.get_mut(node_id)?;
if displayed.error != new_error {
displayed.error = new_error.clone();
Some((displayed.view_id?, new_error))
} else {
None
}
}
fn convert_payload_to_error(
&self,
node_id: AstNodeId,
payload: Option<ExpressionUpdatePayload>,
) -> Option<node_view::error::Error> {
use node_view::error::Kind;
use ExpressionUpdatePayload::*;
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 nodes = self.nodes.borrow();
let root_cause = trace.iter().find(|id| nodes.get(**id).is_some());
!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);
let propagated = Immutable(propagated);
Some(node_view::error::Error { kind, message, propagated })
}
/// Set the node's attached visualization. The `visualization_data` should be the content of
/// `visualization` field in node's metadata. If the visualization actually changes, the
/// to-be-updated node view is returned with the deserialized visualization path.
pub fn set_node_visualization(
&self,
node_id: AstNodeId,
visualization_data: Option<serde_json::Value>,
) -> Option<(ViewNodeId, Option<visualization_view::Path>)> {
let controller_path = visualization_data.and_then(|data| {
// It is perfectly fine to ignore deserialization errors here. This is metadata, that
// might not even be initialized.
serde_json::from_value(data).ok()
});
let mut nodes = self.state.nodes.borrow_mut();
let displayed = nodes.get_mut_or_create(node_id);
if displayed.visualization != controller_path {
displayed.visualization = controller_path.clone();
Some((displayed.view_id?, controller_path))
} else {
None
}
}
}
@ -485,6 +605,24 @@ impl<'a> ViewChange<'a> {
pub fn remove_node(&self, id: ViewNodeId) -> Option<AstNodeId> {
self.nodes.borrow_mut().remove_node(id)
}
/// Set the new node visualization. If the visualization actually changes, the AST id of the
/// affected node is returned.
pub fn set_node_visualization(
&self,
id: ViewNodeId,
new_path: Option<visualization_view::Path>,
) -> Option<AstNodeId> {
let mut nodes = self.nodes.borrow_mut();
let ast_id = nodes.ast_id_of_view(id)?;
let displayed = nodes.get_mut(ast_id)?;
if displayed.visualization != new_path {
displayed.visualization = new_path;
Some(ast_id)
} else {
None
}
}
}

View File

@ -0,0 +1,312 @@
//! The module with the [`Visualization`] presenter. See [`crate::presenter`] documentation to know
//! more about presenters in general.
pub mod manager;
use crate::prelude::*;
use crate::presenter::graph;
use crate::presenter::graph::visualization::manager::Manager;
use crate::executor::global::spawn_stream_handler;
use crate::model::execution_context::VisualizationUpdateData;
use crate::presenter::graph::AstNodeId;
use crate::presenter::graph::ViewNodeId;
use enso_frp as frp;
use ide_view as view;
use ide_view::graph_editor::component::node as node_view;
use ide_view::graph_editor::component::visualization as visualization_view;
// =============
// === Model ===
// =============
#[derive(Clone, CloneRef, Debug)]
struct Model {
logger: Logger,
controller: controller::Visualization,
graph_view: view::graph_editor::GraphEditor,
manager: Rc<Manager>,
error_manager: Rc<Manager>,
state: Rc<graph::state::State>,
}
impl Model {
/// Handle the showing visualization UI.
fn visualization_shown(&self, node_id: ViewNodeId, metadata: visualization_view::Metadata) {
self.update_visualization(node_id, &self.manager, Some(metadata));
}
/// Handle the hiding in UI.
fn visualization_hidden(&self, node_id: view::graph_editor::NodeId) {
self.update_visualization(node_id, &self.manager, None);
}
/// Handle the node removal in UI.
fn node_removed(&self, node_id: view::graph_editor::NodeId) {
if self.state.ast_node_id_of_view(node_id).is_some() {
self.update_visualization(node_id, &self.manager, None);
self.update_visualization(node_id, &self.error_manager, None);
}
}
/// Handle the preprocessor change requested by visualization.
fn visualization_preprocessor_changed(
&self,
node_id: ViewNodeId,
preprocessor: visualization_view::instance::PreprocessorConfiguration,
) {
let metadata = visualization_view::Metadata { preprocessor };
self.update_visualization(node_id, &self.manager, Some(metadata))
}
/// Handle the error change on given node: attach/detach the error visualization if needed.
fn error_on_node_changed(&self, node_id: ViewNodeId, error: &Option<node_view::Error>) {
use view::graph_editor::builtin::visualization::native::error as error_visualization;
let error_kind = error.as_ref().map(|error| *error.kind);
let needs_error_vis = error_kind.contains(&node_view::error::Kind::Dataflow);
let metadata = needs_error_vis.then(error_visualization::metadata);
self.update_visualization(node_id, &self.error_manager, metadata);
}
/// Route the metadata description as a desired visualization state to the Manager.
fn update_visualization(
&self,
node_id: ViewNodeId,
manager: &Rc<Manager>,
metadata: Option<visualization_view::Metadata>,
) {
if let Some(target_id) = self.state.ast_node_id_of_view(node_id) {
manager.set_visualization(target_id, metadata);
} else {
error!(
self.logger,
"Failed to update visualization: {node_id:?} does not represent any AST code."
)
}
}
/// Pass the value update received from controllers to the Graph view appropriate endpoint.
///
/// The `update_endpoint` should be `set_visualization_data` or `set_error_visualization_data`,
/// of [`ide_view::graph_editor::GraphEditor`].
fn handle_value_update(
&self,
update_endpoint: &frp::Source<(ViewNodeId, visualization_view::Data)>,
target: AstNodeId,
data: VisualizationUpdateData,
) {
if let Some(view_id) = self.state.view_id_of_ast_node(target) {
match deserialize_visualization_data(data) {
Ok(data) => update_endpoint.emit((view_id, data)),
Err(err) => {
// TODO [mwu]: We should consider having the visualization also accept error
// input.
error!(self.logger, "Failed to deserialize visualization update: {err}");
}
}
}
}
/// Handle the visualization update failure by passing the affected view in the given FPR
/// endpoint.
fn handle_controller_failure(
&self,
failure_endpoint: &frp::Source<ViewNodeId>,
node: AstNodeId,
) {
if let Some(node_view) = self.state.view_id_of_ast_node(node) {
failure_endpoint.emit(node_view);
}
}
/// Load the available visualizations to the view.
///
/// See also [`controller::Visualization`] for information about loaded visualizations.
fn load_visualizations(&self) {
self.graph_view.reset_visualization_registry();
let logger = self.logger.clone_ref();
let controller = self.controller.clone_ref();
let graph_editor = self.graph_view.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.");
});
}
}
// =====================
// === Visualization ===
// =====================
/// Visualization Presenter, synchronizing the visualization attached in the Engine with the
/// visualization shown in the view, including the error visualizations.
#[derive(Debug)]
pub struct Visualization {
_network: frp::Network,
model: Rc<Model>,
}
impl Visualization {
/// Constructor. The returned structure is does not require any further initialization.
pub fn new(
project: model::Project,
graph: controller::ExecutedGraph,
view: view::graph_editor::GraphEditor,
state: Rc<graph::state::State>,
) -> Self {
let logger = Logger::new("presenter::graph::Visualization");
let network = frp::Network::new("presenter::graph::Visualization");
let controller = project.visualization().clone_ref();
let (manager, notifications) =
Manager::new(&logger, graph.clone_ref(), project.clone_ref());
let (error_manager, error_notifications) =
Manager::new(&logger, graph.clone_ref(), project);
let model = Rc::new(Model {
logger,
controller,
graph_view: view.clone_ref(),
manager: manager.clone_ref(),
error_manager: error_manager.clone_ref(),
state,
});
frp::extend! { network
eval view.visualization_shown (((node, metadata)) model.visualization_shown(*node, metadata.clone()));
eval view.visualization_hidden ((node) model.node_removed(*node));
eval view.node_removed ((node) model.visualization_hidden(*node));
eval view.visualization_preprocessor_changed (((node, preprocessor)) model.visualization_preprocessor_changed(*node, preprocessor.clone_ref()));
eval view.set_node_error_status (((node, error)) model.error_on_node_changed(*node, error));
update <- source::<(ViewNodeId, visualization_view::Data)>();
error_update <- source::<(ViewNodeId, visualization_view::Data)>();
visualization_failure <- source::<ViewNodeId>();
error_vis_failure <- source::<ViewNodeId>();
view.set_visualization_data <+ update;
view.set_error_visualization_data <+ error_update;
view.disable_visualization <+ visualization_failure;
eval_ view.visualization_registry_reload_requested (model.load_visualizations());
}
Self { model, _network: network }
.spawn_visualization_handler(notifications, manager, update, visualization_failure)
.spawn_visualization_handler(
error_notifications,
error_manager,
error_update,
error_vis_failure,
)
.setup_graph_listener(graph)
}
fn spawn_visualization_handler(
self,
notifier: impl Stream<Item = manager::Notification> + Unpin + 'static,
manager: Rc<Manager>,
update_endpoint: frp::Source<(ViewNodeId, visualization_view::Data)>,
failure_endpoint: frp::Source<ViewNodeId>,
) -> Self {
let weak = Rc::downgrade(&self.model);
spawn_stream_handler(weak, notifier, move |notification, model| {
let logger = &model.logger;
info!(logger, "Received update for visualization: {notification:?}");
match notification {
manager::Notification::ValueUpdate { target, data, .. } => {
model.handle_value_update(&update_endpoint, target, data);
}
manager::Notification::FailedToAttach { visualization, error } => {
error!(logger, "Visualization {visualization.id} failed to attach: {error}.");
model.handle_controller_failure(&failure_endpoint, visualization.expression_id);
}
manager::Notification::FailedToDetach { visualization, error } => {
error!(logger, "Visualization {visualization.id} failed to detach: {error}.");
// Here we cannot really do much. Failing to detach might mean that
// visualization was already detached, that we detached it
// but failed to observe this (e.g. due to a connectivity
// issue) or that we did something really wrong. For now, we
// will just forget about this visualization. Better to unlikely "leak"
// it rather than likely break visualizations on the node altogether.
let forgotten = manager.forget_visualization(visualization.expression_id);
if let Some(forgotten) = forgotten {
error!(logger, "The visualization will be forgotten: {forgotten:?}")
}
}
manager::Notification::FailedToModify { desired, error } => {
error!(
logger,
"Visualization {desired.id} failed to be modified: {error}. Will hide it in GUI."
);
// Actually it would likely have more sense if we had just restored the previous
// visualization, as its LS state should be preserved. However, we already
// scrapped it on the GUI side and we don't even know its
// path anymore.
model.handle_controller_failure(&failure_endpoint, desired.expression_id);
}
}
std::future::ready(())
});
self
}
fn setup_graph_listener(self, graph_controller: controller::ExecutedGraph) -> Self {
use controller::graph;
use controller::graph::executed::Notification;
let notifications = graph_controller.subscribe();
let weak = Rc::downgrade(&self.model);
spawn_stream_handler(weak, notifications, move |notification, model| {
match notification {
Notification::Graph(graph::Notification::Invalidate)
| Notification::EnteredNode(_)
| Notification::SteppedOutOfNode(_) => match graph_controller.graph().nodes() {
Ok(nodes) => {
let nodes_set = nodes.into_iter().map(|n| n.id()).collect();
model.manager.retain_visualizations(&nodes_set);
model.error_manager.retain_visualizations(&nodes_set);
}
Err(err) => {
error!(
model.logger,
"Cannot update visualization after graph change: {err}"
);
}
},
_ => {}
}
std::future::ready(())
});
self
}
}
// ========================
// === Helper Functions ===
// ========================
fn deserialize_visualization_data(
data: VisualizationUpdateData,
) -> FallibleResult<visualization_view::Data> {
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_view::Data::from(as_json))
}

View File

@ -1,4 +1,4 @@
//! Utilities facilitating integration for visualizations.
//! A module containing helpers for attaching, detaching and updating visualizations in controllers.
use crate::prelude::*;
@ -16,6 +16,8 @@ use ide_view::graph_editor::component::visualization::instance::ContextModule;
use ide_view::graph_editor::component::visualization::Metadata;
use ide_view::graph_editor::SharedHashMap;
// ================================
// === Resolving Context Module ===
// ================================
@ -233,10 +235,11 @@ impl Manager {
/// Return a handle to the Manager and the receiver for notifications.
/// Note that receiver cannot be re-retrieved or changed in the future.
pub fn new(
logger: Logger,
logger: impl AnyLogger,
executed_graph: ExecutedGraph,
project: model::Project,
) -> (Rc<Self>, UnboundedReceiver<Notification>) {
let logger = logger.sub("visualization::Manager");
let (notification_sender, notification_receiver) = futures::channel::mpsc::unbounded();
let ret = Self {
logger,
@ -507,6 +510,14 @@ impl Manager {
task().await;
});
}
/// Remove (set desired state to None) each visualization not attached to any of the `targets`.
pub fn retain_visualizations(self: &Rc<Self>, targets: &HashSet<ast::Id>) {
let to_remove = self.visualizations.keys().into_iter().filter(|id| !targets.contains(id));
for target in to_remove {
self.set_visualization(target, None);
}
}
}
@ -619,11 +630,9 @@ mod tests {
inner.project.clone_ref(),
execution_context,
);
let (manager, notifier) = Manager::new(
inner.logger.sub("manager"),
executed_graph.clone_ref(),
inner.project.clone_ref(),
);
let logger: Logger = inner.logger.sub("manager");
let (manager, notifier) =
Manager::new(logger, executed_graph.clone_ref(), inner.project.clone_ref());
Self { inner, is_ready, manager, notifier, requests }
}
}

View File

@ -5,10 +5,12 @@ use crate::prelude::*;
use crate::presenter;
use crate::executor::global::spawn_stream_handler;
use crate::presenter::graph::ViewNodeId;
use enso_frp as frp;
use ide_view as view;
// =============
// === Model ===
// =============
@ -17,9 +19,130 @@ use ide_view as view;
#[allow(unused)]
#[derive(Debug)]
struct Model {
controller: controller::Project,
view: view::project::View,
graph: presenter::Graph,
logger: Logger,
controller: controller::Project,
module_model: model::Module,
graph_controller: controller::ExecutedGraph,
ide_controller: controller::Ide,
view: view::project::View,
status_bar: view::status_bar::View,
graph: presenter::Graph,
code: presenter::Code,
searcher: RefCell<Option<presenter::Searcher>>,
}
impl Model {
fn new(
ide_controller: controller::Ide,
controller: controller::Project,
init_result: controller::project::InitializationResult,
view: view::project::View,
status_bar: view::status_bar::View,
) -> Self {
let logger = Logger::new("presenter::Project");
let graph_controller = init_result.main_graph;
let text_controller = init_result.main_module_text;
let module_model = init_result.main_module_model;
let graph = presenter::Graph::new(
controller.model.clone_ref(),
graph_controller.clone_ref(),
&view,
);
let code = presenter::Code::new(text_controller, &view);
let searcher = default();
Model {
logger,
controller,
module_model,
graph_controller,
ide_controller,
view,
status_bar,
graph,
code,
searcher,
}
}
fn setup_searcher_presenter(&self, node_view: ViewNodeId) {
let new_presenter = presenter::Searcher::setup_controller(
&self.logger,
self.ide_controller.clone_ref(),
self.controller.clone_ref(),
self.graph_controller.clone_ref(),
&self.graph,
self.view.clone_ref(),
node_view,
);
match new_presenter {
Ok(searcher) => {
*self.searcher.borrow_mut() = Some(searcher);
}
Err(err) => {
error!(self.logger, "Error while creating searcher integration: {err}");
}
}
}
fn editing_committed(
&self,
node: ViewNodeId,
entry_id: Option<view::searcher::entry::Id>,
) -> bool {
let searcher = self.searcher.take();
if let Some(searcher) = searcher {
let is_example = entry_id.map_or(false, |i| searcher.is_entry_an_example(i));
if let Some(created_node) = searcher.commit_editing(entry_id) {
self.graph.assign_node_view_explicitly(node, created_node);
if is_example {
self.view.graph().enable_visualization(node);
}
false
} else {
true
}
} else {
false
}
}
fn editing_aborted(&self) {
let searcher = self.searcher.take();
if let Some(searcher) = searcher {
searcher.abort_editing();
} else {
warning!(self.logger, "Editing aborted without searcher controller.");
}
}
fn rename_project(&self, name: impl Str) {
if self.controller.model.name() != name.as_ref() {
let project = self.controller.model.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 {
error!(logger, "The project couldn't be renamed: {e}");
breadcrumbs.cancel_project_name_editing.emit(());
}
});
}
}
fn undo(&self) {
debug!(self.logger, "Undo triggered in UI.");
if let Err(e) = self.controller.model.urm().undo() {
error!(self.logger, "Undo failed: {e}");
}
}
fn redo(&self) {
debug!(self.logger, "Redo triggered in UI.");
if let Err(e) = self.controller.model.urm().redo() {
error!(self.logger, "Redo failed: {e}");
}
}
}
@ -31,7 +154,8 @@ struct Model {
/// The Project Presenter, synchronizing state between project controller and project view.
#[derive(Clone, CloneRef, Debug)]
pub struct Project {
model: Rc<Model>,
network: frp::Network,
model: Rc<Model>,
}
impl Project {
@ -40,14 +164,106 @@ impl Project {
/// The returned presenter will be already working: it will display the initial main graph, and
/// react to all notifications.
pub fn new(
ide_controller: controller::Ide,
controller: controller::Project,
init_result: controller::project::InitializationResult,
view: view::project::View,
status_bar: view::status_bar::View,
) -> Self {
let graph_controller = init_result.main_graph;
let graph = presenter::Graph::new(graph_controller, view.graph().clone_ref());
let model = Model { controller, view, graph };
Self { model: Rc::new(model) }
let network = frp::Network::new("presenter::Project");
let model = Model::new(ide_controller, controller, init_result, view, status_bar);
Self { network, model: Rc::new(model) }.init()
}
fn init(self) -> Self {
let model = &self.model;
let network = &self.network;
let view = &model.view.frp;
let breadcrumbs = &model.view.graph().model.breadcrumbs;
let graph_view = &model.view.graph().frp;
frp::extend! { network
searcher_input <- view.searcher_input.filter_map(|view| *view);
eval searcher_input ((node_view) {
model.setup_searcher_presenter(*node_view)
});
graph_view.remove_node <+ view.editing_committed.filter_map(f!([model]((node_view, entry)) {
model.editing_committed(*node_view, *entry).as_some(*node_view)
}));
eval_ view.editing_aborted(model.editing_aborted());
eval breadcrumbs.output.project_name((name) {model.rename_project(name);});
eval_ view.undo (model.undo());
eval_ view.redo (model.redo());
values_computed <- source::<()>();
values_computed_first_time <- values_computed.constant(true).on_change().constant(());
view.show_prompt <+ values_computed_first_time;
}
let graph_controller = self.model.graph_controller.clone_ref();
self.init_analytics()
.setup_notification_handler()
.attach_frp_to_values_computed_notifications(graph_controller, values_computed)
}
fn init_analytics(self) -> Self {
let network = &self.network;
let project = &self.model.view;
let graph = self.model.view.graph();
let searcher = self.model.view.searcher();
frp::extend! { network
eval_ graph.node_editing_started([]analytics::remote_log_event("graph_editor::node_editing_started"));
eval_ graph.node_editing_finished([]analytics::remote_log_event("graph_editor::node_editing_finished"));
eval_ graph.node_added([]analytics::remote_log_event("graph_editor::node_added"));
eval_ graph.node_removed([]analytics::remote_log_event("graph_editor::node_removed"));
eval_ graph.nodes_collapsed([]analytics::remote_log_event("graph_editor::nodes_collapsed"));
eval_ graph.node_entered([]analytics::remote_log_event("graph_editor::node_enter_request"));
eval_ graph.node_exited([]analytics::remote_log_event("graph_editor::node_exit_request"));
eval_ graph.on_edge_endpoints_set([]analytics::remote_log_event("graph_editor::edge_endpoints_set"));
eval_ graph.visualization_shown([]analytics::remote_log_event("graph_editor::visualization_shown"));
eval_ graph.visualization_hidden([]analytics::remote_log_event("graph_editor::visualization_hidden"));
eval_ graph.on_edge_endpoint_unset([]analytics::remote_log_event("graph_editor::connection_removed"));
eval_ searcher.used_as_suggestion([]analytics::remote_log_event("searcher::used_as_suggestion"));
eval_ project.editing_committed([]analytics::remote_log_event("project::editing_committed"));
}
self
}
fn setup_notification_handler(self) -> Self {
let notifications = self.model.controller.model.subscribe();
let weak = Rc::downgrade(&self.model);
spawn_stream_handler(weak, notifications, |notification, model| {
info!(model.logger, "Processing notification {notification:?}");
let message = match notification {
model::project::Notification::ConnectionLost(_) =>
crate::BACKEND_DISCONNECTED_MESSAGE,
};
let message = view::status_bar::event::Label::from(message);
model.status_bar.add_event(message);
std::future::ready(())
});
self
}
fn attach_frp_to_values_computed_notifications(
self,
graph: controller::ExecutedGraph,
values_computed: frp::Source<()>,
) -> Self {
let weak = Rc::downgrade(&self.model);
let notifications = graph.subscribe();
spawn_stream_handler(weak, notifications, move |notification, _| {
if let controller::graph::executed::Notification::ComputedValueInfo(_) = notification {
values_computed.emit(());
}
std::future::ready(())
});
self
}
/// Initialize project and return working presenter.
@ -55,10 +271,12 @@ impl Project {
/// This calls the [`controller::Project::initialize`] method and use the initialization result
/// to construct working presenter.
pub async fn initialize(
ide_controller: controller::Ide,
controller: controller::Project,
view: view::project::View,
status_bar: view::status_bar::View,
) -> FallibleResult<Self> {
let init_result = controller.initialize().await?;
Ok(Self::new(controller, init_result, view))
Ok(Self::new(ide_controller, controller, init_result, view, status_bar))
}
}

View File

@ -0,0 +1,204 @@
//! The module containing [`Searcher`] presenter. See [`crate::presenter`] documentation to know
//! more about presenters in general.
pub mod provider;
use crate::prelude::*;
use crate::controller::searcher::Notification;
use crate::controller::searcher::UserAction;
use crate::executor::global::spawn_stream_handler;
use crate::presenter;
use crate::presenter::graph::AstNodeId;
use crate::presenter::graph::ViewNodeId;
use enso_frp as frp;
use ide_view as view;
use ide_view::graph_editor::component::node as node_view;
// =============
// === Model ===
// =============
#[derive(Clone, CloneRef, Debug)]
struct Model {
logger: Logger,
controller: controller::Searcher,
view: view::project::View,
input_view: ViewNodeId,
}
impl Model {
fn new(
parent: impl AnyLogger,
controller: controller::Searcher,
view: view::project::View,
input_view: ViewNodeId,
) -> Self {
let logger = parent.sub("presenter::Searcher");
Self { logger, controller, view, input_view }
}
fn input_changed(&self, new_input: &str) {
if let Err(err) = self.controller.set_input(new_input.to_owned()) {
error!(self.logger, "Error while setting new searcher input: {err}");
}
}
fn entry_used_as_suggestion(
&self,
entry_id: view::searcher::entry::Id,
) -> Option<(ViewNodeId, node_view::Expression)> {
match self.controller.use_as_suggestion(entry_id) {
Ok(new_code) => {
let new_code_and_trees = node_view::Expression::new_plain(new_code);
Some((self.input_view, new_code_and_trees))
}
Err(err) => {
error!(self.logger, "Error while applying suggestion: {err}");
None
}
}
}
fn commit_editing(&self, entry_id: Option<view::searcher::entry::Id>) -> Option<AstNodeId> {
let result = match entry_id {
Some(id) => self.controller.execute_action_by_index(id),
None => self.controller.commit_node().map(Some),
};
result.unwrap_or_else(|err| {
error!(self.logger, "Error while executing action: {err}");
None
})
}
fn create_providers(&self) -> provider::Any {
provider::create_providers_from_controller(&self.logger, &self.controller)
}
fn should_auto_select_first_action(&self) -> bool {
let user_action = self.controller.current_user_action();
let list_not_empty = matches!(self.controller.actions(), controller::searcher::Actions::Loaded {list} if list.matching_count() > 0);
// Usually we don't want to select first entry and display docs when user finished typing
// function or argument.
let starting_typing = user_action == UserAction::StartingTypingArgument;
!starting_typing && list_not_empty
}
}
/// The Searcher presenter, synchronizing state between searcher view and searcher controller.
///
/// The presenter should be created for one instantiated searcher controller (when node starts to
/// being edited). Alternatively, the [`setup_controller`] method covers constructing the controller
/// and the presenter.
#[derive(Debug)]
pub struct Searcher {
_network: frp::Network,
model: Rc<Model>,
}
impl Searcher {
/// Constructor. The returned structure works rigth away.
pub fn new(
parent: impl AnyLogger,
controller: controller::Searcher,
view: view::project::View,
input_view: ViewNodeId,
) -> Self {
let model = Rc::new(Model::new(parent, controller, view, input_view));
let network = frp::Network::new("presenter::Searcher");
let graph = &model.view.graph().frp;
let searcher = &model.view.searcher().frp;
frp::extend! { network
eval graph.node_expression_set ([model]((changed_node, expr)) {
if *changed_node == input_view {
model.input_changed(expr);
}
});
action_list_changed <- source::<()>();
new_providers <- action_list_changed.map(f_!(model.create_providers()));
searcher.set_actions <+ new_providers;
select_entry <- action_list_changed.filter(f_!(model.should_auto_select_first_action()));
searcher.select_action <+ select_entry.constant(0);
used_as_suggestion <- searcher.used_as_suggestion.filter_map(|entry| *entry);
new_input <- used_as_suggestion.filter_map(f!((e) model.entry_used_as_suggestion(*e)));
graph.set_node_expression <+ new_input;
}
let weak_model = Rc::downgrade(&model);
let notifications = model.controller.subscribe();
spawn_stream_handler(weak_model, notifications, move |notification, _| {
match notification {
Notification::NewActionList => action_list_changed.emit(()),
};
std::future::ready(())
});
Self { model, _network: network }
}
/// Setup new, appropriate searcher controller for the edition of `node_view`, and construct
/// presenter handling it.
pub fn setup_controller(
parent: impl AnyLogger,
ide_controller: controller::Ide,
project_controller: controller::Project,
graph_controller: controller::ExecutedGraph,
graph_presenter: &presenter::Graph,
view: view::project::View,
node_view: ViewNodeId,
) -> FallibleResult<Self> {
let ast_node = graph_presenter.ast_node_of_view(node_view);
let mode = match ast_node {
Some(node_id) => controller::searcher::Mode::EditNode { node_id },
None => {
let view_data = view.graph().model.nodes.get_cloned_ref(&node_view);
let position = view_data.map(|node| node.position().xy());
let position = position.map(|vector| model::module::Position { vector });
controller::searcher::Mode::NewNode { position }
}
};
let selected_views = view.graph().model.nodes.all_selected();
let selected_nodes =
selected_views.iter().filter_map(|view| graph_presenter.ast_node_of_view(*view));
let searcher_controller = controller::Searcher::new_from_graph_controller(
&parent,
ide_controller,
&project_controller.model,
graph_controller,
mode,
selected_nodes.collect(),
)?;
Ok(Self::new(parent, searcher_controller, view, node_view))
}
/// Commit editing.
///
/// This method takes `self`, as the presenter (with the searcher view) should be dropped once
/// editing finishes. The `entry_id` might be none in case where the searcher should accept
/// the node input without any entry selected. If the commitment will result in creating a new
/// node, its AST id is returned.
pub fn commit_editing(self, entry_id: Option<view::searcher::entry::Id>) -> Option<AstNodeId> {
self.model.commit_editing(entry_id)
}
/// Abort editing, without taking any action.
///
/// This method takes `self`, as the presenter (with the searcher view) should be dropped once
/// editing finishes.
pub fn abort_editing(self) {}
/// Returns true if the entry under given index is one of the examples.
pub fn is_entry_an_example(&self, entry: view::searcher::entry::Id) -> bool {
use crate::controller::searcher::action::Action::Example;
let controller = &self.model.controller;
let entry = controller.actions().list().and_then(|l| l.get_cloned(entry));
entry.map_or(false, |e| matches!(e.action, Example(_)))
}
}

View File

@ -0,0 +1,149 @@
//! A module with the providers for searcher view, taking content from the Action List controller.
use crate::prelude::*;
use crate::controller::searcher::action::MatchInfo;
use crate::model::suggestion_database;
use ensogl_component::list_view;
use ensogl_component::list_view::entry::GlyphHighlightedLabel;
use ide_view as view;
// ============================
// === Any Provider Helpers ===
// ============================
/// Wrappers for some instance of the structure being both entry and documentation provider.
pub type Any = (
list_view::entry::AnyModelProvider<GlyphHighlightedLabel>,
view::searcher::AnyDocumentationProvider,
);
/// Create providers from the current controller's action list.
pub fn create_providers_from_controller(logger: &Logger, controller: &controller::Searcher) -> Any {
use controller::searcher::Actions;
match controller.actions() {
Actions::Loading => as_any(Rc::new(list_view::entry::EmptyProvider)),
Actions::Loaded { list } => {
let user_action = controller.current_user_action();
let intended_function = controller.intended_function_suggestion();
let provider = Action { actions: list, user_action, intended_function };
as_any(Rc::new(provider))
}
Actions::Error(err) => {
error!(logger, "Error while obtaining searcher action list: {err}");
as_any(Rc::new(list_view::entry::EmptyProvider))
}
}
}
fn as_any<P>(provider: Rc<P>) -> Any
where P: list_view::entry::ModelProvider<view::searcher::Entry>
+ view::searcher::DocumentationProvider
+ 'static {
(provider.clone_ref().into(), provider.into())
}
// ========================
// === provider::Action ===
// ========================
/// An searcher actions provider, based on the action list retrieved from the searcher controller.
#[derive(Clone, Debug)]
pub struct Action {
actions: Rc<controller::searcher::action::List>,
user_action: controller::searcher::UserAction,
intended_function: Option<controller::searcher::action::Suggestion>,
}
impl Action {
fn doc_placeholder_for(suggestion: &controller::searcher::action::Suggestion) -> String {
use controller::searcher::action::Suggestion;
let code = match suggestion {
Suggestion::FromDatabase(suggestion) => {
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",
suggestion_database::entry::Kind::Module => "Module",
};
let code = suggestion.code_to_insert(None, true).code;
format!("{} `{}`\n\nNo documentation available", title, code)
}
Suggestion::Hardcoded(suggestion) => {
format!("{}\n\nNo documentation available", suggestion.name)
}
};
let parser = parser::DocParser::new();
match parser {
Ok(p) => {
let output = p.generate_html_doc_pure((*code).to_string());
output.unwrap_or(code)
}
Err(_) => code,
}
}
}
impl list_view::entry::ModelProvider<GlyphHighlightedLabel> for Action {
fn entry_count(&self) -> usize {
// TODO[ao] Because of "All Search Results" category, the actions on list are duplicated.
// But we don't want to display duplicates on the old searcher list. To be fixed/removed
// once new searcher GUI will be implemented
// (https://github.com/enso-org/ide/issues/1681)
self.actions.matching_count() / 2
}
fn get(&self, id: usize) -> Option<list_view::entry::GlyphHighlightedLabelModel> {
let action = self.actions.get_cloned(id)?;
if let MatchInfo::Matches { subsequence } = action.match_info {
let label = action.action.to_string();
let mut char_iter = label.char_indices().enumerate();
let highlighted = 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 = enso_text::unit::Bytes(byte_id as i32);
let end = enso_text::unit::Bytes((byte_id + char.len_utf8()) as i32);
break Some(enso_text::Range::new(start, end));
}
} else {
break None;
}
})
.collect();
Some(list_view::entry::GlyphHighlightedLabelModel { label, highlighted })
} else {
None
}
}
}
impl ide_view::searcher::DocumentationProvider for Action {
fn get(&self) -> Option<String> {
use controller::searcher::UserAction::*;
self.intended_function.as_ref().and_then(|function| match self.user_action {
StartingTypingArgument => function.documentation_html().map(ToOwned::to_owned),
_ => None,
})
}
fn get_for_entry(&self, id: usize) -> Option<String> {
use controller::searcher::action::Action;
match self.actions.get_cloned(id)?.action {
Action::Suggestion(suggestion) => {
let doc = suggestion.documentation_html().map(ToOwned::to_owned);
Some(doc.unwrap_or_else(|| Self::doc_placeholder_for(&suggestion)))
}
Action::Example(example) => Some(example.documentation_html.clone()),
Action::ProjectManagement(_) => None,
}
}
}

View File

@ -29,7 +29,7 @@ pub enum Kind {
}
/// Additional error information (beside the error value itself) for some erroneous node.
#[derive(Clone, CloneRef, Debug)]
#[derive(Clone, CloneRef, Debug, Eq, PartialEq)]
#[allow(missing_docs)]
pub struct Error {
pub kind: Immutable<Kind>,

View File

@ -816,7 +816,6 @@ class Config {
public authentication_enabled: boolean
public email: string
public application_config_url: string
public rust_new_presentation_layer: boolean
static default() {
let config = new Config()
@ -879,9 +878,6 @@ class Config {
this.application_config_url = ok(other.application_config_url)
? tryAsString(other.application_config_url)
: this.application_config_url
this.rust_new_presentation_layer = ok(other.rust_new_presentation_layer)
? tryAsBoolean(other.rust_new_presentation_layer)
: this.rust_new_presentation_layer
}
}

View File

@ -163,6 +163,9 @@ commands.build.rust = async function (argv) {
if (argv.dev) {
args.push('--dev')
}
if (cargoArgs) {
args.push('--')
}
await run_cargo('wasm-pack', args)
await patch_file(paths.dist.wasm.glue, js_workaround_patcher)
await fs.rename(paths.dist.wasm.mainRaw, paths.dist.wasm.main)

View File

@ -486,8 +486,10 @@ impl Area {
// === Changes ===
self.frp.source.changed <+ m.buffer.frp.text_change;
// The `content` event should be fired first, as any listener for `changed` may want to
// read the new content, so it should be up-to-date.
self.frp.source.content <+ m.buffer.frp.text_change.map(f_!(m.buffer.text()));
self.frp.source.changed <+ m.buffer.frp.text_change;
}
self
}