Skip to content

Commit

Permalink
Show spinner when opening/creating a project, take #2 (#6827)
Browse files Browse the repository at this point in the history
* Remove unused code: project management in component browser

* Encapsulate internal FRP logic of project list

* Collapse some code paths

* Open project passed on command line through presenter

A project name or ID that is passed on the command line was initialised
in the controller setup, before the presenters and views are set up.
Now, we fully initialise the IDE before opening a project so we have
control over the view while a project is being opened.

* Show a spinner in all cases of opening a project

* Let root presenter open/close projects when switching projects

* Change spinner to make progress over a fixed period

* Resolve issues when Project Manager API isn't available

* Bump wasm size limit
  • Loading branch information
Procrat committed May 26, 2023
1 parent e7ee2ca commit 0ed78f9
Show file tree
Hide file tree
Showing 18 changed files with 280 additions and 326 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@
dashboard.][6474]
- [Keyboard shortcuts for graph editing are now disabled when the full-screen
visualization is active.][6844]
- [A loading animation is now shown when opening and creating projects][6827],
as the previous behaviour of showing a blank screen while the project was
being loaded was potentially confusing to users.

[6279]: https://github.com/enso-org/enso/pull/6279
[6421]: https://github.com/enso-org/enso/pull/6421
Expand All @@ -187,6 +190,7 @@
[6719]: https://github.com/enso-org/enso/pull/6719
[6474]: https://github.com/enso-org/enso/pull/6474
[6844]: https://github.com/enso-org/enso/pull/6844
[6827]: https://github.com/enso-org/enso/pull/6827

#### EnsoGL (rendering engine)

Expand Down
70 changes: 54 additions & 16 deletions app/gui/src/controller/ide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

use crate::prelude::*;

use crate::config::ProjectToOpen;

use double_representation::name::project;
use mockall::automock;
use parser::Parser;
Expand Down Expand Up @@ -102,9 +104,7 @@ impl StatusNotificationPublisher {
/// used internally in code.
#[derive(Copy, Clone, Debug)]
pub enum Notification {
/// User created a new project. The new project is opened in IDE.
NewProjectCreated,
/// User opened an existing project.
/// User opened a new or existing project.
ProjectOpened,
/// User closed the project.
ProjectClosed,
Expand All @@ -118,10 +118,12 @@ pub enum Notification {

// === Errors ===

#[allow(missing_docs)]
/// Error raised when a project with given name or ID was not found.
#[derive(Clone, Debug, Fail)]
#[fail(display = "Project with name \"{}\" not found.", 0)]
struct ProjectNotFound(String);
#[fail(display = "Project '{}' was not found.", project)]
pub struct ProjectNotFound {
project: ProjectToOpen,
}


// === Managing API ===
Expand All @@ -131,11 +133,16 @@ struct ProjectNotFound(String);
/// It is a separate trait, because those methods are not supported in some environments (see also
/// [`API::manage_projects`]).
pub trait ManagingProjectAPI {
/// Create a new unnamed project and open it in the IDE.
/// Create a new project and open it in the IDE.
///
/// `name` is an optional project name. It overrides the name of the template if given.
/// `template` is an optional project template name. Available template names are defined in
/// `lib/scala/pkg/src/main/scala/org/enso/pkg/Template.scala`.
fn create_new_project(&self, template: Option<project::Template>) -> BoxFuture<FallibleResult>;
fn create_new_project(
&self,
name: Option<String>,
template: Option<project::Template>,
) -> BoxFuture<FallibleResult>;

/// Return a list of existing projects.
fn list_projects(&self) -> BoxFuture<FallibleResult<Vec<ProjectMetadata>>>;
Expand All @@ -150,18 +157,44 @@ pub trait ManagingProjectAPI {
/// 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())
let project_id = self.find_project(&ProjectToOpen::Name(name.into())).await?;
self.open_project(project_id).await
}
.boxed_local()
}

/// Open a project by name or ID. If no project with the given name exists, it will be created.
fn open_or_create_project(&self, project_to_open: ProjectToOpen) -> BoxFuture<FallibleResult> {
async move {
match self.find_project(&project_to_open).await {
Ok(project_id) => self.open_project(project_id).await,
Err(error) =>
if let ProjectToOpen::Name(name) = project_to_open {
info!("Attempting to create project with name '{name}'.");
self.create_new_project(Some(name.to_string()), None).await
} else {
Err(error)
},
}
}
.boxed_local()
}

/// Find a project by name or ID.
fn find_project<'a: 'c, 'b: 'c, 'c>(
&'a self,
project_to_open: &'b ProjectToOpen,
) -> BoxFuture<'c, FallibleResult<Uuid>> {
async move {
self.list_projects()
.await?
.into_iter()
.find(|project_metadata| project_to_open.matches(project_metadata))
.map(|metadata| metadata.id)
.ok_or_else(|| ProjectNotFound { project: project_to_open.clone() }.into())
}
.boxed_local()
}
}


