From 8569ab98c20b84ed3b77f94f7034307e0b18ecf0 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Mon, 15 May 2023 21:47:51 +1000 Subject: [PATCH 1/9] Fix `--startup.project` to bypass opening dashboard (#6671) --- app/ide-desktop/lib/content/src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts index af9bb17ba35a..236aca13cb69 100644 --- a/app/ide-desktop/lib/content/src/index.ts +++ b/app/ide-desktop/lib/content/src/index.ts @@ -187,7 +187,12 @@ class Main implements AppRunner { const isOpeningMainEntryPoint = contentConfig.OPTIONS.groups.startup.options.entry.value === contentConfig.OPTIONS.groups.startup.options.entry.default - if ((isUsingAuthentication || isUsingNewDashboard) && isOpeningMainEntryPoint) { + const isNotOpeningProject = contentConfig.OPTIONS.groups.startup.options.entry.value === '' + if ( + (isUsingAuthentication || isUsingNewDashboard) && + isOpeningMainEntryPoint && + isNotOpeningProject + ) { const hideAuth = () => { const auth = document.getElementById('dashboard') const ide = document.getElementById('root') From 06624f34a881f6a2e7425aeea286c461ff73b574 Mon Sep 17 00:00:00 2001 From: Hubert Plociniczak Date: Mon, 15 May 2023 14:15:37 +0200 Subject: [PATCH 2/9] Ensure slow shutdown of LS always kicks off hooks (#6665) This change fixes the rather elusive bug where shutdown hooks could not be fired when shutdown was taking too long and termination was forced. Under the circumstances described in detail in ticket #6515 there was a small chance that we could have a shutdown race condition. Essentially the messages received when client was disconnected and language server forced the termination could lead to language server not sending the public `ProjectClosed` message which triggers shutdown hook. Now we always do. Also made sure that multiple `ProjectClosed` messages don't lead to firing multiple shutdown hooks, which was another possibility. No tests as one would have to be able to introduce different delays in various message handlers to simulate the problem. Having ability to do such chaos testing would be nice but it is beyond the scope of this ticket. I was able to reproduce the problem 100% with my specially crafted setup so I'm fairly confident about the change. Closes #6515. --- .../LanguageServerController.scala | 3 +++ .../languageserver/LanguageServerKiller.scala | 24 +++++++++++++------ .../LanguageServerRegistry.scala | 2 +- .../ShutdownHookActivator.scala | 18 ++++++++++---- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerController.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerController.scala index 283066070324..988d22e3b598 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerController.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerController.scala @@ -297,6 +297,9 @@ class LanguageServerController( s"Received client ($clientId) disconnect request during shutdown. Ignoring." ) + case ShutDownServer => + logger.debug(s"Received shutdown request during shutdown. Ignoring.") + case m: StartServer => // This instance has not yet been shut down. Retry context.parent.forward(Retry(m)) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerKiller.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerKiller.scala index a0b51b6c6200..f0536a233ee6 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerKiller.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerKiller.scala @@ -2,6 +2,7 @@ package org.enso.projectmanager.infrastructure.languageserver import akka.actor.{Actor, ActorRef, Cancellable, PoisonPill, Props, Terminated} import com.typesafe.scalalogging.LazyLogging +import org.enso.projectmanager.event.ProjectEvent.ProjectClosed import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController.ShutDownServer import org.enso.projectmanager.infrastructure.languageserver.LanguageServerKiller.KillTimeout import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol.{ @@ -10,6 +11,7 @@ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProto } import org.enso.projectmanager.util.UnhandledLogging +import java.util.UUID import scala.concurrent.duration.FiniteDuration /** An actor that shuts all running language servers. It orchestrates all @@ -20,7 +22,7 @@ import scala.concurrent.duration.FiniteDuration * @param shutdownTimeout a shutdown timeout */ class LanguageServerKiller( - controllers: List[ActorRef], + controllers: Map[UUID, ActorRef], shutdownTimeout: FiniteDuration ) extends Actor with LazyLogging @@ -34,20 +36,22 @@ class LanguageServerKiller( context.stop(self) } else { logger.info("Killing all servers [{}].", controllers) - controllers.foreach(context.watch) - controllers.foreach(_ ! ShutDownServer) + controllers.foreach { case (_, ref) => + context.watch(ref) + ref ! ShutDownServer + } val cancellable = context.system.scheduler.scheduleOnce( shutdownTimeout, self, KillTimeout ) - context.become(killing(controllers.toSet, cancellable, sender())) + context.become(killing(controllers.map(_.swap), cancellable, sender())) } } private def killing( - liveControllers: Set[ActorRef], + liveControllers: Map[ActorRef, UUID], cancellable: Cancellable, replyTo: ActorRef ): Receive = { @@ -63,7 +67,13 @@ class LanguageServerKiller( } case KillTimeout => - liveControllers.foreach(_ ! PoisonPill) + logger.warn( + s"Not all language servers' controllers finished on time. Forcing termination." + ) + liveControllers.foreach { case (actorRef, projectId) => + actorRef ! PoisonPill + context.system.eventStream.publish(ProjectClosed(projectId)) + } context.stop(self) } @@ -80,7 +90,7 @@ object LanguageServerKiller { * @return a configuration object */ def props( - controllers: List[ActorRef], + controllers: Map[UUID, ActorRef], shutdownTimeout: FiniteDuration ): Props = Props(new LanguageServerKiller(controllers, shutdownTimeout)) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerRegistry.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerRegistry.scala index 09eb95249abe..bf371d3805d1 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerRegistry.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/LanguageServerRegistry.scala @@ -88,7 +88,7 @@ class LanguageServerRegistry( case msg @ KillThemAll => val killer = context.actorOf( LanguageServerKiller.props( - serverControllers.values.toList, + serverControllers, timeoutConfig.shutdownTimeout ), "language-server-killer" diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ShutdownHookActivator.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ShutdownHookActivator.scala index 80f0b706c4a9..bb6aa2c99d05 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ShutdownHookActivator.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/infrastructure/languageserver/ShutdownHookActivator.scala @@ -32,12 +32,13 @@ class ShutdownHookActivator[F[+_, +_]: Exec: CovariantFlatMap] private def running( hooks: Map[UUID, List[ShutdownHook[F]]] = - Map.empty.withDefaultValue(List.empty) + Map.empty.withDefaultValue(List.empty), + scheduled: List[UUID] = Nil ): Receive = { case RegisterShutdownHook(projectId, hook) => val realHook = hook.asInstanceOf[ShutdownHook[F]] val updated = hooks.updated(projectId, realHook :: hooks(projectId)) - context.become(running(updated)) + context.become(running(updated, scheduled)) case ProjectClosed(projectId) => val projectHooks = hooks(projectId) @@ -45,13 +46,22 @@ class ShutdownHookActivator[F[+_, +_]: Exec: CovariantFlatMap] context.actorOf( ShutdownHookRunner.props[F](projectId, projectHooks.reverse) ) + context.become(running(hooks - projectId, projectId :: scheduled)) + } else if (scheduled.contains(projectId)) { + logger.debug( + s"Request for starting shutdown hooks has already been filed for project ${projectId}. Ignoring." + ) + } else { + logger.warn( + s"Shutdown hook activator has no recollection of project ${projectId}. Either it was closed already or it never existed. Ignoring." + ) } case ShutdownHooksFired(projectId) => - context.become(running(hooks - projectId)) + context.become(running(hooks, scheduled.filter(_ != projectId))) case ArePendingShutdownHooks => - val arePending = hooks.values.map(_.size).sum != 0 + val arePending = hooks.values.map(_.size).sum != 0 || scheduled.nonEmpty sender() ! arePending } From b7d51ed5c6532949593d6a0b4448e53d334cbbbe Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Mon, 15 May 2023 15:03:18 +0100 Subject: [PATCH 3/9] Create unique atom getter suggestions (#6694) related #6611 Prevent creating atom getters with the same name. --- .../compiler/context/SuggestionBuilder.scala | 2 +- .../searcher/sql/SqlSuggestionsRepo.scala | 56 +++++++++++++------ .../searcher/sql/SuggestionsRepoTest.scala | 8 ++- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/engine/runtime/src/main/scala/org/enso/compiler/context/SuggestionBuilder.scala b/engine/runtime/src/main/scala/org/enso/compiler/context/SuggestionBuilder.scala index 9c5aa819f4f0..835d85be69d1 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/context/SuggestionBuilder.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/context/SuggestionBuilder.scala @@ -92,7 +92,7 @@ final class SuggestionBuilder[A: IndexedSource]( } val getters = members .flatMap(_.arguments) - .distinctBy(_.name) + .distinctBy(_.name.name) .map(buildGetter(module, tpName.name, _)) val tpSuggestions = tpe +: conses ++: getters diff --git a/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala b/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala index 28dc737d7c7a..a17e7ca8a3cc 100644 --- a/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala +++ b/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala @@ -1,21 +1,17 @@ package org.enso.searcher.sql -import java.util.UUID +import org.enso.polyglot.runtime.Runtime.Api._ import org.enso.polyglot.{ExportedSymbol, Suggestion} -import org.enso.polyglot.runtime.Runtime.Api.{ - ExportsAction, - ExportsUpdate, - SuggestionAction, - SuggestionUpdate, - SuggestionsDatabaseAction -} import org.enso.searcher.data.QueryResult import org.enso.searcher.{SuggestionEntry, SuggestionsRepo} import slick.jdbc.SQLiteProfile.api._ import slick.jdbc.meta.MTable import slick.relational.RelationalProfile -import scala.collection.immutable.{HashMap, ListMap} +import java.util.UUID + +import scala.collection.immutable.HashMap +import scala.collection.mutable import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} @@ -682,15 +678,28 @@ final class SqlSuggestionsRepo(val db: SqlDatabase)(implicit private def insertAllQuery( suggestions: Iterable[Suggestion] ): DBIO[Seq[Long]] = { - val suggestionsMap = - suggestions.map(s => SuggestionRowUniqueIndex(s) -> s).to(ListMap) - val rows = suggestions.map(toSuggestionRow) - for { - _ <- Suggestions ++= rows - rows <- Suggestions.result - } yield { - val rowsMap = rows.map(r => SuggestionRowUniqueIndex(r) -> r.id.get).toMap - suggestionsMap.keys.map(rowsMap(_)).toSeq + val duplicatesBuilder = Vector.newBuilder[(Suggestion, Suggestion)] + val suggestionsMap: mutable.Map[SuggestionRowUniqueIndex, Suggestion] = + mutable.LinkedHashMap() + suggestions.foreach { suggestion => + val idx = SuggestionRowUniqueIndex(suggestion) + suggestionsMap.put(idx, suggestion).foreach { duplicate => + duplicatesBuilder.addOne((duplicate, suggestion)) + } + } + val duplicates = duplicatesBuilder.result() + if (duplicates.isEmpty) { + val rows = suggestions.map(toSuggestionRow) + for { + _ <- Suggestions ++= rows + rows <- Suggestions.result + } yield { + val rowsMap = + rows.map(r => SuggestionRowUniqueIndex(r) -> r.id.get).toMap + suggestionsMap.keys.map(rowsMap(_)).toSeq + } + } else { + DBIO.failed(SqlSuggestionsRepo.UniqueConstraintViolatedError(duplicates)) } } @@ -1053,3 +1062,14 @@ final class SqlSuggestionsRepo(val db: SqlDatabase)(implicit } yield new UUID(m, l) } + +object SqlSuggestionsRepo { + + /** An error indicating that the database unique constraint was violated. + * + * @param duplicates the entries that violate the unique constraint + */ + final case class UniqueConstraintViolatedError( + duplicates: Seq[(Suggestion, Suggestion)] + ) extends Exception(s"Database unique constraint is violated [$duplicates].") +} diff --git a/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala b/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala index ff3ed4cd1fe9..bd154d5c37c6 100644 --- a/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala +++ b/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala @@ -6,14 +6,13 @@ import org.enso.polyglot.{ExportedSymbol, ModuleExports, Suggestion} import org.enso.polyglot.runtime.Runtime.Api import org.enso.searcher.SuggestionEntry import org.enso.searcher.data.QueryResult +import org.enso.searcher.sql.SqlSuggestionsRepo.UniqueConstraintViolatedError import org.enso.searcher.sql.equality.SuggestionsEquality import org.enso.testkit.RetrySpec import org.scalactic.TripleEqualsSupport import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import java.sql.SQLException - import scala.concurrent.Await import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ @@ -132,7 +131,10 @@ class SuggestionsRepoTest _ <- repo.insertAll(Seq(suggestion.local, suggestion.local)) } yield () - an[SQLException] should be thrownBy Await.result(action, Timeout) + an[UniqueConstraintViolatedError] should be thrownBy Await.result( + action, + Timeout + ) } "select suggestion by id" taggedAs Retry in withRepo { repo => From 23e75f5e977bedfbebbee4b0187cdb3422dc8099 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Tue, 16 May 2023 00:06:56 +1000 Subject: [PATCH 4/9] Fix logic for enabling dashboard (#6696) * Fix logic for enabling dashboard * Run prettier --- app/ide-desktop/lib/content/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/ide-desktop/lib/content/src/index.ts b/app/ide-desktop/lib/content/src/index.ts index 236aca13cb69..3d43ce325b42 100644 --- a/app/ide-desktop/lib/content/src/index.ts +++ b/app/ide-desktop/lib/content/src/index.ts @@ -187,7 +187,8 @@ class Main implements AppRunner { const isOpeningMainEntryPoint = contentConfig.OPTIONS.groups.startup.options.entry.value === contentConfig.OPTIONS.groups.startup.options.entry.default - const isNotOpeningProject = contentConfig.OPTIONS.groups.startup.options.entry.value === '' + const isNotOpeningProject = + contentConfig.OPTIONS.groups.startup.options.project.value === '' if ( (isUsingAuthentication || isUsingNewDashboard) && isOpeningMainEntryPoint && From 341f1275e062e9621c6f8632cf7c5558be8a2f5e Mon Sep 17 00:00:00 2001 From: "Stijn (\"stain\") Seghers" Date: Mon, 15 May 2023 16:08:56 +0200 Subject: [PATCH 5/9] Show spinner when opening/creating a project (#6321) Closes #5505: A spinner with the Enso logo is now shown for most of the duration when opening and creating a project. https://user-images.githubusercontent.com/607786/234019207-e34096c7-2cb3-4ae3-abd3-291fcaf43084.mp4 # Important Notes There's still a tiny bit of lag between the end of the spinner and when the nodes pop up, but that seemed less trivial to me to work around. --- app/gui/src/controller/ide.rs | 65 ++++++--- app/gui/src/controller/ide/desktop.rs | 74 +++-------- app/gui/src/controller/searcher.rs | 33 ----- app/gui/src/controller/searcher/action.rs | 14 -- app/gui/src/ide.rs | 7 +- app/gui/src/ide/initializer.rs | 139 +++++--------------- app/gui/src/integration_test.rs | 5 +- app/gui/src/presenter.rs | 100 ++++++++++++-- app/gui/src/presenter/project.rs | 69 +--------- app/gui/src/presenter/searcher/provider.rs | 1 - app/gui/src/tests.rs | 12 +- app/gui/view/src/project.rs | 2 +- app/gui/view/src/project_list.rs | 48 ++++++- app/gui/view/src/root.rs | 15 ++- build-config.yaml | 2 +- integration-test/tests/engine.rs | 2 +- lib/rust/ensogl/pack/js/src/runner/index.ts | 5 +- 17 files changed, 267 insertions(+), 326 deletions(-) diff --git a/app/gui/src/controller/ide.rs b/app/gui/src/controller/ide.rs index 8bfe4937c3c2..5776fb6dba77 100644 --- a/app/gui/src/controller/ide.rs +++ b/app/gui/src/controller/ide.rs @@ -5,6 +5,8 @@ use crate::prelude::*; +use crate::config::ProjectToOpen; + use double_representation::name::project; use mockall::automock; use parser::Parser; @@ -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, @@ -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 === @@ -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) -> BoxFuture; + fn create_new_project( + &self, + name: Option, + template: Option, + ) -> BoxFuture; /// Return a list of existing projects. fn list_projects(&self) -> BoxFuture>>; @@ -150,18 +157,44 @@ pub trait ManagingProjectAPI { /// and then for the project opening. fn open_project_by_name(&self, name: String) -> BoxFuture { 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 { + 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> { + 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() + } } diff --git a/app/gui/src/controller/ide/desktop.rs b/app/gui/src/controller/ide/desktop.rs index c84598e88a7f..14cc7f0c7b86 100644 --- a/app/gui/src/controller/ide/desktop.rs +++ b/app/gui/src/controller/ide/desktop.rs @@ -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; @@ -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, - project_to_open: Option, - ) -> FallibleResult { - 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, - project: Option, - ) -> 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) -> FallibleResult { + 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, - project_to_open: ProjectToOpen, - ) -> FallibleResult { - // TODO[ao]: Reuse of initializer used in previous code design. It should be soon replaced - // anyway, because we will soon resign from the "open or create" approach when opening - // IDE. See https://github.com/enso-org/ide/issues/1492 for details. - let initializer = initializer::WithProjectManager::new(project_manager, 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(), + }) } } @@ -133,14 +94,16 @@ impl API for Handle { impl ManagingProjectAPI for Handle { #[profile(Objective)] - fn create_new_project(&self, template: Option) -> BoxFuture { + fn create_new_project( + &self, + name: Option, + template: Option, + ) -> BoxFuture { 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; @@ -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() } diff --git a/app/gui/src/controller/searcher.rs b/app/gui/src/controller/searcher.rs index 2d1f2cba95d9..f93ee22ebe43 100644 --- a/app/gui/src/controller/searcher.rs +++ b/app/gui/src/controller/searcher.rs @@ -679,33 +679,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()), - } - } } } @@ -1001,12 +974,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 { diff --git a/app/gui/src/controller/searcher/action.rs b/app/gui/src/controller/searcher/action.rs index 1cb848f360e9..020740f6a015 100644 --- a/app/gui/src/controller/searcher/action.rs +++ b/app/gui/src/controller/searcher/action.rs @@ -66,14 +66,6 @@ impl Suggestion { /// Action of adding example code. pub type Example = Rc; -/// 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, name: ImString }, -} - /// A single action on the Searcher list. See also `controller::searcher::Searcher` docs. #[derive(Clone, CloneRef, Debug, PartialEq)] pub enum Action { @@ -84,8 +76,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.). } @@ -101,10 +91,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), } } } diff --git a/app/gui/src/ide.rs b/app/gui/src/ide.rs index 4ff2a3bc5e48..291220b8783e 100644 --- a/app/gui/src/ide.rs +++ b/app/gui/src/ide.rs @@ -2,6 +2,7 @@ use crate::prelude::*; +use crate::config::ProjectToOpen; use crate::presenter::Presenter; use analytics::AnonymousData; @@ -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. @@ -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() diff --git a/app/gui/src/ide/initializer.rs b/app/gui/src/ide/initializer.rs index 9251c4d3d8aa..c7a5f7eaa1d4 100644 --- a/app/gui/src/ide/initializer.rs +++ b/app/gui/src/ide/initializer.rs @@ -3,17 +3,14 @@ use crate::prelude::*; use crate::config; -use crate::config::ProjectToOpen; use crate::ide::Ide; use crate::transport::web::WebSocket; use crate::FailedIde; use engine_protocol::project_manager; -use engine_protocol::project_manager::ProjectName; use enso_web::sleep; use ensogl::application::Application; use std::time::Duration; -use uuid::Uuid; @@ -35,19 +32,6 @@ 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 === // =================== @@ -94,26 +78,36 @@ impl Initializer { // issues to user, such information should be properly passed // in case of setup failure. + match self.initialize_ide_controller_with_retries().await { + Ok(controller) => { + let ide = Ide::new(ensogl_app, view.clone_ref(), controller); + if let Some(project) = &self.config.project_to_open { + ide.open_or_create_project(project.clone()); + } + info!("IDE was successfully initialized."); + Ok(ide) + } + Err(error) => { + let message = format!("Failed to initialize application: {error}"); + status_bar.add_event(ide_view::status_bar::event::Label::new(message)); + Err(FailedIde { view }) + } + } + } + + async fn initialize_ide_controller_with_retries(&self) -> FallibleResult { let mut retry_after = INITIALIZATION_RETRY_TIMES.iter(); loop { match self.initialize_ide_controller().await { - Ok(controller) => { - let ide = Ide::new(ensogl_app, view.clone_ref(), controller); - info!("Setup done."); - break Ok(ide); - } + Ok(controller) => break Ok(controller), Err(error) => { - let message = format!("Failed to initialize application: {error}"); - error!("{message}"); + error!("Failed to initialize controller: {error}"); match retry_after.next() { Some(time) => { error!("Retrying after {} seconds", time.as_secs_f32()); sleep(*time).await; } - None => { - status_bar.add_event(ide_view::status_bar::event::Label::new(message)); - break Err(FailedIde { view }); - } + None => break Err(error), } } } @@ -130,9 +124,8 @@ impl Initializer { match &self.config.backend { ProjectManager { endpoint } => { let project_manager = self.setup_project_manager(endpoint).await?; - 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?)) + let controller = controller::ide::Desktop::new(project_manager)?; + Ok(Rc::new(controller)) } LanguageServer { json_endpoint, binary_endpoint, namespace, project_name } => { let json_endpoint = json_endpoint.clone(); @@ -172,81 +165,6 @@ 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, - pub project_to_open: ProjectToOpen, -} - -impl WithProjectManager { - /// Constructor. - pub fn new( - project_manager: Rc, - 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 { - 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 { - 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 { - 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 { - 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 === // ============= @@ -290,6 +208,10 @@ 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; @@ -313,9 +235,10 @@ 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 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.")) + let project_id = + ide_controller.find_project(&project_to_open).await.expect("Couldn't get project."); + assert_eq!(project_id, expected_id); } } diff --git a/app/gui/src/integration_test.rs b/app/gui/src/integration_test.rs index 377f2cee66f3..fb6de04f3076 100644 --- a/app/gui/src/integration_test.rs +++ b/app/gui/src/integration_test.rs @@ -83,7 +83,10 @@ impl Fixture { let project_management = controller.manage_projects().expect("Cannot access Managing Project API"); - project_management.create_new_project(None).await.expect("Failed to create new project"); + project_management + .create_new_project(None, None) + .await + .expect("Failed to create new project"); } /// After returning, the IDE is in a state with the project opened and ready to work diff --git a/app/gui/src/presenter.rs b/app/gui/src/presenter.rs index 8a3b2bac2867..0d19114ef6a5 100644 --- a/app/gui/src/presenter.rs +++ b/app/gui/src/presenter.rs @@ -4,13 +4,16 @@ 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; // ============== @@ -29,6 +32,19 @@ pub use searcher::Searcher; +// ================= +// === 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 === // ============= @@ -94,15 +110,15 @@ impl Model { #[profile(Task)] pub fn open_project(&self, project_name: String) { let controller = self.controller.clone_ref(); - crate::executor::global::spawn(async move { + crate::executor::global::spawn(with_progress_indicator(|| 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 opening failed: no ProjectManagingAPI available."); + warn!("Project Manager API not available, cannot open project."); } - }); + })); } /// Create a new project. `template` is an optional name of the project template passed to the @@ -113,9 +129,10 @@ impl Model { if let Ok(template) = template.map(double_representation::name::project::Template::from_text).transpose() { - crate::executor::global::spawn(async move { + crate::executor::global::spawn(with_progress_indicator(|| async move { if let Ok(managing_api) = controller.manage_projects() { - if let Err(err) = managing_api.create_new_project(template.clone()).await { + if let Err(err) = managing_api.create_new_project(None, template.clone()).await + { if let Some(template) = template { error!("Could not create new project from template {template}: {err}."); } else { @@ -123,13 +140,66 @@ impl Model { } } } else { - warn!("Project creation failed: no ProjectManagingAPI available."); + warn!("Project Manager API not available, cannot create project."); } - }) + })) } 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: F) +where + F: FnOnce() -> T, + T: Future, { + // 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(); } @@ -165,6 +235,13 @@ 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() @@ -174,7 +251,6 @@ impl Presenter { fn init(self) -> Self { self.setup_status_bar_notification_handler(); self.setup_controller_notification_handler(); - self.model.clone_ref().setup_and_display_new_project(); executor::global::spawn(self.clone_ref().set_projects_list_on_welcome_screen()); self } @@ -214,8 +290,7 @@ impl Presenter { let weak = Rc::downgrade(&self.model); spawn_stream_handler(weak, stream, move |notification, model| { match notification { - controller::ide::Notification::NewProjectCreated - | controller::ide::Notification::ProjectOpened => + controller::ide::Notification::ProjectOpened => model.setup_and_display_new_project(), controller::ide::Notification::ProjectClosed => { model.close_project(); @@ -239,6 +314,11 @@ 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) + } } diff --git a/app/gui/src/presenter/project.rs b/app/gui/src/presenter/project.rs index 01ed7c3efddb..c41a9a127308 100644 --- a/app/gui/src/presenter/project.rs +++ b/app/gui/src/presenter/project.rs @@ -9,8 +9,8 @@ use crate::presenter; use crate::presenter::graph::ViewNodeId; 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 model::module::NotificationKind; @@ -19,16 +19,6 @@ 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 === // ============= @@ -46,7 +36,7 @@ struct Model { graph: presenter::Graph, code: presenter::Code, searcher: RefCell>, - available_projects: Rc>>, + available_projects: Rc>>, } impl Model { @@ -268,8 +258,6 @@ 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(()); } @@ -277,36 +265,6 @@ 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, @@ -370,28 +328,15 @@ 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(); + let project_list = &model.view.project_list().frp; frp::extend! { network project_list_ready <- source_(); - - 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; - + project_list.project_list <+ project_list_ready.map( + f_!(model.available_projects.borrow().clone()) + ); open_project_list <- view.project_list_shown.on_true(); - 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_ open_project_list (model.project_list_opened(project_list_ready.clone_ref())); eval view.searcher ([model](params) { if let Some(params) = params { diff --git a/app/gui/src/presenter/searcher/provider.rs b/app/gui/src/presenter/searcher/provider.rs index bf5ea8ab628b..e28132c1c8a4 100644 --- a/app/gui/src/presenter/searcher/provider.rs +++ b/app/gui/src/presenter/searcher/provider.rs @@ -133,7 +133,6 @@ 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, } } } diff --git a/app/gui/src/tests.rs b/app/gui/src/tests.rs index 31f4de972528..ca4777c97950 100644 --- a/app/gui/src/tests.rs +++ b/app/gui/src/tests.rs @@ -1,11 +1,10 @@ use super::prelude::*; -use crate::config::ProjectToOpen; -use crate::ide; +use crate::controller::ide; +use crate::controller::ide::ManagingProjectAPI; 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; @@ -28,11 +27,8 @@ 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 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; + let ide_controller = ide::Desktop::new(project_manager).unwrap(); + let result = ide_controller.create_new_project(None, None).await; result.expect_err("Error should have been reported."); }); fixture.when_stalled_send_response(json!({ diff --git a/app/gui/view/src/project.rs b/app/gui/view/src/project.rs index b92539147cc8..29ddec48298d 100644 --- a/app/gui/view/src/project.rs +++ b/app/gui/view/src/project.rs @@ -567,7 +567,7 @@ impl View { // === Project Dialog === eval_ frp.show_project_list (model.show_project_list()); - project_chosen <- project_list.grid.entry_selected.constant(()); + project_chosen <- project_list.frp.selected_project.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); diff --git a/app/gui/view/src/project_list.rs b/app/gui/view/src/project_list.rs index 9936b1e49036..71177c7f2ff9 100644 --- a/app/gui/view/src/project_list.rs +++ b/app/gui/view/src/project_list.rs @@ -4,6 +4,7 @@ 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,6 +197,23 @@ mod background { +// =========== +// === FRP === +// =========== + +ensogl::define_endpoints! { + Input { + /// This is a list of projects to choose from. + project_list (Vec), + } + Output { + /// This is the selected project. + selected_project (Option), + } +} + + + // =================== // === ProjectList === // =================== @@ -205,18 +223,19 @@ mod background { /// 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, #[allow(missing_docs)] - pub grid: grid_view::scrollable::SelectableGridView, + pub frp: Frp, } impl ProjectList { /// Create Project List Component. pub fn new(app: &Application) -> Self { - let network = frp::Network::new("ProjectList"); + let frp = Frp::new(); + let network = &frp.network; let display_object = display::object::Instance::new(); let background = background::View::new(); let caption = app.new_view::(); @@ -236,7 +255,7 @@ impl ProjectList { } 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::<()>(); @@ -274,11 +293,30 @@ 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 { network, display_object, background, caption, grid } + Self { display_object, background, caption, grid, frp } } } diff --git a/app/gui/view/src/root.rs b/app/gui/view/src/root.rs index a11d9b4954df..11984b2f081e 100644 --- a/app/gui/view/src/root.rs +++ b/app/gui/view/src/root.rs @@ -6,6 +6,7 @@ use ensogl::prelude::*; +use engine_protocol::project_manager::ProjectMetadata; use enso_frp as frp; use ensogl::application; use ensogl::application::Application; @@ -38,11 +39,12 @@ pub struct Model { status_bar: crate::status_bar::View, welcome_view: crate::welcome_screen::View, project_view: Rc>>, + frp: Frp, } impl Model { /// Constuctor. - pub fn new(app: &Application) -> Self { + pub fn new(app: &Application, frp: &Frp) -> Self { let app = app.clone_ref(); let display_object = display::object::Instance::new(); let state = Rc::new(CloneCell::new(State::WelcomeScreen)); @@ -51,8 +53,9 @@ impl Model { let welcome_view = app.new_view::(); let project_view = Rc::new(CloneCell::new(None)); display_object.add_child(&welcome_view); + let frp = frp.clone_ref(); - Self { app, display_object, status_bar, welcome_view, project_view, state } + Self { app, display_object, state, status_bar, welcome_view, project_view, frp } } /// Switch displayed view from Project View to Welcome Screen. Project View will not be @@ -82,6 +85,10 @@ impl Model { fn init_project_view(&self) { if self.project_view.get().is_none() { let view = self.app.new_view::(); + let project_list_frp = &view.project_list().frp; + frp::extend! { network + self.frp.source.selected_project <+ project_list_frp.selected_project; + } self.project_view.set(Some(view)); } } @@ -101,6 +108,8 @@ ensogl::define_endpoints! { switch_view_to_welcome_screen(), } Output { + /// The selected project in the project list + selected_project (Option), } } @@ -128,8 +137,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); diff --git a/build-config.yaml b/build-config.yaml index bf84c332d939..ca035692c78a 100644 --- a/build-config.yaml +++ b/build-config.yaml @@ -1,6 +1,6 @@ # Options intended to be common for all developers. -wasm-size-limit: 15.85 MiB +wasm-size-limit: 15.87 MiB required-versions: # NB. The Rust version is pinned in rust-toolchain.toml. diff --git a/integration-test/tests/engine.rs b/integration-test/tests/engine.rs index 7f046d27e25f..622245b8b875 100644 --- a/integration-test/tests/engine.rs +++ b/integration-test/tests/engine.rs @@ -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).await.unwrap(); + ide.manage_projects().unwrap().create_new_project(None, None).await.unwrap(); let project = ide.current_project().unwrap(); Self { _ide: ide, project, _executor: executor } } diff --git a/lib/rust/ensogl/pack/js/src/runner/index.ts b/lib/rust/ensogl/pack/js/src/runner/index.ts index 4f42493ced5e..81dd9966408f 100644 --- a/lib/rust/ensogl/pack/js/src/runner/index.ts +++ b/lib/rust/ensogl/pack/js/src/runner/index.ts @@ -426,10 +426,9 @@ export class App { /** Show a spinner. The displayed progress is constant. */ showProgressIndicator(progress: number) { - if (this.progressIndicator) { - this.hideProgressIndicator() + if (this.progressIndicator == null) { + this.progressIndicator = new wasm.ProgressIndicator(this.config) } - this.progressIndicator = new wasm.ProgressIndicator(this.config) this.progressIndicator.set(progress) } From 7bf9d3614ce9262af00185485af0553846abf0c5 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Tue, 16 May 2023 01:58:20 +1000 Subject: [PATCH 6/9] Fix opening cloud projects (#6683) --- .../src/authentication/src/dashboard/components/ide.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/ide.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/ide.tsx index 998d865274af..3f8f9c37c0d4 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/ide.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/ide.tsx @@ -88,6 +88,7 @@ function Ide(props: Props) { return } else { const script = document.createElement('script') + script.crossOrigin = 'anonymous' script.src = `${IDE_CDN_URL}/${engineVersion}/index.js.gz` script.onload = async () => { document.body.removeChild(script) @@ -101,6 +102,7 @@ function Ide(props: Props) { } document.body.appendChild(script) const style = document.createElement('link') + style.crossOrigin = 'anonymous' style.rel = 'stylesheet' style.href = `${IDE_CDN_URL}/${engineVersion}/style.css` document.body.appendChild(style) From 0a3a82c07fd427fb0581da61c27647d2cd9b0d88 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Tue, 16 May 2023 20:18:38 +1000 Subject: [PATCH 7/9] Fix top bar styles (#6695) --- .../src/dashboard/components/topBar.tsx | 123 +++++++++--------- 1 file changed, 59 insertions(+), 64 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx index 41d7f838128f..cae206b6166c 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/topBar.tsx @@ -44,82 +44,77 @@ function TopBar(props: TopBarProps) { return (
-
- {platform === platformModule.Platform.desktop && ( -
- - -
- )} -
- - {projectName ?? 'Dashboard'} - -
- {svg.BARS_ICON} -
- +
-
-
{svg.MAGNIFYING_GLASS_ICON}
- { - setQuery(event.target.value) + {svg.COMPUTER_ICON} + +
+ )} +
+ + {projectName ?? 'Dashboard'} + +
{svg.BARS_ICON}
+ + {projectName ?? 'No project open'} + +
+
+
{svg.MAGNIFYING_GLASS_ICON}
+ { + setQuery(event.target.value) + }} + className="flex-1 mx-2 bg-transparent" + />
-
- help chat + help chat
{svg.SPEECH_BUBBLE_ICON}
{/* User profile and menu. */} -
+
{ event.stopPropagation() From 25eeeb7da13cb938f350eb6974334ab0df8972a7 Mon Sep 17 00:00:00 2001 From: somebody1234 Date: Tue, 16 May 2023 20:30:33 +1000 Subject: [PATCH 8/9] Hide "shared with" column on local backend (#6684) * Hide "shared with" column on local backend * Constant column widths --- .../src/dashboard/components/dashboard.tsx | 91 +++++++++++++------ 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx index fb1d1329ad04..154b7f91012d 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/dashboard/components/dashboard.tsx @@ -126,6 +126,19 @@ const COLUMN_NAME: Record, string> = { [Column.ide]: 'IDE', } as const +/** CSS classes for every column. Currently only used to set the widths. */ +const COLUMN_CSS_CLASS: Record = { + [Column.name]: 'w-60', + [Column.lastModified]: 'w-32', + [Column.sharedWith]: 'w-36', + [Column.docs]: 'w-96', + [Column.labels]: 'w-80', + [Column.dataAccess]: 'w-96', + [Column.usagePlan]: '', + [Column.engine]: 'w-20', + [Column.ide]: 'w-20', +} as const + /** The corresponding `Permissions` for each backend `PermissionAction`. */ const PERMISSION: Record = { [backendModule.PermissionAction.own]: { type: permissionDisplay.Permission.owner }, @@ -193,6 +206,14 @@ function rootDirectoryId(userOrOrganizationId: backendModule.UserOrOrganizationI ) } +/** Returns the list of columns to be displayed. */ +function columnsFor(displayMode: ColumnDisplayMode, backendPlatform: platformModule.Platform) { + const columns = COLUMNS_FOR[displayMode] + return backendPlatform === platformModule.Platform.desktop + ? columns.filter(column => column !== Column.sharedWith) + : columns +} + // ================= // === Dashboard === // ================= @@ -812,9 +833,13 @@ function Dashboard(props: DashboardProps) { )}
- +
- + + {columnsFor(columnDisplayMode, backend.platform).map(column => ( + > items={visibleProjectAssets} getKey={proj => proj.id} @@ -824,7 +849,7 @@ function Dashboard(props: DashboardProps) { above. } - columns={COLUMNS_FOR[columnDisplayMode].map(column => ({ + columns={columnsFor(columnDisplayMode, backend.platform).map(column => ({ id: column, heading: ColumnHeading(column, backendModule.AssetType.project), render: renderer(column, backendModule.AssetType.project), @@ -916,14 +941,19 @@ function Dashboard(props: DashboardProps) { {query ? ' matching your query' : ''}. } - columns={COLUMNS_FOR[columnDisplayMode].map(column => ({ - id: column, - heading: ColumnHeading( - column, - backendModule.AssetType.directory - ), - render: renderer(column, backendModule.AssetType.directory), - }))} + columns={columnsFor(columnDisplayMode, backend.platform).map( + column => ({ + id: column, + heading: ColumnHeading( + column, + backendModule.AssetType.directory + ), + render: renderer( + column, + backendModule.AssetType.directory + ), + }) + )} onClick={(directoryAsset, event) => { event.stopPropagation() setSelectedAssets( @@ -948,14 +978,19 @@ function Dashboard(props: DashboardProps) { {query ? ' matching your query' : ''}. } - columns={COLUMNS_FOR[columnDisplayMode].map(column => ({ - id: column, - heading: ColumnHeading( - column, - backendModule.AssetType.secret - ), - render: renderer(column, backendModule.AssetType.secret), - }))} + columns={columnsFor(columnDisplayMode, backend.platform).map( + column => ({ + id: column, + heading: ColumnHeading( + column, + backendModule.AssetType.secret + ), + render: renderer( + column, + backendModule.AssetType.secret + ), + }) + )} onClick={(secret, event) => { event.stopPropagation() setSelectedAssets( @@ -998,14 +1033,16 @@ function Dashboard(props: DashboardProps) { {query ? ' matching your query' : ''}. } - columns={COLUMNS_FOR[columnDisplayMode].map(column => ({ - id: column, - heading: ColumnHeading( - column, - backendModule.AssetType.file - ), - render: renderer(column, backendModule.AssetType.file), - }))} + columns={columnsFor(columnDisplayMode, backend.platform).map( + column => ({ + id: column, + heading: ColumnHeading( + column, + backendModule.AssetType.file + ), + render: renderer(column, backendModule.AssetType.file), + }) + )} onClick={(file, event) => { event.stopPropagation() setSelectedAssets( From f38033b037800bddef55257a3374aa9f571678aa Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Tue, 16 May 2023 13:14:19 +0200 Subject: [PATCH 9/9] Add verbose logs mention to CONTRIBUTING.md (#6703) --- docs/CONTRIBUTING.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 61780f67b450..69daf7756f13 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -107,6 +107,26 @@ reproduce it, the faster we can fix the bug! It's also helpful to have the output of `enso --version`, as that will let us know if the bug is Operating System or Architecture specific. +### Turning on verbose logs + +Sometimes, it is helpful to attach a verbose log to your bug report. The way to +enable verbose logging depends on which version of Enso you are using. For a +standalone distribution (`.exe` on Windows, `.AppImage` on Linux), you can +enable verbose logging by passing `-debug.verbose` option. If you are starting +the `project-manager`, or language server separately, then pass +`--log-level trace` option. With verbose logging, there are a lot of messages +printed to the standard output, and it is possible that on slower terminal +emulators this will clog the terminal and hence the whole backend. To avoid +this, we recommend redirecting the output to `/dev/null`, via a command like +`enso -debug.verbose > /dev/null 2>&1`. + +The logs are kept in a central location `$ENSO_DATA_DIRECTORY/log` - on Linux, +they are in `$XDG_DATA_HOME/enso/log` (usually `~/.local/share/enso/log`), and +on Windows they are in `%APPDATA%\enso\log`, see +[distribution.md](distribution/distribution.md) for details. The log level name +consists of the timestamp of the log file creation. There is no automatic log +rotation, so you may want to delete the old logs from time to time. + ## Hacking on Enso This will get you up and running for Enso development, with only a minimal
+ ))} +