mirror of
https://github.com/enso-org/enso.git
synced 2024-11-23 08:08:34 +03:00
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:
parent
2676aa50a3
commit
e8077253c9
@ -56,6 +56,5 @@ ensogl::read_args! {
|
||||
authentication_enabled : bool,
|
||||
email : String,
|
||||
application_config_url : String,
|
||||
rust_new_presentation_layer : bool,
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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
@ -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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
124
app/gui/src/presenter/code.rs
Normal file
124
app/gui/src/presenter/code.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
201
app/gui/src/presenter/graph/call_stack.rs
Normal file
201
app/gui/src/presenter/graph/call_stack.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
312
app/gui/src/presenter/graph/visualization.rs
Normal file
312
app/gui/src/presenter/graph/visualization.rs
Normal 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))
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
204
app/gui/src/presenter/searcher.rs
Normal file
204
app/gui/src/presenter/searcher.rs
Normal 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(_)))
|
||||
}
|
||||
}
|
149
app/gui/src/presenter/searcher/provider.rs
Normal file
149
app/gui/src/presenter/searcher/provider.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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>,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user