Revert "Show spinner when opening/creating a project, take #2 (#6827)" (#7229)

This commit is contained in:
Adam Obuchowicz 2023-07-14 17:00:52 +02:00 committed by GitHub
parent f691713077
commit 853dd2f455
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 339 additions and 259 deletions

View File

@ -5,8 +5,6 @@
use crate::prelude::*;
use crate::config::ProjectToOpen;
use double_representation::name::project;
use mockall::automock;
use parser::Parser;
@ -104,7 +102,9 @@ impl StatusNotificationPublisher {
/// used internally in code.
#[derive(Copy, Clone, Debug)]
pub enum Notification {
/// User opened a new or existing project.
/// User created a new project. The new project is opened in IDE.
NewProjectCreated,
/// User opened an existing project.
ProjectOpened,
/// User closed the project.
ProjectClosed,
@ -118,12 +118,10 @@ pub enum Notification {
// === Errors ===
/// Error raised when a project with given name or ID was not found.
#[allow(missing_docs)]
#[derive(Clone, Debug, Fail)]
#[fail(display = "Project '{}' was not found.", project)]
pub struct ProjectNotFound {
project: ProjectToOpen,
}
#[fail(display = "Project with name \"{}\" not found.", 0)]
struct ProjectNotFound(String);
// === Managing API ===
@ -133,16 +131,11 @@ pub struct ProjectNotFound {
/// 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 project and open it in the IDE.
/// Create a new unnamed 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,
name: Option<String>,
template: Option<project::Template>,
) -> BoxFuture<FallibleResult>;
fn create_new_project(&self, template: Option<project::Template>) -> BoxFuture<FallibleResult>;
/// Return a list of existing projects.
fn list_projects(&self) -> BoxFuture<FallibleResult<Vec<ProjectMetadata>>>;
@ -157,44 +150,18 @@ pub trait ManagingProjectAPI {
/// and then for the project opening.
fn open_project_by_name(&self, name: String) -> BoxFuture<FallibleResult> {
async move {
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)
},
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()
}
/// 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()
}
}
@ -226,11 +193,6 @@ 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;

View File

@ -4,10 +4,12 @@
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;
@ -47,16 +49,53 @@ pub struct Handle {
}
impl Handle {
/// Create IDE controller.
pub fn new(project_manager: Rc<dyn project_manager::API>) -> FallibleResult<Self> {
Ok(Self {
current_project: default(),
/// 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,
project_manager,
status_notifications: default(),
parser: default(),
notifications: default(),
component_browser_private_entries_visibility_flag: default(),
})
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)
}
}
@ -94,16 +133,14 @@ impl API for Handle {
impl ManagingProjectAPI for Handle {
#[profile(Objective)]
fn create_new_project(
&self,
name: Option<String>,
template: Option<project::Template>,
) -> BoxFuture<FallibleResult> {
fn create_new_project(&self, 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 = name.unwrap_or_else(|| make_project_name(&template));
let name = 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;
@ -114,7 +151,12 @@ impl ManagingProjectAPI for Handle {
.project_manager
.create_project(&name, &template.map(|t| t.into()), &version, &action)
.await?;
self.open_project(create_result.project_id).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(())
}
.boxed_local()
}

View File

@ -10,8 +10,8 @@ use crate::controller::searcher::component::group;
use crate::model::module::NodeEditStatus;
use crate::model::module::NodeMetadata;
use crate::model::suggestion_database;
use crate::presenter::searcher;
use breadcrumbs::Breadcrumbs;
use double_representation::graph::GraphInfo;
use double_representation::graph::LocationHint;
@ -712,6 +712,33 @@ 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()),
}
}
}
}
@ -949,6 +976,12 @@ 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 {

View File

@ -67,6 +67,14 @@ 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 {
@ -77,6 +85,8 @@ 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.).
}
@ -92,6 +102,10 @@ 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),
}
}
}

View File