Expand Down Expand Up @@ -193,6 +226,11 @@ pub trait API: Debug {
#[allow(clippy::needless_lifetimes)]
fn manage_projects<'a>(&'a self) -> FallibleResult<&'a dyn ManagingProjectAPI>;

/// Returns whether the Managing Project API is available.
fn can_manage_projects(&self) -> bool {
self.manage_projects().is_ok()
}

/// Return whether private entries should be visible in the component browser.
fn are_component_browser_private_entries_visible(&self) -> bool;

Expand Down
74 changes: 16 additions & 58 deletions app/gui/src/controller/ide/desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

use crate::prelude::*;

use crate::config::ProjectToOpen;
use crate::controller::ide::ManagingProjectAPI;
use crate::controller::ide::Notification;
use crate::controller::ide::StatusNotificationPublisher;
use crate::controller::ide::API;
use crate::ide::initializer;

use double_representation::name::project;
use engine_protocol::project_manager;
Expand Down Expand Up @@ -49,53 +47,16 @@ pub struct Handle {
}

impl Handle {
/// Create IDE controller. If `maybe_project_name` is `Some`, a project with provided name will
/// be opened. Otherwise controller will be used for project manager operations by Welcome
/// Screen.
pub async fn new(
project_manager: Rc<dyn project_manager::API>,
project_to_open: Option<ProjectToOpen>,
) -> FallibleResult<Self> {
let project = match project_to_open {
Some(project_to_open) =>
Some(Self::init_project_model(project_manager.clone_ref(), project_to_open).await?),
None => None,
};
Ok(Self::new_with_project_model(project_manager, project))
}

/// Create IDE controller with prepared project model. If `project` is `None`,
/// `API::current_project` returns `None` as well.
pub fn new_with_project_model(
project_manager: Rc<dyn project_manager::API>,
project: Option<model::Project>,
) -> Self {
let current_project = Rc::new(CloneCell::new(project));
let status_notifications = default();
let parser = Parser::new();
let notifications = default();
let component_browser_private_entries_visibility_flag = default();
Self {
current_project,
/// Create IDE controller.
pub fn new(project_manager: Rc<dyn project_manager::API>) -> FallibleResult<Self> {
Ok(Self {
current_project: default(),
project_manager,
status_notifications,
parser,
notifications,
component_browser_private_entries_visibility_flag,
}
}

/// Open project with provided name.
async fn init_project_model(
project_manager: Rc<dyn project_manager::API>,
project_to_open: ProjectToOpen,
) -> FallibleResult<model::Project> {
// TODO[ao]: Reuse of initializer used in previous code design. It should be soon replaced
// anyway, because we will soon resign from the "open or create" approach when opening
// IDE. See https://github.com/enso-org/ide/issues/1492 for details.
let initializer = initializer::WithProjectManager::new(project_manager, project_to_open);
let model = initializer.initialize_project_model().await?;
Ok(model)
status_notifications: default(),
parser: default(),
notifications: default(),
component_browser_private_entries_visibility_flag: default(),
})
}
}

Expand Down Expand Up @@ -133,14 +94,16 @@ impl API for Handle {

impl ManagingProjectAPI for Handle {
#[profile(Objective)]
fn create_new_project(&self, template: Option<project::Template>) -> BoxFuture<FallibleResult> {
fn create_new_project(
&self,
name: Option<String>,
template: Option<project::Template>,
) -> BoxFuture<FallibleResult> {
async move {
use model::project::Synchronized as Project;

let list = self.project_manager.list_projects(&None).await?;
let existing_names: HashSet<_> =
list.projects.into_iter().map(|p| p.name.into()).collect();
let name = make_project_name(&template);
let name = name.unwrap_or_else(|| make_project_name(&template));
let name = choose_unique_project_name(&existing_names, &name);
let name = ProjectName::new_unchecked(name);
let version = &enso_config::ARGS.groups.engine.options.preferred_version.value;
Expand All @@ -151,12 +114,7 @@ impl ManagingProjectAPI for Handle {
.project_manager
.create_project(&name, &template.map(|t| t.into()), &version, &action)
.await?;
let new_project_id = create_result.project_id;
let project_mgr = self.project_manager.clone_ref();
let new_project = Project::new_opened(project_mgr, new_project_id);
self.current_project.set(Some(new_project.await?));
self.notifications.notify(Notification::NewProjectCreated);
Ok(())
self.open_project(create_result.project_id).await
}
.boxed_local()
}
Expand Down
33 changes: 0 additions & 33 deletions app/gui/src/controller/searcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,33 +694,6 @@ impl Searcher {
Mode::NewNode { .. } => self.add_example(&example).map(Some),
_ => Err(CannotExecuteWhenEditingNode.into()),
},
Action::ProjectManagement(action) => {
match self.ide.manage_projects() {
Ok(_) => {
let ide = self.ide.clone_ref();
executor::global::spawn(async move {
// We checked that manage_projects returns Some just a moment ago, so
// unwrapping is safe.
let manage_projects = ide.manage_projects().unwrap();
let result = match action {
action::ProjectManagement::CreateNewProject =>
manage_projects.create_new_project(None),
action::ProjectManagement::OpenProject { id, .. } =>
manage_projects.open_project(*id),
};
if let Err(err) = result.await {
error!("Error when creating new project: {err}");
}
});
Ok(None)
}
Err(err) => Err(NotSupported {
action_label: Action::ProjectManagement(action).to_string(),
reason: err,
}
.into()),
}
}
}
}

Expand Down Expand Up @@ -1017,12 +990,6 @@ impl Searcher {
let mut actions = action::ListWithSearchResultBuilder::new();
let (libraries_icon, default_icon) =
action::hardcoded::ICONS.with(|i| (i.libraries.clone_ref(), i.default.clone_ref()));
if should_add_additional_entries && self.ide.manage_projects().is_ok() {
let mut root_cat = actions.add_root_category("Projects", default_icon.clone_ref());
let category = root_cat.add_category("Projects", default_icon.clone_ref());
let create_project = action::ProjectManagement::CreateNewProject;
category.add_action(Action::ProjectManagement(create_project));
}
let mut libraries_root_cat =
actions.add_root_category("Libraries", libraries_icon.clone_ref());
if should_add_additional_entries {
Expand Down
14 changes: 0 additions & 14 deletions app/gui/src/controller/searcher/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,6 @@ impl Suggestion {
/// Action of adding example code.
pub type Example = Rc<model::suggestion_database::Example>;

/// A variants of project management actions. See also [`Action`].
#[allow(missing_docs)]
#[derive(Clone, CloneRef, Debug, Eq, PartialEq)]
pub enum ProjectManagement {
CreateNewProject,
OpenProject { id: Immutable<Uuid>, name: ImString },
}

/// A single action on the Searcher list. See also `controller::searcher::Searcher` docs.
#[derive(Clone, CloneRef, Debug, PartialEq)]
pub enum Action {
Expand All @@ -85,8 +77,6 @@ pub enum Action {
/// Add to the current module a new function with example code, and a new node in
/// current scene calling that function.
Example(Example),
/// The project management operation: creating or opening, projects.
ProjectManagement(ProjectManagement),
// In the future, other action types will be added (like module/method management, etc.).
}

Expand All @@ -102,10 +92,6 @@ impl Display for Action {
Self::Suggestion(Suggestion::Hardcoded(suggestion)) =>
Display::fmt(&suggestion.name, f),
Self::Example(example) => write!(f, "Example: {}", example.name),
Self::ProjectManagement(ProjectManagement::CreateNewProject) =>
write!(f, "New Project"),
Self::ProjectManagement(ProjectManagement::OpenProject { name, .. }) =>
Display::fmt(name, f),
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion app/gui/src/ide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::prelude::*;

use crate::config::ProjectToOpen;
use crate::presenter::Presenter;

use analytics::AnonymousData;
Expand Down Expand Up @@ -90,6 +91,11 @@ impl Ide {
}
}
}

/// Open a project by name or ID. If no project with the given name exists, it will be created.
pub fn open_or_create_project(&self, project: ProjectToOpen) {
self.presenter.open_or_create_project(project)
}
}

/// A reduced version of [`Ide`] structure, representing an application which failed to initialize.
Expand All @@ -101,7 +107,6 @@ pub struct FailedIde {
pub view: ide_view::root::View,
}


/// The Path of the module initially opened after opening project in IDE.
pub fn initial_module_path(project: &model::Project) -> model::module::Path {
project.main_module_path()
Expand Down

0 comments on commit 0ed78f9

Please sign in to comment.