@ -2,7 +2,6 @@
use crate::prelude::*;
use crate::config::ProjectToOpen;
use crate::presenter::Presenter;
use analytics::AnonymousData;
@ -94,11 +93,6 @@ 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.
@ -110,6 +104,7 @@ 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()

View File

@ -3,14 +3,17 @@
use crate::prelude::*;
use crate::config;
use crate::config::ProjectToOpen;
use crate::ide::Ide;
use crate::retry::retry_operation;
use crate::transport::web::WebSocket;
use crate::FailedIde;
use engine_protocol::project_manager;
use engine_protocol::project_manager::ProjectName;
use ensogl::application::Application;
use std::time::Duration;
use uuid::Uuid;
@ -32,6 +35,19 @@ const INITIALIZATION_RETRY_TIMES: &[Duration] =
// ==============
// === Errors ===
// ==============
/// Error raised when project with given name was not found.
#[derive(Clone, Debug, Fail)]
#[fail(display = "Project '{}' was not found.", name)]
pub struct ProjectNotFound {
name: ProjectToOpen,
}
// ===================
// === Initializer ===
// ===================
@ -80,13 +96,7 @@ impl Initializer {
match self.initialize_ide_controller_with_retries().await {
Ok(controller) => {
let can_manage_projects = controller.can_manage_projects();
let ide = Ide::new(ensogl_app, view, controller);
if can_manage_projects {
if let Some(project) = &self.config.project_to_open {
ide.open_or_create_project(project.clone());
}
}
info!("IDE was successfully initialized.");
Ok(ide)
}
@ -116,8 +126,9 @@ impl Initializer {
match &self.config.backend {
ProjectManager { endpoint } => {
let project_manager = self.setup_project_manager(endpoint).await?;
let controller = controller::ide::Desktop::new(project_manager)?;
Ok(Rc::new(controller))
let project_to_open = self.config.project_to_open.clone();
let controller = controller::ide::Desktop::new(project_manager, project_to_open);
Ok(Rc::new(controller.await?))
}
LanguageServer { json_endpoint, binary_endpoint, namespace, project_name } => {
let json_endpoint = json_endpoint.clone();
@ -157,6 +168,81 @@ impl Initializer {
// ==========================
// === WithProjectManager ===
// ==========================
/// Ide Initializer with project manager.
///
/// This structure do the specific initialization part when we are connected to Project Manager,
/// like list projects, find the one we want to open, open it, or create new one if it does not
/// exist.
#[allow(missing_docs)]
#[derive(Clone, Derivative)]
#[derivative(Debug)]
pub struct WithProjectManager {
#[derivative(Debug = "ignore")]
pub project_manager: Rc<dyn project_manager::API>,
pub project_to_open: ProjectToOpen,
}
impl WithProjectManager {
/// Constructor.
pub fn new(
project_manager: Rc<dyn project_manager::API>,
project_to_open: ProjectToOpen,
) -> Self {
Self { project_manager, project_to_open }
}
/// Create and initialize a new Project Model, for a project with name passed in constructor.
///
/// If the project with given name does not exist yet, it will be created.
pub async fn initialize_project_model(self) -> FallibleResult<model::Project> {
let project_id = self.get_project_or_create_new().await?;
let project_manager = self.project_manager;
model::project::Synchronized::new_opened(project_manager, project_id).await
}
/// Creates a new project and returns its id, so the newly connected project can be opened.
pub async fn create_project(&self, project_name: &ProjectName) -> FallibleResult<Uuid> {
use project_manager::MissingComponentAction::Install;
info!("Creating a new project named '{}'.", project_name);
let version = &enso_config::ARGS.groups.engine.options.preferred_version.value;
let version = (!version.is_empty()).as_some_from(|| version.clone());
let response = self.project_manager.create_project(project_name, &None, &version, &Install);
Ok(response.await?.project_id)
}
async fn lookup_project(&self) -> FallibleResult<Uuid> {
let response = self.project_manager.list_projects(&None).await?;
let mut projects = response.projects.iter();
projects
.find(|project_metadata| self.project_to_open.matches(project_metadata))
.map(|md| md.id)
.ok_or_else(|| ProjectNotFound { name: self.project_to_open.clone() }.into())
}
/// Look for the project with the name specified when constructing this initializer,
/// or, if it does not exist, create it. The id of found/created project is returned.
pub async fn get_project_or_create_new(&self) -> FallibleResult<Uuid> {
let project = self.lookup_project().await;
if let Ok(project_id) = project {
Ok(project_id)
} else if let ProjectToOpen::Name(name) = &self.project_to_open {
info!("Attempting to create {}", name);
self.create_project(name).await
} else {
// This can happen only if we are told to open project by id but it cannot be found.
// We cannot fallback to creating a new project in this case, as we cannot create a
// project with a given id. Thus, we simply propagate the lookup result.
project
}
}
}
// =============
// === Utils ===
// =============
@ -201,10 +287,6 @@ pub fn register_views(app: &Application) {
mod test {
use super::*;
use crate::config::ProjectToOpen;
use crate::controller::ide::ManagingProjectAPI;
use crate::engine_protocol::project_manager::ProjectName;
use json_rpc::expect_call;
use wasm_bindgen_test::wasm_bindgen_test;
@ -228,10 +310,9 @@ mod test {
expect_call!(mock_client.list_projects(count) => Ok(project_lists));
let project_manager = Rc::new(mock_client);
let ide_controller = controller::ide::Desktop::new(project_manager).unwrap();
let project_to_open = ProjectToOpen::Name(project_name);
let project_id =
ide_controller.find_project(&project_to_open).await.expect("Couldn't get project.");
assert_eq!(project_id, expected_id);
let initializer = WithProjectManager { project_manager, project_to_open };
let project = initializer.get_project_or_create_new().await;
assert_eq!(expected_id, project.expect("Couldn't get project."))
}
}

View File

@ -83,10 +83,7 @@ impl Fixture {
let project_management =
controller.manage_projects().expect("Cannot access Managing Project API");
project_management
.create_new_project(None, None)
.await
.expect("Failed to create new project");
project_management.create_new_project(None).await.expect("Failed to create new project");
}
/// After returning, the IDE is in a state with the project opened and ready to work

View File

@ -4,16 +4,13 @@
use crate::prelude::*;
use crate::config::ProjectToOpen;
use crate::controller::ide::StatusNotification;
use crate::executor::global::spawn_stream_handler;
use crate::presenter;
use enso_frp as frp;
use ensogl::system::js;
use ide_view as view;
use ide_view::graph_editor::SharedHashMap;
use std::time::Duration;
// ==============
@ -32,19 +29,6 @@ pub use searcher::component_browser::ComponentBrowserSearcher;
// =================
// === Constants ===
// =================
/// We don't know how long opening the project will take, but we still want to show a fake
/// progress indicator for the user. This constant represents how long the spinner will run for in
/// milliseconds.
const OPEN_PROJECT_SPINNER_TIME_MS: u64 = 5_000;
/// The interval in milliseconds at which we should increase the spinner
const OPEN_PROJECT_SPINNER_UPDATE_PERIOD_MS: u64 = 10;
// =============
// === Model ===
// =============
@ -110,15 +94,15 @@ impl Model {
#[profile(Task)]
pub fn open_project(&self, project_name: String) {
let controller = self.controller.clone_ref();
crate::executor::global::spawn(with_progress_indicator(|| async move {
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!("Cannot open project by name: {err}.");
}
} else {
warn!("Project Manager API not available, cannot open project.");
warn!("Project opening failed: no ProjectManagingAPI available.");
}
}));
});
}
/// Create a new project. `template` is an optional name of the project template passed to the
@ -129,10 +113,9 @@ impl Model {
if let Ok(template) =
template.map(double_representation::name::project::Template::from_text).transpose()
{
crate::executor::global::spawn(with_progress_indicator(|| async move {
crate::executor::global::spawn(async move {
if let Ok(managing_api) = controller.manage_projects() {
if let Err(err) = managing_api.create_new_project(None, template.clone()).await
{
if let Err(err) = managing_api.create_new_project(template.clone()).await {
if let Some(template) = template {
error!("Could not create new project from template {template}: {err}.");
} else {
@ -140,66 +123,13 @@ impl Model {
}
}
} else {
warn!("Project Manager API not available, cannot create project.");
warn!("Project creation failed: no ProjectManagingAPI available.");
}
}))
})
} else if let Some(template) = template {
error!("Invalid project template name: {template}");
};
}
/// Open a project by name or ID. If no project with the given name exists, it will be created.
#[profile(Task)]
fn open_or_create_project(&self, project: ProjectToOpen) {
let controller = self.controller.clone_ref();
crate::executor::global::spawn(with_progress_indicator(|| async move {
if let Ok(managing_api) = controller.manage_projects() {
if let Err(error) = managing_api.open_or_create_project(project).await {
error!("Cannot open or create project. {error}");
}
} else {
warn!("Project Manager API not available, cannot open or create project.");
}
}));
}
}
/// Show a full-screen spinner for the exact duration of the specified function.
async fn with_progress_indicator<F, T>(f: F)
where
F: FnOnce() -> T,
T: Future<Output = ()>, {
// TODO[ss]: Use a safer variant of getting the JS app. This one gets a variable from JS, casts
// it to a type, etc. Somewhere in EnsoGL we might already have some logic for getting the JS
// app and throwing an error if it's not defined.
let Ok(app) = js::app() else { return error!("Failed to get JavaScript EnsoGL app.") };
app.show_progress_indicator(0.0);
let (finished_tx, finished_rx) = futures::channel::oneshot::channel();
let spinner_progress = futures::stream::unfold(0, |time| async move {
enso_web::sleep(Duration::from_millis(OPEN_PROJECT_SPINNER_UPDATE_PERIOD_MS)).await;
let new_time = time + OPEN_PROJECT_SPINNER_UPDATE_PERIOD_MS;
if new_time < OPEN_PROJECT_SPINNER_TIME_MS {
let progress = new_time as f32 / OPEN_PROJECT_SPINNER_TIME_MS as f32;
Some((progress, new_time))
} else {
None
}
})
.take_until(finished_rx);
executor::global::spawn(spinner_progress.for_each(|progress| async move {
let Ok(app) = js::app() else { return error!("Failed to get JavaScript EnsoGL app.") };
app.show_progress_indicator(progress);
}));
f().await;
// This fails when the spinner progressed until the end before the function got completed
// and therefore the receiver got dropped, so we'll ignore the result.
let _ = finished_tx.send(());
let Ok(app) = js::app() else { return error!("Failed to get JavaScript EnsoGL app.") };
app.hide_progress_indicator();
}
@ -235,13 +165,6 @@ impl Presenter {
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(());
eval root_frp.selected_project ([model] (project) {
if let Some(project) = project {
model.close_project();
model.open_project(project.name.to_string());
}
});
}
Self { model, network }.init()
@ -251,8 +174,8 @@ impl Presenter {
fn init(self) -> Self {
self.setup_status_bar_notification_handler();
self.setup_controller_notification_handler();
executor::global::spawn(self.clone_ref().set_projects_list_on_welcome_screen());
self.model.clone_ref().setup_and_display_new_project();
executor::global::spawn(self.clone_ref().set_projects_list_on_welcome_screen());
self
}
@ -291,7 +214,8 @@ impl Presenter {
let weak = Rc::downgrade(&self.model);
spawn_stream_handler(weak, stream, move |notification, model| {
match notification {
controller::ide::Notification::ProjectOpened =>
controller::ide::Notification::NewProjectCreated
| controller::ide::Notification::ProjectOpened =>
model.setup_and_display_new_project(),
controller::ide::Notification::ProjectClosed => {
model.close_project();
@ -315,11 +239,6 @@ impl Presenter {
}
}
}
/// 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.model.open_or_create_project(project)
}
}

View File

@ -11,8 +11,8 @@ use crate::presenter::searcher::SearcherPresenter;
use crate::presenter::ComponentBrowserSearcher;
use engine_protocol::language_server::ExecutionEnvironment;
use engine_protocol::project_manager::ProjectMetadata;
use enso_frp as frp;
use ensogl::system::js;
use ide_view as view;
use ide_view::project::SearcherParams;
use ide_view::project::SearcherType;
@ -22,6 +22,16 @@ use model::project::VcsStatus;
// =================
// === Constants ===
// =================
/// We don't know how long the project opening will take, but we still want to show a fake progress
/// indicator for the user. This constant represents a progress percentage that will be displayed.
const OPEN_PROJECT_SPINNER_PROGRESS: f32 = 0.8;
// =============
// === Model ===
// =============
@ -39,7 +49,7 @@ struct Model {
graph: presenter::Graph,
code: presenter::Code,
searcher: RefCell<Option<Box<dyn SearcherPresenter>>>,
available_projects: Rc<RefCell<Vec<ProjectMetadata>>>,
available_projects: Rc<RefCell<Vec<(ImString, Uuid)>>>,
shortcut_transaction: RefCell<Option<Rc<model::undo_redo::Transaction>>>,
}
@ -275,6 +285,8 @@ impl Model {
executor::global::spawn(async move {
if let Ok(api) = controller.manage_projects() {
if let Ok(projects) = api.list_projects().await {
let projects = projects.into_iter();
let projects = projects.map(|p| (p.name.clone().into(), p.id)).collect_vec();
*projects_list.borrow_mut() = projects;
project_list_ready.emit(());
}
@ -282,6 +294,36 @@ impl Model {
})
}
/// User clicked a project in the Open Project dialog. Open it.
fn open_project(&self, id_in_list: &usize) {
let controller = self.ide_controller.clone_ref();
let projects_list = self.available_projects.clone_ref();
let view = self.view.clone_ref();
let status_bar = self.status_bar.clone_ref();
let id = *id_in_list;
executor::global::spawn(async move {
let app = js::app_or_panic();
app.show_progress_indicator(OPEN_PROJECT_SPINNER_PROGRESS);
view.hide_graph_editor();
if let Ok(api) = controller.manage_projects() {
api.close_project();
let uuid = projects_list.borrow().get(id).map(|(_name, uuid)| *uuid);
if let Some(uuid) = uuid {
if let Err(error) = api.open_project(uuid).await {
error!("Error opening project: {error}.");
status_bar.add_event(format!("Error opening project: {error}."));
}
} else {
error!("Project with id {id} not found.");
}
} else {
error!("Project Manager API not available, cannot open project.");
}
app.hide_progress_indicator();
view.show_graph_editor();
})
}
fn execution_environment_changed(
&self,
execution_environment: ide_view::execution_environment_selector::ExecutionEnvironment,
@ -355,15 +397,28 @@ impl Project {
let view = &model.view.frp;
let breadcrumbs = &model.view.graph().model.breadcrumbs;
let graph_view = &model.view.graph().frp;
let project_list = &model.view.project_list().frp;
let project_list = &model.view.project_list();
frp::extend! { network
project_list_ready <- source_();
project_list.project_list <+ project_list_ready.map(
f_!(model.available_projects.borrow().clone())
);
project_list.grid.reset_entries <+ project_list_ready.map(f_!([model]{
let cols = 1;
let rows = model.available_projects.borrow().len();
(rows, cols)
}));
entry_model <- project_list.grid.model_for_entry_needed.map(f!([model]((row, col)) {
let projects = model.available_projects.borrow();
let project = projects.get(*row);
project.map(|(name, _)| (*row, *col, name.clone_ref()))
})).filter_map(|t| t.clone());
project_list.grid.model_for_entry <+ entry_model;
open_project_list <- view.project_list_shown.on_true();
eval_ open_project_list (model.project_list_opened(project_list_ready.clone_ref()));
eval_ open_project_list(model.project_list_opened(project_list_ready.clone_ref()));
selected_project <- project_list.grid.entry_selected.filter_map(|e| *e);
eval selected_project(((row, _col)) model.open_project(row));
project_list.grid.select_entry <+ selected_project.constant(None);
eval view.searcher ([model](params) {
if let Some(params) = params {

View File

@ -18,10 +18,16 @@ use ide_view::graph_editor::NodeId;
use ide_view::project::SearcherParams;
use ide_view::project::SearcherType;
// ==============
// === Export ===
// ==============
pub mod ai;
pub mod component_browser;
/// Trait for the searcher.
pub trait SearcherPresenter: Debug {
/// Initiate the operating mode for the searcher based on the given [`SearcherParams`]. If the

View File

@ -13,8 +13,8 @@ use crate::presenter;
use crate::presenter::graph::AstNodeId;
use crate::presenter::graph::ViewNodeId;
use crate::presenter::searcher::component_browser::provider::ControllerComponentsProviderExt;
use crate::presenter::searcher::SearcherPresenter;
use enso_frp as frp;
use enso_suggestion_database::documentation_ir::EntryDocumentation;
use enso_suggestion_database::documentation_ir::Placeholder;
@ -35,6 +35,7 @@ use ide_view::project::SearcherParams;
pub mod provider;
// ==============
// === Errors ===
// ==============

View File

@ -133,6 +133,7 @@ impl ide_view::searcher::DocumentationProvider for Action {
Some(doc.unwrap_or_else(|| Self::doc_placeholder_for(&suggestion)))
}
Action::Example(example) => Some(example.documentation_html.clone()),
Action::ProjectManagement(_) => None,
}
}
}

View File

@ -1,10 +1,11 @@
use super::prelude::*;
use crate::controller::ide;
use crate::controller::ide::ManagingProjectAPI;
use crate::config::ProjectToOpen;
use crate::ide;
use crate::transport::test_utils::TestWithMockedTransport;
use engine_protocol::project_manager;
use engine_protocol::project_manager::ProjectName;
use json_rpc::test_util::transport::mock::MockTransport;
use serde_json::json;
use wasm_bindgen_test::wasm_bindgen_test;
@ -27,8 +28,11 @@ fn failure_to_open_project_is_reported() {
fixture.run_test(async move {
let project_manager = Rc::new(project_manager::Client::new(transport));
executor::global::spawn(project_manager.runner());
let ide_controller = ide::Desktop::new(project_manager).unwrap();
let result = ide_controller.create_new_project(None, None).await;
let name = ProjectName::new_unchecked(crate::constants::DEFAULT_PROJECT_NAME.to_owned());
let project_to_open = ProjectToOpen::Name(name);
let initializer =
ide::initializer::WithProjectManager::new(project_manager, project_to_open);
let result = initializer.initialize_project_model().await;
result.expect_err("Error should have been reported.");
});
fixture.when_stalled_send_response(json!({

View File

@ -105,6 +105,10 @@ ensogl::define_endpoints! {
hide_project_list(),
/// Close the searcher without taking any actions
close_searcher(),
/// Show the graph editor.
show_graph_editor(),
/// Hide the graph editor.
hide_graph_editor(),
/// Simulates a style toggle press event.
toggle_style(),
/// Toggles the visibility of private components in the component browser.
@ -296,6 +300,14 @@ impl Model {
fn hide_project_list(&self) {
self.display_object.remove_child(&*self.project_list);
}
fn show_graph_editor(&self) {
self.display_object.add_child(&*self.graph_editor);
}
fn hide_graph_editor(&self) {
self.display_object.remove_child(&*self.graph_editor);
}
}
@ -428,6 +440,9 @@ impl View {
let documentation = &searcher.model().documentation;
frp::extend! { network
eval_ frp.show_graph_editor(model.show_graph_editor());
eval_ frp.hide_graph_editor(model.hide_graph_editor());
// We block graph navigator if it interferes with other panels (searcher, documentation,
// etc.)
searcher_active <- searcher.is_hovered || documentation.frp.is_selected;
@ -657,7 +672,7 @@ impl View {
let project_list = &model.project_list;
frp::extend! { network
eval_ frp.show_project_list (model.show_project_list());
project_chosen <- project_list.frp.selected_project.constant(());
project_chosen <- project_list.grid.entry_selected.constant(());
mouse_down <- scene.mouse.frp_deprecated.down.constant(());
clicked_on_bg <- mouse_down.filter(f_!(scene.mouse.target.get().is_background()));
should_be_closed <- any(frp.hide_project_list,project_chosen,clicked_on_bg);

View File

@ -4,7 +4,6 @@
use crate::prelude::*;
use ensogl::display::shape::*;
use engine_protocol::project_manager::ProjectMetadata;
use enso_frp as frp;
use ensogl::application::frp::API;
use ensogl::application::Application;
@ -196,23 +195,6 @@ mod background {
// ===========
// === FRP ===
// ===========
ensogl::define_endpoints! {
Input {
/// This is a list of projects to choose from.
project_list (Vec<ProjectMetadata>),
}
Output {
/// This is the selected project.
selected_project (Option<ProjectMetadata>),
}
}
// ===================
// === ProjectList ===
// ===================
@ -222,19 +204,18 @@ ensogl::define_endpoints! {
/// This is a list of projects in a nice frame with a title.
#[derive(Clone, CloneRef, Debug)]
pub struct ProjectList {
network: frp::Network,
display_object: display::object::Instance,
background: background::View,
caption: text::Text,
grid: grid_view::scrollable::SelectableGridView<Entry>,
#[allow(missing_docs)]
pub frp: Frp,
pub grid: grid_view::scrollable::SelectableGridView<Entry>,
}
impl ProjectList {
/// Create Project List Component.
pub fn new(app: &Application) -> Self {
let frp = Frp::new();
let network = &frp.network;
let network = frp::Network::new("ProjectList");
let display_object = display::object::Instance::new();
let background = background::View::new();
let caption = app.new_view::<text::Text>();
@ -247,7 +228,7 @@ impl ProjectList {
caption.add_to_scene_layer(&app.display.default_scene.layers.panel_text);
let style_frp = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let style = Style::from_theme(network, &style_frp);
let style = Style::from_theme(&network, &style_frp);
frp::extend! { network
init <- source::<()>();
@ -285,30 +266,11 @@ impl ProjectList {
grid_x <- grid_width.map(|width| -width / 2.0);
grid_y <- all_with3(&content_size, &bar_height, &paddings, |s,h,p| s.y / 2.0 - *h - *p);
_eval <- all_with(&grid_x, &grid_y, f!((x, y) grid.set_xy(Vector2(*x, *y))));
grid.reset_entries <+ frp.input.project_list.map(|projects| (projects.len(), 1));
grid_model_for_entry <= grid.model_for_entry_needed.map2(
&frp.input.project_list,
|(row, col), projects| {
let project = projects.get(*row)?;
Some((*row, *col, project.name.clone().into()))
}
);
grid.model_for_entry <+ grid_model_for_entry;
frp.source.selected_project <+ grid.entry_selected.map2(
&frp.input.project_list,
|selected_entry, projects| {
let (row, _) = (*selected_entry)?;
projects.get(row).cloned()
}
);
grid.select_entry <+ frp.output.selected_project.filter_map(|s| s.as_ref().map(|_| None));
}
style.init.emit(());
init.emit(());
Self { display_object, background, caption, grid, frp }
Self { network, display_object, background, caption, grid }
}
}

View File

@ -6,7 +6,6 @@
use ensogl::prelude::*;
use engine_protocol::project_manager::ProjectMetadata;
use enso_frp as frp;
use ensogl::application;
use ensogl::application::Application;
@ -40,12 +39,11 @@ pub struct Model {
status_bar: crate::status_bar::View,
welcome_view: crate::welcome_screen::View,
project_view: Rc<CloneCell<Option<crate::project::View>>>,
frp_outputs: FrpOutputsSource,
}
impl Model {
/// Constructor.
pub fn new(app: &Application, frp: &Frp) -> Self {
/// Constuctor.
pub fn new(app: &Application) -> Self {
let app = app.clone_ref();
let display_object = display::object::Instance::new();
let state = Rc::new(CloneCell::new(State::WelcomeScreen));
@ -54,9 +52,8 @@ impl Model {
let welcome_view = app.new_view::<crate::welcome_screen::View>();
let project_view = Rc::new(CloneCell::new(None));
display_object.add_child(&welcome_view);
let frp_outputs = frp.output.source.clone_ref();
Self { app, display_object, state, status_bar, welcome_view, project_view, frp_outputs }
Self { app, display_object, state, status_bar, welcome_view, project_view }
}
/// Switch displayed view from Project View to Welcome Screen. Project View will not be
@ -87,7 +84,6 @@ impl Model {
if self.project_view.get().is_none() {
let view = self.app.new_view::<crate::project::View>();
let network = &view.network;
let project_list_frp = &view.project_list().frp;
let status_bar = &self.status_bar;
let display_object = &self.display_object;
frp::extend! { network
@ -95,8 +91,6 @@ impl Model {
fs_vis_hidden <- view.fullscreen_visualization_shown.on_false();
eval fs_vis_shown ((_) status_bar.unset_parent());
eval fs_vis_hidden ([display_object, status_bar](_) display_object.add_child(&status_bar));
self.frp_outputs.selected_project <+ project_list_frp.selected_project;
}
self.project_view.set(Some(view));
}
@ -117,8 +111,6 @@ ensogl::define_endpoints! {
switch_view_to_welcome_screen(),
}
Output {
/// The selected project in the project list
selected_project (Option<ProjectMetadata>),
}
}
@ -146,8 +138,8 @@ impl Deref for View {
impl View {
/// Constuctor.
pub fn new(app: &Application) -> Self {
let model = Model::new(app);
let frp = Frp::new();
let model = Model::new(app, &frp);
let network = &frp.network;
let style = StyleWatchFrp::new(&app.display.default_scene.style_sheet);
let offset_y = style.get_number(ensogl_hardcoded_theme::application::status_bar::offset_y);

View File

@ -33,7 +33,7 @@ impl TestOnNewProjectControllersOnly {
let initializer = enso_gui::Initializer::new(config);
let error_msg = "Couldn't open project.";
let ide = initializer.initialize_ide_controller().await.expect(error_msg);
ide.manage_projects().unwrap().create_new_project(None, None).await.unwrap();
ide.manage_projects().unwrap().create_new_project(None).await.unwrap();
let project = ide.current_project().unwrap();
Self { _ide: ide, project, _executor: executor }
}

View File

@ -428,9 +428,10 @@ export class App {
/** Show a spinner. The displayed progress is constant. */
showProgressIndicator(progress: number) {
if (this.progressIndicator == null) {
this.progressIndicator = new wasm.ProgressIndicator(this.config)
if (this.progressIndicator) {
this.hideProgressIndicator()
}
this.progressIndicator = new wasm.ProgressIndicator(this.config)
this.progressIndicator.set(progress)
}