diff --git a/RELEASES.md b/RELEASES.md index ad9a7c3177..b26d9dd262 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,12 @@ # Enso Next +## Tooling + +- Implement parts of the new Language Server API related to library support + ([#1875](https://github.com/enso-org/enso/pull/1875)). Parts of the API are + still mocked internally, but they are supported externally for testing + purposes. + # Enso 0.2.14 (2021-07-15) ## Interpreter/Runtime diff --git a/build.sbt b/build.sbt index 2d8e3a95ae..c859d99ada 100644 --- a/build.sbt +++ b/build.sbt @@ -233,6 +233,7 @@ lazy val enso = (project in file(".")) logger.jvm, pkg, cli, + `task-progress-notifications`, `logging-utils`, `logging-service`, `akka-native`, @@ -716,6 +717,20 @@ lazy val cli = project Test / parallelExecution := false ) +lazy val `task-progress-notifications` = project + .in(file("lib/scala/task-progress-notifications")) + .configs(Test) + .settings( + version := "0.1", + libraryDependencies ++= Seq( + "com.beachape" %% "enumeratum-circe" % enumeratumCirceVersion, + "org.scalatest" %% "scalatest" % scalatestVersion % Test + ), + Test / parallelExecution := false + ) + .dependsOn(cli) + .dependsOn(`json-rpc-server`) + lazy val `version-output` = (project in file("lib/scala/version-output")) .settings( version := "0.1" @@ -798,6 +813,7 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager")) .dependsOn(`version-output`) .dependsOn(editions) .dependsOn(cli) + .dependsOn(`task-progress-notifications`) .dependsOn(`polyglot-api`) .dependsOn(`runtime-version-manager`) .dependsOn(`library-manager`) @@ -977,6 +993,7 @@ lazy val `language-server` = (project in file("engine/language-server")) ), Test / testOptions += Tests .Argument(TestFrameworks.ScalaCheck, "-minSuccessfulTests", "1000"), + Test / envVars ++= distributionEnvironmentOverrides, GenerateFlatbuffers.flatcVersion := flatbuffersVersion, Compile / sourceGenerators += GenerateFlatbuffers.task ) @@ -991,6 +1008,8 @@ lazy val `language-server` = (project in file("engine/language-server")) ) .dependsOn(`json-rpc-server-test` % Test) .dependsOn(`json-rpc-server`) + .dependsOn(`task-progress-notifications`) + .dependsOn(`library-manager`) .dependsOn(`logging-service`) .dependsOn(`polyglot-api`) .dependsOn(`searcher`) @@ -998,6 +1017,7 @@ lazy val `language-server` = (project in file("engine/language-server")) .dependsOn(`version-output`) .dependsOn(pkg) .dependsOn(testkit % Test) + .dependsOn(`runtime-version-manager-test` % Test) lazy val ast = (project in file("lib/scala/ast")) .settings( diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index 8b4b708710..48cb2a429d 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -4014,13 +4014,11 @@ the repositories and include them in the result as well. > available. In the future it should emit warnings using proper notification > channels. -The `update` field is optional and if it is not provided, it defaults to false. - #### Parameters ```typescript { - update?: Boolean; + update: Boolean; } ``` diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala index ed708fdcda..c7276b57b5 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala @@ -1,27 +1,26 @@ package org.enso.languageserver.boot -import java.io.File -import java.net.URI -import java.time.Clock - import akka.actor.ActorSystem +import org.enso.distribution.{ + DistributionManager, + EditionManager, + Environment, + LanguageHome +} +import org.enso.editions.EditionResolver import org.enso.jsonrpc.JsonRpcServer import org.enso.languageserver.boot.DeploymentType.{Azure, Desktop} import org.enso.languageserver.capability.CapabilityRouter import org.enso.languageserver.data._ import org.enso.languageserver.effect.ZioExec -import org.enso.languageserver.filemanager.{ - ContentRoot, - ContentRootManager, - ContentRootManagerActor, - ContentRootManagerWrapper, - ContentRootWithFile, - FileManager, - FileSystem, - ReceivesTreeUpdatesHandler -} +import org.enso.languageserver.filemanager._ import org.enso.languageserver.http.server.BinaryWebSocketServer import org.enso.languageserver.io._ +import org.enso.languageserver.libraries.{ + EditionReferenceResolver, + LocalLibraryManager, + ProjectSettingsManager +} import org.enso.languageserver.monitoring.{ HealthCheckEndpoint, IdlenessEndpoint, @@ -49,6 +48,9 @@ import org.graalvm.polyglot.Context import org.graalvm.polyglot.io.MessageEndpoint import org.slf4j.LoggerFactory +import java.io.File +import java.net.URI +import java.time.Clock import scala.concurrent.duration._ /** A main module containing all components of the server. @@ -270,20 +272,48 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) { context )(system.dispatcher) + val environment = new Environment {} + val languageHome = LanguageHome.detectFromExecutableLocation(environment) + val distributionManager = new DistributionManager(environment) + + val editionProvider = + EditionManager.makeEditionProvider(distributionManager, Some(languageHome)) + val editionResolver = EditionResolver(editionProvider) + val editionReferenceResolver = new EditionReferenceResolver( + contentRoot.file, + editionProvider, + editionResolver + ) + val editionManager = EditionManager(distributionManager, Some(languageHome)) + + val projectSettingsManager = system.actorOf( + ProjectSettingsManager.props(contentRoot.file, editionResolver), + "project-settings-manager" + ) + + val localLibraryManager = system.actorOf( + LocalLibraryManager.props(contentRoot.file, distributionManager), + "local-library-manager" + ) + val jsonRpcControllerFactory = new JsonConnectionControllerFactory( - initializationComponent, - bufferRegistry, - capabilityRouter, - fileManager, - contentRootManagerActor, - contextRegistry, - suggestionsHandler, - stdOutController, - stdErrController, - stdInController, - runtimeConnector, - idlenessMonitor, - languageServerConfig + mainComponent = initializationComponent, + bufferRegistry = bufferRegistry, + capabilityRouter = capabilityRouter, + fileManager = fileManager, + contentRootManager = contentRootManagerActor, + contextRegistry = contextRegistry, + suggestionsHandler = suggestionsHandler, + stdOutController = stdOutController, + stdErrController = stdErrController, + stdInController = stdInController, + runtimeConnector = runtimeConnector, + idlenessMonitor = idlenessMonitor, + projectSettingsManager = projectSettingsManager, + localLibraryManager = localLibraryManager, + editionReferenceResolver = editionReferenceResolver, + editionManager = editionManager, + config = languageServerConfig ) log.trace( "Created JSON connection controller factory [{}].", diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/EditionReference.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/EditionReference.scala new file mode 100644 index 0000000000..918804a160 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/EditionReference.scala @@ -0,0 +1,54 @@ +package org.enso.languageserver.libraries + +import io.circe.{Decoder, DecodingFailure, Encoder, Json} +import io.circe.syntax._ +import io.circe.generic.auto._ + +/** A reference to an edition - either a named edition or an unnamed one + * associated with the current project. + */ +sealed trait EditionReference +object EditionReference { + + /** An edition identified by its name. */ + case class NamedEdition(editionName: String) extends EditionReference + + /** The edition associated with the current project. */ + case object CurrentProjectEdition extends EditionReference + + object CodecField { + val Type = "type" + val EditionName = "editionName" + } + + object CodecType { + val NamedEdition = "NamedEdition" + val CurrentProjectEdition = "CurrentProjectEdition" + } + + implicit val encoder: Encoder[EditionReference] = { + case NamedEdition(editionName) => + Json.obj( + CodecField.Type -> CodecType.NamedEdition.asJson, + CodecField.EditionName -> editionName.asJson + ) + case CurrentProjectEdition => + Json.obj(CodecField.Type -> CodecType.CurrentProjectEdition.asJson) + } + + implicit val decoder: Decoder[EditionReference] = { json => + val typeCursor = json.downField(CodecField.Type) + typeCursor.as[String].flatMap { + case CodecType.NamedEdition => + Decoder[NamedEdition].tryDecode(json) + case CodecType.CurrentProjectEdition => Right(CurrentProjectEdition) + case unknownType => + Left( + DecodingFailure( + s"Unknown EditionReference type [$unknownType].", + typeCursor.history + ) + ) + } + } +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/EditionReferenceResolver.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/EditionReferenceResolver.scala new file mode 100644 index 0000000000..9060558ad0 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/EditionReferenceResolver.scala @@ -0,0 +1,48 @@ +package org.enso.languageserver.libraries + +import org.enso.editions.provider.EditionProvider +import org.enso.editions.{DefaultEdition, EditionResolver, Editions} +import org.enso.languageserver.libraries.EditionReference.NamedEdition +import org.enso.pkg.PackageManager + +import java.io.File +import scala.util.Try + +/** Resolves [[EditionReference]] to a raw or resolved edition. */ +class EditionReferenceResolver( + projectRoot: File, + editionProvider: EditionProvider, + editionResolver: EditionResolver +) { + private lazy val projectPackage = + PackageManager.Default.loadPackage(projectRoot).get + + /** Loads the raw edition corresponding to the given [[EditionReference]]. */ + def resolveReference( + editionReference: EditionReference + ): Try[Editions.RawEdition] = editionReference match { + case EditionReference.NamedEdition(editionName) => + editionProvider.findEditionForName(editionName) + case EditionReference.CurrentProjectEdition => + Try { + projectPackage.config.edition.getOrElse { + // TODO [RW] default edition from config (#1864) + DefaultEdition.getDefaultEdition + } + } + } + + /** Resolves all edition dependencies of an edition identified by + * [[EditionReference]]. + */ + def resolveEdition( + editionReference: EditionReference + ): Try[Editions.ResolvedEdition] = for { + raw <- resolveReference(editionReference) + resolved <- editionResolver.resolve(raw).toTry + } yield resolved + + /** Resolves all edition dependencies of an edition identified by its name. */ + def resolveEdition(name: String): Try[Editions.ResolvedEdition] = + resolveEdition(NamedEdition(name)) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/FakeDownload.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/FakeDownload.scala new file mode 100644 index 0000000000..49a783845d --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/FakeDownload.scala @@ -0,0 +1,48 @@ +package org.enso.languageserver.libraries + +import org.enso.cli.task.{ + ProgressReporter, + ProgressUnit, + TaskProgress, + TaskProgressImplementation +} + +import scala.util.Success + +/** A temporary helper for mocked parts of the API. + * + * It should be removed soon, when the missing parts are implemented. + */ +object FakeDownload { + + /** Creates a [[TaskProgress]] which reports progress updates for a few seconds. + * + * Intended for mocking download-like endpoints. + */ + def make(seconds: Int = 10): TaskProgress[Unit] = { + val tracker = new TaskProgressImplementation[Unit](ProgressUnit.Bytes) + val thread = new Thread(() => { + val n = (seconds * 10).toLong + for (i <- 0L to n) { + tracker.reportProgress(i, Some(n)) + Thread.sleep(100) + } + tracker.setComplete(Success(())) + }) + thread.start() + tracker + } + + /** Simulates a download operation reporting progress updates to the + * [[ProgressReporter]]. + */ + def simulateDownload( + message: String, + progressReporter: ProgressReporter, + seconds: Int = 10 + ): Unit = { + val download = make(seconds = seconds) + progressReporter.trackProgress(message, download) + download.force() + } +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryApi.scala new file mode 100644 index 0000000000..e6c04dc883 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryApi.scala @@ -0,0 +1,235 @@ +package org.enso.languageserver.libraries + +import io.circe.Json +import io.circe.literal.JsonStringContext +import org.enso.editions.{LibraryName, LibraryVersion} +import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused} + +object LibraryApi { + case object EditionsListAvailable extends Method("editions/listAvailable") { + self => + + case class Params(update: Boolean) + + case class Result(editionNames: Seq[String]) + + implicit val hasParams = new HasParams[this.type] { + type Params = self.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = self.Result + } + } + + case object EditionsResolve extends Method("editions/resolve") { + self => + + case class Params(edition: EditionReference) + + case class Result(engineVersion: String) + + implicit val hasParams = new HasParams[this.type] { + type Params = self.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = self.Result + } + } + + case object EditionsGetProjectSettings + extends Method("editions/getProjectSettings") { self => + + case class Result( + parentEdition: Option[String], + preferLocalLibraries: Boolean + ) + + implicit val hasParams = new HasParams[this.type] { + type Params = Unused.type + } + implicit val hasResult = new HasResult[this.type] { + type Result = self.Result + } + } + + case object EditionsSetParentEdition + extends Method("editions/setParentEdition") { self => + + case class Params(newEditionName: String) + + case class Result(needsRestart: Option[Boolean]) + + implicit val hasParams = new HasParams[this.type] { + type Params = self.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = self.Result + } + } + + case object EditionsSetLocalLibrariesPreference + extends Method("editions/setProjectLocalLibrariesPreference") { self => + + case class Params(preferLocalLibraries: Boolean) + + case class Result(needsRestart: Option[Boolean]) + + implicit val hasParams = new HasParams[this.type] { + type Params = self.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = self.Result + } + } + + case object EditionsListDefinedLibraries + extends Method("editions/listDefinedLibraries") { self => + + case class Params(edition: EditionReference) + + case class Result(availableLibraries: Seq[LibraryEntry]) + + implicit val hasParams = new HasParams[this.type] { + type Params = self.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = self.Result + } + } + + case object LibraryListLocal extends Method("library/listLocal") { self => + + case class Result(localLibraries: Seq[LibraryEntry]) + + implicit val hasParams = new HasParams[this.type] { + type Params = Unused.type + } + implicit val hasResult = new HasResult[this.type] { + type Result = self.Result + } + } + + case object LibraryCreate extends Method("library/create") { self => + + case class Params( + namespace: String, + name: String, + authors: Seq[String], + maintainers: Seq[String], + license: String + ) + + implicit val hasParams = new HasParams[this.type] { + type Params = self.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = Unused.type + } + } + + case object LibraryGetMetadata extends Method("library/getMetadata") { self => + + case class Params(namespace: String, name: String) + + case class Result(description: Option[String], tagLine: Option[String]) + + implicit val hasParams = new HasParams[this.type] { + type Params = self.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = self.Result + } + } + + case object LibrarySetMetadata extends Method("library/setMetadata") { self => + + case class Params( + namespace: String, + name: String, + description: Option[String], + tagLine: Option[String] + ) + + implicit val hasParams = new HasParams[this.type] { + type Params = self.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = Unused.type + } + } + + case object LibraryPublish extends Method("library/publish") { self => + + case class Params( + namespace: String, + name: String, + authToken: String, + bumpVersionAfterPublish: Option[Boolean] + ) + + implicit val hasParams = new HasParams[this.type] { + type Params = self.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = Unused.type + } + } + + case object LibraryPreinstall extends Method("library/preinstall") { self => + + case class Params(namespace: String, name: String) + + implicit val hasParams = new HasParams[this.type] { + type Params = self.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = Unused.type + } + } + + case class EditionNotFoundError(editionName: String) + extends Error(8001, s"Edition [$editionName] could not be found.") { + override def payload: Option[Json] = Some( + json""" { "editionName" : $editionName } """ + ) + } + + case class LibraryAlreadyExists(libraryName: LibraryName) + extends Error(8002, s"Library [$libraryName] already exists.") + + case class LibraryRepositoryAuthenticationError(reason: String) + extends Error(8003, s"Authentication failed: $reason.") + + case class LibraryPublishError(reason: String) + extends Error(8004, s"Could not publish the library: $reason.") + + case class LibraryUploadError(reason: String) + extends Error(8005, s"Could not upload the library: $reason.") + + case class LibraryDownloadError( + name: LibraryName, + version: LibraryVersion, + reason: String + ) extends Error(8006, s"Could not download the library: $reason.") { + override def payload: Option[Json] = Some( + json""" { + "namespace" : ${name.namespace}, + "name" : ${name.name}, + "version" : ${version.toString} + } """ + ) + } + + case class LocalLibraryNotFound(libraryName: LibraryName) + extends Error(8007, s"Local library [$libraryName] has not been found.") + + case class LibraryNotResolved(name: LibraryName) + extends Error(8008, s"Could not resolve [$name].") { + override def payload: Option[Json] = Some( + json""" { + "namespace" : ${name.namespace}, + "name" : ${name.name} + } """ + ) + } +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryEntry.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryEntry.scala new file mode 100644 index 0000000000..9397c13589 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryEntry.scala @@ -0,0 +1,91 @@ +package org.enso.languageserver.libraries + +import io.circe.generic.semiauto._ +import io.circe.syntax._ +import io.circe.{Decoder, DecodingFailure, Encoder, Json} +import org.enso.editions + +/** An entry in library lists sent to the client. + * + * @param namespace namespace of the library + * @param name name of the library + * @param version version of the library + */ +case class LibraryEntry( + namespace: String, + name: String, + version: LibraryEntry.LibraryVersion +) + +object LibraryEntry { + + /** Version of a library. */ + sealed trait LibraryVersion + + /** A library version that references a locally editable version of the + * library. + */ + case object LocalLibraryVersion extends LibraryVersion + + /** A library version that references a version of the library published in + * some repository. + */ + case class PublishedLibraryVersion(version: String, repositoryUrl: String) + extends LibraryVersion + + /** Converts an instance of [[editions.LibraryVersion]] into one that is used + * in the Language Server protocol. + */ + implicit def convertLibraryVersion( + libraryVersion: editions.LibraryVersion + ): LibraryVersion = libraryVersion match { + case editions.LibraryVersion.Local => LocalLibraryVersion + case editions.LibraryVersion.Published(version, repository) => + PublishedLibraryVersion(version.toString, repository.url) + } + + implicit val encoder: Encoder[LibraryEntry] = deriveEncoder[LibraryEntry] + implicit val decoder: Decoder[LibraryEntry] = deriveDecoder[LibraryEntry] + + object CodecField { + val Type = "type" + val Version = "version" + val RepositoryUrl = "repositoryUrl" + } + + object CodecType { + val LocalLibraryVersion = "LocalLibraryVersion" + val PublishedLibraryVersion = "PublishedLibraryVersion" + } + + implicit val versionEncoder: Encoder[LibraryVersion] = { + case LocalLibraryVersion => + Json.obj(CodecField.Type -> CodecType.LocalLibraryVersion.asJson) + case PublishedLibraryVersion(version, repositoryUrl) => + Json.obj( + CodecField.Type -> CodecType.PublishedLibraryVersion.asJson, + CodecField.Version -> version.asJson, + CodecField.RepositoryUrl -> repositoryUrl.asJson + ) + } + + implicit val versionDecoder: Decoder[LibraryVersion] = { json => + val typeCursor = json.downField(CodecField.Type) + typeCursor.as[String].flatMap { + case CodecType.LocalLibraryVersion => + Right(LocalLibraryVersion) + case CodecType.PublishedLibraryVersion => + for { + version <- json.get[String](CodecField.Version) + repositoryUrl <- json.get[String](CodecField.RepositoryUrl) + } yield PublishedLibraryVersion(version, repositoryUrl) + case unknownType => + Left( + DecodingFailure( + s"Unknown LibraryVersion type [$unknownType].", + typeCursor.history + ) + ) + } + } +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala new file mode 100644 index 0000000000..cde1650435 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala @@ -0,0 +1,123 @@ +package org.enso.languageserver.libraries + +import akka.actor.{Actor, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.distribution.{DistributionManager, FileSystem} +import org.enso.editions.{Editions, LibraryName} +import org.enso.languageserver.libraries.LocalLibraryManagerProtocol._ +import org.enso.librarymanager.local.LocalLibraryProvider +import org.enso.pkg.PackageManager + +import java.io.File +import java.nio.file.Files +import scala.util.{Failure, Success, Try} + +/** An Actor that manages local libraries. */ +class LocalLibraryManager( + currentProjectRoot: File, + distributionManager: DistributionManager +) extends Actor + with LazyLogging { + override def receive: Receive = { case request: Request => + request match { + case GetMetadata(_) => + logger.warn( + "Getting local library metadata is currently not implemented." + ) + sender() ! Success(GetMetadataResponse(None, None)) + case SetMetadata(_, _, _) => + logger.error( + "Setting local library metadata is currently not implemented." + ) + sender() ! Failure(new NotImplementedError()) + case ListLocalLibraries => + sender() ! listLocalLibraries() + case Create(libraryName, authors, maintainers, license) => + sender() ! createLibrary(libraryName, authors, maintainers, license) + case Publish(_, _, _) => + logger.error("Publishing libraries is currently not implemented.") + sender() ! Failure(new NotImplementedError()) + } + } + + /** Creates a new local library project. + * + * The project is created in the first directory of the local library search + * path that is writable. + */ + private def createLibrary( + libraryName: LibraryName, + authors: Seq[String], + maintainers: Seq[String], + license: String + ): Try[Unit] = Try { + // TODO [RW] modify protocol to be able to create Contact instances + val _ = (authors, maintainers) + + // TODO [RW] make the exceptions more relevant + val possibleRoots = LazyList + .from(distributionManager.paths.localLibrariesSearchPaths) + .filter { path => + Try { if (Files.notExists(path)) Files.createDirectories(path) } + Files.isWritable(path) + } + val librariesRoot = possibleRoots.headOption.getOrElse { + throw new RuntimeException( + "Cannot find a writable directory on local library path." + ) + } + + val libraryPath = + LocalLibraryProvider.resolveLibraryPath(librariesRoot, libraryName) + if (Files.exists(libraryPath)) { + throw new RuntimeException("Local library already exists") + } + + PackageManager.Default.create( + libraryPath.toFile, + name = libraryName.name, + namespace = libraryName.namespace, + edition = findCurrentProjectEdition(), + license = license + ) + } + + /** Lists all local libraries. */ + private def listLocalLibraries(): Try[ListLocalLibrariesResponse] = for { + libraryNames <- findLocalLibraries() + libraryEntries = libraryNames.distinct.map { name => + LibraryEntry(name.namespace, name.name, LibraryEntry.LocalLibraryVersion) + } + } yield ListLocalLibrariesResponse(libraryEntries) + + private def findLocalLibraries(): Try[Seq[LibraryName]] = Try { + for { + searchPathRoot <- distributionManager.paths.localLibrariesSearchPaths + namespaceDir <- FileSystem + .listDirectory(searchPathRoot) + .filter(Files.isDirectory(_)) + nameDir <- FileSystem + .listDirectory(namespaceDir) + .filter(Files.isDirectory(_)) + namespace = namespaceDir.getFileName.toString + name = nameDir.getFileName.toString + } yield LibraryName(namespace, name) + } + + /** Finds the edition associated with the current project, if specified in its + * config. + */ + private def findCurrentProjectEdition(): Option[Editions.RawEdition] = { + val pkg = PackageManager.Default.loadPackage(currentProjectRoot).get + pkg.config.edition + } +} + +object LocalLibraryManager { + def props( + currentProjectRoot: File, + distributionManager: DistributionManager + ): Props = Props( + new LocalLibraryManager(currentProjectRoot, distributionManager) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManagerProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManagerProtocol.scala new file mode 100644 index 0000000000..9f3996509c --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManagerProtocol.scala @@ -0,0 +1,46 @@ +package org.enso.languageserver.libraries + +import org.enso.editions.LibraryName + +object LocalLibraryManagerProtocol { + + /** A top class representing any request to the [[LocalLibraryManager]]. */ + sealed trait Request + + /** A request to get metadata of a library. */ + case class GetMetadata(libraryName: LibraryName) extends Request + + /** Response to [[GetMetadata]]. */ + case class GetMetadataResponse( + description: Option[String], + tagLine: Option[String] + ) + + /** A request to update metadata of a library. */ + case class SetMetadata( + libraryName: LibraryName, + description: Option[String], + tagLine: Option[String] + ) extends Request + + /** A request to list local libraries. */ + case object ListLocalLibraries extends Request + + /** A response to [[ListLocalLibraries]]. */ + case class ListLocalLibrariesResponse(libraries: Seq[LibraryEntry]) + + /** A request to create a new library project. */ + case class Create( + libraryName: LibraryName, + authors: Seq[String], + maintainers: Seq[String], + license: String + ) extends Request + + /** A request to publish a library. */ + case class Publish( + libraryName: LibraryName, + authToken: String, + bumpVersionAfterPublish: Boolean + ) extends Request +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/ProjectSettingsManager.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/ProjectSettingsManager.scala new file mode 100644 index 0000000000..d950eb7cd6 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/ProjectSettingsManager.scala @@ -0,0 +1,80 @@ +package org.enso.languageserver.libraries + +import akka.actor.{Actor, Props} +import org.enso.editions.{DefaultEdition, EditionResolver, Editions} +import org.enso.pkg.PackageManager + +import java.io.File +import scala.util.Try + +/** An Actor that manages edition-related settings of the current project. */ +class ProjectSettingsManager( + projectRoot: File, + editionResolver: EditionResolver +) extends Actor { + import ProjectSettingsManager._ + + override def receive: Receive = { case request: Request => + request match { + case GetSettings => + sender() ! loadSettings() + case SetParentEdition(editionName) => + sender() ! setParentEdition(editionName) + case SetPreferLocalLibraries(preferLocalLibraries) => + sender() ! setPreferLocalLibraries(preferLocalLibraries) + } + } + + private def loadSettings(): Try[SettingsResponse] = for { + pkg <- PackageManager.Default.loadPackage(projectRoot) + edition = pkg.config.edition.getOrElse(DefaultEdition.getDefaultEdition) + } yield SettingsResponse(edition.parent, pkg.config.preferLocalLibraries) + + private def setParentEdition(editionName: String): Try[Unit] = for { + pkg <- PackageManager.Default.loadPackage(projectRoot) + newEdition = pkg.config.edition match { + case Some(edition) => edition.copy(parent = Some(editionName)) + case None => Editions.Raw.Edition(parent = Some(editionName)) + } + _ <- editionResolver.resolve(newEdition).toTry + updated = pkg.updateConfig { config => + config.copy(edition = Some(newEdition)) + } + _ <- updated.save() + } yield () + + private def setPreferLocalLibraries( + preferLocalLibraries: Boolean + ): Try[Unit] = for { + pkg <- PackageManager.Default.loadPackage(projectRoot) + updated = pkg.updateConfig { config => + config.copy(preferLocalLibraries = preferLocalLibraries) + } + _ <- updated.save() + } yield () +} + +object ProjectSettingsManager { + def props(projectRoot: File, editionResolver: EditionResolver): Props = Props( + new ProjectSettingsManager(projectRoot, editionResolver) + ) + + /** A request to the [[ProjectSettingsManager]]. */ + sealed trait Request + + /** A request to get the current project settings. */ + case object GetSettings extends Request + + /** Response to [[GetSettings]]. */ + case class SettingsResponse( + parentEdition: Option[String], + preferLocalLibraries: Boolean + ) + + /** A request to set the parent edition for the project. */ + case class SetParentEdition(editionName: String) extends Request + + /** A request to set the local libraries preference. */ + case class SetPreferLocalLibraries(preferLocalLibraries: Boolean) + extends Request +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsGetProjectSettingsHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsGetProjectSettingsHandler.scala new file mode 100644 index 0000000000..706420d2cf --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsGetProjectSettingsHandler.scala @@ -0,0 +1,81 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, ActorRef, Cancellable, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult} +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.libraries.ProjectSettingsManager +import org.enso.languageserver.requesthandler.RequestTimeout +import org.enso.languageserver.util.UnhandledLogging + +import scala.concurrent.duration.FiniteDuration +import scala.util.{Failure, Success} + +/** A request handler for the `editions/getProjectSettings` endpoint. + * + * @param timeout request timeout + * @param projectSettingsManager a reference to the [[ProjectSettingsManager]] + */ +class EditionsGetProjectSettingsHandler( + timeout: FiniteDuration, + projectSettingsManager: ActorRef +) extends Actor + with LazyLogging + with UnhandledLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request(EditionsGetProjectSettings, id, _) => + projectSettingsManager ! ProjectSettingsManager.GetSettings + val cancellable = + context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) + context.become(responseStage(id, sender(), cancellable)) + } + + private def responseStage( + id: Id, + replyTo: ActorRef, + cancellable: Cancellable + ): Receive = { + case RequestTimeout => + replyTo ! RequestTimeout + context.stop(self) + + case Success(settings: ProjectSettingsManager.SettingsResponse) => + replyTo ! ResponseResult( + EditionsGetProjectSettings, + id, + EditionsGetProjectSettings.Result( + parentEdition = settings.parentEdition, + preferLocalLibraries = settings.preferLocalLibraries + ) + ) + cancellable.cancel() + context.stop(self) + + case Failure(exception) => + replyTo ! ResponseError(Some(id), FileSystemError(exception.getMessage)) + cancellable.cancel() + context.stop(self) + } + +} + +object EditionsGetProjectSettingsHandler { + + /** Creates a configuration object to create + * [[EditionsGetProjectSettingsHandler]]. + * + * @param timeout request timeout + * @param projectSettingsManager a reference to the + * [[ProjectSettingsManager]] + */ + def props(timeout: FiniteDuration, projectSettingsManager: ActorRef): Props = + Props( + new EditionsGetProjectSettingsHandler(timeout, projectSettingsManager) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsListAvailableHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsListAvailableHandler.scala new file mode 100644 index 0000000000..847e9fce50 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsListAvailableHandler.scala @@ -0,0 +1,52 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.distribution.EditionManager +import org.enso.jsonrpc.{Request, ResponseError, ResponseResult} +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.util.UnhandledLogging + +import scala.util.{Failure, Success, Try} + +/** A request handler for the `editions/listAvailable` endpoint. + * + * It is a partial implementation - it already allows to list existing + * editions, but updating is not yet implemented. + * + * @param editionManager an edition manager instance + */ +class EditionsListAvailableHandler(editionManager: EditionManager) + extends Actor + with LazyLogging + with UnhandledLogging { + override def receive: Receive = { + case Request(EditionsListAvailable, id, _: EditionsListAvailable.Params) => + // TODO [RW] once updating editions is implemented this should be made asynchronous + Try(editionManager.findAllAvailableEditions()) match { + case Success(editions) => + sender() ! ResponseResult( + EditionsListAvailable, + id, + EditionsListAvailable.Result(editions) + ) + case Failure(exception) => + sender() ! ResponseError( + Some(id), + FileSystemError(exception.toString) + ) + } + } +} + +object EditionsListAvailableHandler { + + /** Creates a configuration object to create [[EditionsListAvailableHandler]]. + * + * @param editionManager an edition manager instance + */ + def props(editionManager: EditionManager): Props = Props( + new EditionsListAvailableHandler(editionManager) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsListDefinedLibrariesHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsListDefinedLibrariesHandler.scala new file mode 100644 index 0000000000..bb6d7e925f --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsListDefinedLibrariesHandler.scala @@ -0,0 +1,69 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc.{Request, ResponseError, ResponseResult} +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.{ + EditionReferenceResolver, + LibraryEntry +} +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.util.UnhandledLogging + +import scala.util.{Failure, Success} + +/** A request handler for the `editions/listDefinedLibraries` endpoint. + * + * @param editionReferenceResolver an [[EditionReferenceResolver]] instance + */ +class EditionsListDefinedLibrariesHandler( + editionReferenceResolver: EditionReferenceResolver +) extends Actor + with LazyLogging + with UnhandledLogging { + override def receive: Receive = { + case Request( + EditionsListDefinedLibraries, + id, + EditionsListDefinedLibraries.Params(reference) + ) => + val result = for { + edition <- editionReferenceResolver.resolveEdition(reference) + } yield edition.getAllDefinedLibraries.toSeq.map { case (name, version) => + LibraryEntry( + namespace = name.namespace, + name = name.name, + version = version + ) + } + + result match { + case Success(libraries) => + sender() ! ResponseResult( + EditionsListDefinedLibraries, + id, + EditionsListDefinedLibraries.Result(libraries) + ) + + case Failure(exception) => + // TODO [RW] more detailed errors + sender() ! ResponseError( + Some(id), + FileSystemError(exception.getMessage) + ) + } + } +} + +object EditionsListDefinedLibrariesHandler { + + /** Creates a configuration object to create + * [[EditionsListDefinedLibrariesHandler]]. + * + * @param editionReferenceResolver an [[EditionReferenceResolver]] instance + */ + def props(editionReferenceResolver: EditionReferenceResolver): Props = Props( + new EditionsListDefinedLibrariesHandler(editionReferenceResolver) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsResolveHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsResolveHandler.scala new file mode 100644 index 0000000000..af8dda4b3c --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsResolveHandler.scala @@ -0,0 +1,54 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc.{Request, ResponseError, ResponseResult} +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.EditionReferenceResolver +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.util.UnhandledLogging + +import scala.util.{Failure, Success} + +/** A request handler for the `editions/resolve` endpoint. + * + * @param editionReferenceResolver an [[EditionReferenceResolver]] instance + */ +class EditionsResolveHandler(editionReferenceResolver: EditionReferenceResolver) + extends Actor + with LazyLogging + with UnhandledLogging { + override def receive: Receive = { + case Request(EditionsResolve, id, EditionsResolve.Params(reference)) => + val result = for { + edition <- editionReferenceResolver.resolveEdition(reference) + } yield edition.getEngineVersion + + result match { + case Success(engineVersion) => + sender() ! ResponseResult( + EditionsResolve, + id, + EditionsResolve.Result(engineVersion.toString) + ) + + case Failure(exception) => + // TODO [RW] more detailed errors + sender() ! ResponseError( + Some(id), + FileSystemError(exception.getMessage) + ) + } + } +} + +object EditionsResolveHandler { + + /** Creates a configuration object to create [[EditionsResolveHandler]]. + * + * @param editionReferenceResolver an [[EditionReferenceResolver]] instance + */ + def props(editionReferenceResolver: EditionReferenceResolver): Props = Props( + new EditionsResolveHandler(editionReferenceResolver) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsSetParentEditionHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsSetParentEditionHandler.scala new file mode 100644 index 0000000000..6d0b8c5fbe --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsSetParentEditionHandler.scala @@ -0,0 +1,88 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, ActorRef, Cancellable, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult} +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.libraries.ProjectSettingsManager +import org.enso.languageserver.requesthandler.RequestTimeout +import org.enso.languageserver.util.UnhandledLogging + +import scala.concurrent.duration.FiniteDuration +import scala.util.{Failure, Success} + +/** A request handler for the `editions/setParentEdition` endpoint. + * + * @param timeout request timeout + * @param projectSettingsManager a reference to the [[ProjectSettingsManager]] + */ +class EditionsSetParentEditionHandler( + timeout: FiniteDuration, + projectSettingsManager: ActorRef +) extends Actor + with LazyLogging + with UnhandledLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request( + EditionsSetParentEdition, + id, + EditionsSetParentEdition.Params(newEditionName) + ) => + projectSettingsManager ! ProjectSettingsManager.SetParentEdition( + newEditionName + ) + val cancellable = + context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) + context.become(responseStage(id, sender(), cancellable)) + } + + private def responseStage( + id: Id, + replyTo: ActorRef, + cancellable: Cancellable + ): Receive = { + case RequestTimeout => + replyTo ! RequestTimeout + context.stop(self) + + case Success(_) => + replyTo ! ResponseResult( + EditionsSetParentEdition, + id, + EditionsSetParentEdition.Result(needsRestart = Some(true)) + ) + cancellable.cancel() + context.stop(self) + + case Failure(exception) => + replyTo ! ResponseError( + Some(id), + FileSystemError( + s"Failed to update the settings: ${exception.getMessage}" + ) + ) + cancellable.cancel() + context.stop(self) + } +} + +object EditionsSetParentEditionHandler { + + /** Creates a configuration object to create + * [[EditionsSetParentEditionHandler]]. + * + * @param timeout request timeout + * @param projectSettingsManager a reference to the + * [[ProjectSettingsManager]] + */ + def props(timeout: FiniteDuration, projectSettingsManager: ActorRef): Props = + Props( + new EditionsSetParentEditionHandler(timeout, projectSettingsManager) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsSetProjectLocalLibrariesPreferenceHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsSetProjectLocalLibrariesPreferenceHandler.scala new file mode 100644 index 0000000000..e58b90e5c2 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsSetProjectLocalLibrariesPreferenceHandler.scala @@ -0,0 +1,94 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, ActorRef, Cancellable, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult} +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.libraries.ProjectSettingsManager +import org.enso.languageserver.requesthandler.RequestTimeout +import org.enso.languageserver.util.UnhandledLogging + +import scala.concurrent.duration.FiniteDuration +import scala.util.{Failure, Success} + +/** A request handler for the `editions/setProjectLocalLibrariesPreference` + * endpoint. + * + * @param timeout request timeout + * @param projectSettingsManager a reference to the [[ProjectSettingsManager]] + */ +class EditionsSetProjectLocalLibrariesPreferenceHandler( + timeout: FiniteDuration, + projectSettingsManager: ActorRef +) extends Actor + with LazyLogging + with UnhandledLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request( + EditionsSetLocalLibrariesPreference, + id, + EditionsSetLocalLibrariesPreference.Params(preferLocalLibraries) + ) => + projectSettingsManager ! ProjectSettingsManager.SetPreferLocalLibraries( + preferLocalLibraries + ) + val cancellable = + context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) + context.become(responseStage(id, sender(), cancellable)) + } + + private def responseStage( + id: Id, + replyTo: ActorRef, + cancellable: Cancellable + ): Receive = { + case RequestTimeout => + replyTo ! RequestTimeout + context.stop(self) + + case Success(_) => + replyTo ! ResponseResult( + EditionsSetLocalLibrariesPreference, + id, + EditionsSetLocalLibrariesPreference.Result(needsRestart = Some(true)) + ) + cancellable.cancel() + context.stop(self) + + case Failure(exception) => + replyTo ! ResponseError( + Some(id), + FileSystemError( + s"Failed to update the settings: ${exception.getMessage}" + ) + ) + cancellable.cancel() + context.stop(self) + } +} + +object EditionsSetProjectLocalLibrariesPreferenceHandler { + + /** Creates a configuration object to create + * [[EditionsSetProjectLocalLibrariesPreferenceHandler]]. + * + * @param timeout request timeout + * @param projectSettingsManager a reference to the + * [[ProjectSettingsManager]] + */ + def props( + timeout: FiniteDuration, + projectSettingsManager: ActorRef + ): Props = Props( + new EditionsSetProjectLocalLibrariesPreferenceHandler( + timeout, + projectSettingsManager + ) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryCreateHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryCreateHandler.scala new file mode 100644 index 0000000000..ebebc5406c --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryCreateHandler.scala @@ -0,0 +1,81 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, ActorRef, Cancellable, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.editions.LibraryName +import org.enso.jsonrpc._ +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.libraries.LocalLibraryManagerProtocol +import org.enso.languageserver.requesthandler.RequestTimeout +import org.enso.languageserver.util.UnhandledLogging + +import scala.concurrent.duration.FiniteDuration +import scala.util.{Failure, Success} + +/** A request handler for the `library/create` endpoint. + * + * @param timeout request timeout + * @param localLibraryManager a reference to the LocalLibraryManager + */ +class LibraryCreateHandler( + timeout: FiniteDuration, + localLibraryManager: ActorRef +) extends Actor + with LazyLogging + with UnhandledLogging { + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request( + LibraryCreate, + id, + LibraryCreate.Params(namespace, name, authors, maintainers, license) + ) => + localLibraryManager ! LocalLibraryManagerProtocol.Create( + LibraryName(namespace, name), + authors = authors, + maintainers = maintainers, + license = license + ) + val cancellable = + context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) + context.become(responseStage(id, sender(), cancellable)) + } + + private def responseStage( + id: Id, + replyTo: ActorRef, + cancellable: Cancellable + ): Receive = { + case RequestTimeout => + replyTo ! RequestTimeout + context.stop(self) + + case Success(_) => + replyTo ! ResponseResult(LibraryCreate, id, Unused) + cancellable.cancel() + context.stop(self) + + case Failure(exception) => + // TODO [RW] handle LibraryAlreadyExists error + replyTo ! ResponseError(Some(id), FileSystemError(exception.getMessage)) + cancellable.cancel() + context.stop(self) + } +} + +object LibraryCreateHandler { + + /** Creates a configuration object to create [[LibraryCreateHandler]]. + * + * @param timeout request timeout + * @param localLibraryManager a reference to the LocalLibraryManager + */ + def props(timeout: FiniteDuration, localLibraryManager: ActorRef): Props = + Props( + new LibraryCreateHandler(timeout, localLibraryManager) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryGetMetadataHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryGetMetadataHandler.scala new file mode 100644 index 0000000000..bc88a9906a --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryGetMetadataHandler.scala @@ -0,0 +1,32 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc.{Request, ResponseResult} +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.util.UnhandledLogging + +/** A request handler for the `library/create` endpoint. + * + * It is currently a stub implementation which will be refined later on. + */ +class LibraryGetMetadataHandler + extends Actor + with LazyLogging + with UnhandledLogging { + override def receive: Receive = { + case Request(LibraryGetMetadata, id, _: LibraryGetMetadata.Params) => + // TODO [RW] actual implementation + sender() ! ResponseResult( + LibraryGetMetadata, + id, + LibraryGetMetadata.Result(None, None) + ) + } +} + +object LibraryGetMetadataHandler { + + /** Creates a configuration object to create [[LibraryGetMetadataHandler]]. */ + def props(): Props = Props(new LibraryGetMetadataHandler) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryListLocalHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryListLocalHandler.scala new file mode 100644 index 0000000000..c14eebb022 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryListLocalHandler.scala @@ -0,0 +1,74 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, ActorRef, Cancellable, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc._ +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.libraries.LocalLibraryManagerProtocol +import org.enso.languageserver.requesthandler.RequestTimeout +import org.enso.languageserver.util.UnhandledLogging + +import scala.concurrent.duration.FiniteDuration +import scala.util.{Failure, Success} + +/** A request handler for the `library/listLocal` endpoint. + * + * @param timeout request timeout + * @param localLibraryManager a reference to the LocalLibraryManager + */ +class LibraryListLocalHandler( + timeout: FiniteDuration, + localLibraryManager: ActorRef +) extends Actor + with LazyLogging + with UnhandledLogging { + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { case Request(LibraryListLocal, id, _) => + localLibraryManager ! LocalLibraryManagerProtocol.ListLocalLibraries + val cancellable = + context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) + context.become(responseStage(id, sender(), cancellable)) + } + + private def responseStage( + id: Id, + replyTo: ActorRef, + cancellable: Cancellable + ): Receive = { + case RequestTimeout => + replyTo ! RequestTimeout + context.stop(self) + + case Success( + LocalLibraryManagerProtocol.ListLocalLibrariesResponse(libraries) + ) => + replyTo ! ResponseResult( + LibraryListLocal, + id, + LibraryListLocal.Result(libraries) + ) + cancellable.cancel() + context.stop(self) + + case Failure(exception) => + replyTo ! ResponseError(Some(id), FileSystemError(exception.getMessage)) + cancellable.cancel() + context.stop(self) + } +} +object LibraryListLocalHandler { + + /** Creates a configuration object to create [[LibraryListLocalHandler]]. + * + * @param timeout request timeout + * @param localLibraryManager a reference to the LocalLibraryManager + */ + def props(timeout: FiniteDuration, localLibraryManager: ActorRef): Props = + Props( + new LibraryListLocalHandler(timeout, localLibraryManager) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPreinstallHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPreinstallHandler.scala new file mode 100644 index 0000000000..0dd2b4c8da --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPreinstallHandler.scala @@ -0,0 +1,56 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.cli.task.notifications.ActorProgressNotificationForwarder +import org.enso.jsonrpc.{Request, ResponseError} +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.FakeDownload +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.util.UnhandledLogging + +/** A request handler for the `library/preinstall` endpoint. + * + * It is currently a stub implementation which will be refined later on. + */ +class LibraryPreinstallHandler + extends Actor + with LazyLogging + with UnhandledLogging { + override def receive: Receive = { + case Request(LibraryPreinstall, id, LibraryPreinstall.Params(_, name)) => + // TODO [RW] actual implementation + val progressReporter = + ActorProgressNotificationForwarder.translateAndForward( + LibraryPreinstall.name, + sender() + ) + + if (name == "Test") { + FakeDownload.simulateDownload( + "Download Test", + progressReporter, + seconds = 1 + ) + } else { + FakeDownload.simulateDownload( + "Downloading something...", + progressReporter + ) + FakeDownload.simulateDownload( + "Downloading something else...", + progressReporter + ) + } + sender() ! ResponseError( + Some(id), + FileSystemError("Feature not implemented") + ) + } +} + +object LibraryPreinstallHandler { + + /** Creates a configuration object to create [[LibraryPreinstallHandler]]. */ + def props(): Props = Props(new LibraryPreinstallHandler) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala new file mode 100644 index 0000000000..0b0095b454 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala @@ -0,0 +1,32 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc.{Request, ResponseError} +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.util.UnhandledLogging + +/** A request handler for the `library/publish` endpoint. + * + * It is currently a stub implementation which will be refined later on. + */ +class LibraryPublishHandler + extends Actor + with LazyLogging + with UnhandledLogging { + override def receive: Receive = { + case Request(LibraryPublish, id, _: LibraryPublish.Params) => + // TODO [RW] actual implementation + sender() ! ResponseError( + Some(id), + FileSystemError("Feature not implemented") + ) + } +} + +object LibraryPublishHandler { + + /** Creates a configuration object to create [[LibraryPublishHandler]]. */ + def props(): Props = Props(new LibraryPublishHandler) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibrarySetMetadataHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibrarySetMetadataHandler.scala new file mode 100644 index 0000000000..77a237c33f --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibrarySetMetadataHandler.scala @@ -0,0 +1,32 @@ +package org.enso.languageserver.libraries.handler + +import akka.actor.{Actor, Props} +import com.typesafe.scalalogging.LazyLogging +import org.enso.jsonrpc.{Request, ResponseError} +import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.util.UnhandledLogging + +/** A request handler for the `library/setMetadata` endpoint. + * + * It is currently a stub implementation which will be refined later on. + */ +class LibrarySetMetadataHandler + extends Actor + with LazyLogging + with UnhandledLogging { + override def receive: Receive = { + case Request(LibrarySetMetadata, id, _: LibrarySetMetadata.Params) => + // TODO [RW] actual implementation + sender() ! ResponseError( + Some(id), + FileSystemError("Feature not implemented") + ) + } +} + +object LibrarySetMetadataHandler { + + /** Creates a configuration object to create [[LibrarySetMetadataHandler]]. */ + def props(): Props = Props(new LibrarySetMetadataHandler) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala index 21c9052327..53b157d36e 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala @@ -1,11 +1,12 @@ package org.enso.languageserver.protocol.json -import java.util.UUID - import akka.actor.{Actor, ActorRef, Cancellable, Props, Stash, Status} import akka.pattern.pipe import akka.util.Timeout import com.typesafe.scalalogging.LazyLogging +import org.enso.cli.task.ProgressUnit +import org.enso.cli.task.notifications.TaskNotificationApi +import org.enso.distribution.EditionManager import org.enso.jsonrpc._ import org.enso.languageserver.boot.resource.InitializationComponent import org.enso.languageserver.capability.CapabilityApi.{ @@ -26,6 +27,9 @@ import org.enso.languageserver.filemanager._ import org.enso.languageserver.io.InputOutputApi._ import org.enso.languageserver.io.OutputKind.{StandardError, StandardOutput} import org.enso.languageserver.io.{InputOutputApi, InputOutputProtocol} +import org.enso.languageserver.libraries.EditionReferenceResolver +import org.enso.languageserver.libraries.LibraryApi._ +import org.enso.languageserver.libraries.handler._ import org.enso.languageserver.monitoring.MonitoringApi.{InitialPing, Ping} import org.enso.languageserver.monitoring.MonitoringProtocol import org.enso.languageserver.refactoring.RefactoringApi.RenameProject @@ -66,7 +70,10 @@ import org.enso.languageserver.text.TextApi._ import org.enso.languageserver.text.TextProtocol import org.enso.languageserver.util.UnhandledLogging import org.enso.languageserver.workspace.WorkspaceApi.ProjectInfo +import org.enso.polyglot.runtime.Runtime.Api +import org.enso.polyglot.runtime.Runtime.Api.ProgressNotification +import java.util.UUID import scala.concurrent.duration._ /** An actor handling communications between a single client and the language @@ -81,6 +88,7 @@ import scala.concurrent.duration._ * @param contextRegistry a router that dispatches execution context requests * @param suggestionsHandler a reference to the suggestions requests handler * @param idlenessMonitor a reference to the idleness monitor actor + * @param projectSettingsManager a reference to the project settings manager * @param requestTimeout a request timeout */ class JsonConnectionController( @@ -97,6 +105,10 @@ class JsonConnectionController( val stdInController: ActorRef, val runtimeConnector: ActorRef, val idlenessMonitor: ActorRef, + val projectSettingsManager: ActorRef, + val localLibraryManager: ActorRef, + val editionReferenceResolver: EditionReferenceResolver, + val editionManager: EditionManager, val languageServerConfig: Config, requestTimeout: FiniteDuration = 10.seconds ) extends Actor @@ -230,8 +242,7 @@ class JsonConnectionController( InitProtocolConnection.Result(allRoots.map(_.toContentRoot).toSet) ) - val requestHandlers = createRequestHandlers(rpcSession) - context.become(initialised(webActor, rpcSession, requestHandlers)) + initialize(webActor, rpcSession) } else { context.become( waitingForContentRoots( @@ -254,6 +265,17 @@ class JsonConnectionController( stash() } + private def initialize( + webActor: ActorRef, + rpcSession: JsonSession + ): Unit = { + val requestHandlers = createRequestHandlers(rpcSession) + context.become(initialised(webActor, rpcSession, requestHandlers)) + + context.system.eventStream + .subscribe(self, classOf[Api.ProgressNotification]) + } + private def initialised( webActor: ActorRef, rpcSession: JsonSession, @@ -364,6 +386,11 @@ class JsonConnectionController( ) } + case Api.ProgressNotification(payload) => + val translated: Notification[_, _] = + translateProgressNotification(payload) + webActor ! translated + case req @ Request(method, _, _) if requestHandlers.contains(method) => refreshIdleTime(method) val handler = context.actorOf( @@ -458,10 +485,57 @@ class JsonConnectionController( RedirectStandardError -> RedirectStdErrHandler .props(stdErrController, rpcSession.clientId), FeedStandardInput -> FeedStandardInputHandler.props(stdInController), - ProjectInfo -> ProjectInfoHandler.props(languageServerConfig) + ProjectInfo -> ProjectInfoHandler.props(languageServerConfig), + EditionsGetProjectSettings -> EditionsGetProjectSettingsHandler + .props(requestTimeout, projectSettingsManager), + EditionsListAvailable -> EditionsListAvailableHandler.props( + editionManager + ), + EditionsListDefinedLibraries -> EditionsListDefinedLibrariesHandler + .props(editionReferenceResolver), + EditionsResolve -> EditionsResolveHandler + .props(editionReferenceResolver), + EditionsSetParentEdition -> EditionsSetParentEditionHandler + .props(requestTimeout, projectSettingsManager), + EditionsSetLocalLibrariesPreference -> EditionsSetProjectLocalLibrariesPreferenceHandler + .props(requestTimeout, projectSettingsManager), + LibraryCreate -> LibraryCreateHandler + .props(requestTimeout, localLibraryManager), + LibraryListLocal -> LibraryListLocalHandler + .props(requestTimeout, localLibraryManager), + LibraryGetMetadata -> LibraryGetMetadataHandler.props(), + LibraryPreinstall -> LibraryPreinstallHandler.props(), + LibraryPublish -> LibraryPublishHandler.props(), + LibrarySetMetadata -> LibrarySetMetadataHandler.props() ) } + private def translateProgressNotification( + progressNotification: ProgressNotification.NotificationType + ): Notification[_, _] = progressNotification match { + case ProgressNotification.TaskStarted( + taskId, + relatedOperation, + unitStr, + total + ) => + val unit = ProgressUnit.fromString(unitStr) + Notification( + TaskNotificationApi.TaskStarted, + TaskNotificationApi.TaskStarted + .Params(taskId, relatedOperation, unit, total) + ) + case ProgressNotification.TaskProgressUpdate(taskId, message, done) => + Notification( + TaskNotificationApi.TaskProgressUpdate, + TaskNotificationApi.TaskProgressUpdate.Params(taskId, message, done) + ) + case ProgressNotification.TaskFinished(taskId, message, success) => + Notification( + TaskNotificationApi.TaskFinished, + TaskNotificationApi.TaskFinished.Params(taskId, message, success) + ) + } } object JsonConnectionController { @@ -493,26 +567,34 @@ object JsonConnectionController { stdInController: ActorRef, runtimeConnector: ActorRef, idlenessMonitor: ActorRef, + projectSettingsManager: ActorRef, + localLibraryManager: ActorRef, + editionReferenceResolver: EditionReferenceResolver, + editionManager: EditionManager, languageServerConfig: Config, requestTimeout: FiniteDuration = 10.seconds ): Props = Props( new JsonConnectionController( - connectionId, - mainComponent, - bufferRegistry, - capabilityRouter, - fileManager, - contentRootManager, - contextRegistry, - suggestionsHandler, - stdOutController, - stdErrController, - stdInController, - runtimeConnector, - idlenessMonitor, - languageServerConfig, - requestTimeout + connectionId = connectionId, + mainComponent = mainComponent, + bufferRegistry = bufferRegistry, + capabilityRouter = capabilityRouter, + fileManager = fileManager, + contentRootManager = contentRootManager, + contextRegistry = contextRegistry, + suggestionsHandler = suggestionsHandler, + stdOutController = stdOutController, + stdErrController = stdErrController, + stdInController = stdInController, + runtimeConnector = runtimeConnector, + idlenessMonitor = idlenessMonitor, + projectSettingsManager = projectSettingsManager, + localLibraryManager = localLibraryManager, + editionReferenceResolver = editionReferenceResolver, + editionManager = editionManager, + languageServerConfig = languageServerConfig, + requestTimeout = requestTimeout ) ) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala index d2bec2be2a..b4aff004ef 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala @@ -1,9 +1,11 @@ package org.enso.languageserver.protocol.json import akka.actor.{ActorRef, ActorSystem} +import org.enso.distribution.EditionManager import org.enso.jsonrpc.ClientControllerFactory import org.enso.languageserver.boot.resource.InitializationComponent import org.enso.languageserver.data.Config +import org.enso.languageserver.libraries.EditionReferenceResolver import java.util.UUID @@ -27,6 +29,10 @@ class JsonConnectionControllerFactory( stdInController: ActorRef, runtimeConnector: ActorRef, idlenessMonitor: ActorRef, + projectSettingsManager: ActorRef, + localLibraryManager: ActorRef, + editionReferenceResolver: EditionReferenceResolver, + editionManager: EditionManager, config: Config )(implicit system: ActorSystem) extends ClientControllerFactory { @@ -39,20 +45,24 @@ class JsonConnectionControllerFactory( override def createClientController(clientId: UUID): ActorRef = system.actorOf( JsonConnectionController.props( - clientId, - mainComponent, - bufferRegistry, - capabilityRouter, - fileManager, - contentRootManager, - contextRegistry, - suggestionsHandler, - stdOutController, - stdErrController, - stdInController, - runtimeConnector, - idlenessMonitor, - config + connectionId = clientId, + mainComponent = mainComponent, + bufferRegistry = bufferRegistry, + capabilityRouter = capabilityRouter, + fileManager = fileManager, + contentRootManager = contentRootManager, + contextRegistry = contextRegistry, + suggestionsHandler = suggestionsHandler, + stdOutController = stdOutController, + stdErrController = stdErrController, + stdInController = stdInController, + runtimeConnector = runtimeConnector, + idlenessMonitor = idlenessMonitor, + projectSettingsManager = projectSettingsManager, + localLibraryManager = localLibraryManager, + editionReferenceResolver = editionReferenceResolver, + editionManager = editionManager, + languageServerConfig = config ) ) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala index a1c20f105b..41955f7a3d 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala @@ -1,6 +1,11 @@ package org.enso.languageserver.protocol.json import io.circe.generic.auto._ +import org.enso.cli.task.notifications.TaskNotificationApi.{ + TaskFinished, + TaskProgressUpdate, + TaskStarted +} import org.enso.jsonrpc.Protocol import org.enso.languageserver.capability.CapabilityApi.{ AcquireCapability, @@ -17,6 +22,7 @@ import org.enso.languageserver.search.SearchApi._ import org.enso.languageserver.runtime.VisualisationApi._ import org.enso.languageserver.session.SessionApi.InitProtocolConnection import org.enso.languageserver.text.TextApi._ +import org.enso.languageserver.libraries.LibraryApi._ import org.enso.languageserver.workspace.WorkspaceApi.ProjectInfo object JsonRpc { @@ -65,6 +71,21 @@ object JsonRpc { .registerRequest(Import) .registerRequest(RenameProject) .registerRequest(ProjectInfo) + .registerRequest(EditionsListAvailable) + .registerRequest(EditionsResolve) + .registerRequest(EditionsGetProjectSettings) + .registerRequest(EditionsSetParentEdition) + .registerRequest(EditionsSetLocalLibrariesPreference) + .registerRequest(EditionsListDefinedLibraries) + .registerRequest(LibraryListLocal) + .registerRequest(LibraryCreate) + .registerRequest(LibraryGetMetadata) + .registerRequest(LibrarySetMetadata) + .registerRequest(LibraryPublish) + .registerRequest(LibraryPreinstall) + .registerNotification(TaskStarted) + .registerNotification(TaskProgressUpdate) + .registerNotification(TaskFinished) .registerNotification(ForceReleaseCapability) .registerNotification(GrantCapability) .registerNotification(TextDidChange) diff --git a/engine/language-server/src/test/resources/package.yaml b/engine/language-server/src/test/resources/package.yaml index 2923e3659b..a8ecba3841 100644 --- a/engine/language-server/src/test/resources/package.yaml +++ b/engine/language-server/src/test/resources/package.yaml @@ -1,6 +1,5 @@ license: APLv2 name: Standard -enso-version: default version: "0.1.0" author: "Enso Team " maintainer: "Enso Team " diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/libraries/EditionNameSerializationSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/libraries/EditionNameSerializationSpec.scala new file mode 100644 index 0000000000..168e6b87e6 --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/libraries/EditionNameSerializationSpec.scala @@ -0,0 +1,21 @@ +package org.enso.languageserver.libraries + +import io.circe.syntax._ +import org.enso.languageserver.libraries.EditionReference.{ + CurrentProjectEdition, + NamedEdition +} +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class EditionNameSerializationSpec extends AnyWordSpec with Matchers { + "EditionName" should { + "serialize and deserialize to the same thing" in { + val edition1: EditionReference = CurrentProjectEdition + edition1.asJson.as[EditionReference] shouldEqual Right(edition1) + + val edition2: EditionReference = NamedEdition("Foo-Bar") + edition2.asJson.as[EditionReference] shouldEqual Right(edition2) + } + } +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/libraries/LibraryEntrySerializationSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/libraries/LibraryEntrySerializationSpec.scala new file mode 100644 index 0000000000..7c8cda8273 --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/libraries/LibraryEntrySerializationSpec.scala @@ -0,0 +1,21 @@ +package org.enso.languageserver.libraries + +import io.circe.syntax._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class LibraryEntrySerializationSpec extends AnyWordSpec with Matchers { + "LibraryEntry" should { + "serialize and deserialize to the same thing" in { + val entry1 = LibraryEntry("Foo", "Bar", LibraryEntry.LocalLibraryVersion) + entry1.asJson.as[LibraryEntry] shouldEqual Right(entry1) + + val entry2 = LibraryEntry( + "Foo", + "Bar", + LibraryEntry.PublishedLibraryVersion("1.2.3", "https://example.com/") + ) + entry2.asJson.as[LibraryEntry] shouldEqual Right(entry2) + } + } +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala index cefdcef268..b8fe0dcd02 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala @@ -1,15 +1,15 @@ package org.enso.languageserver.websocket.json -import java.nio.file.Files -import java.util.UUID - import akka.testkit.TestProbe import io.circe.literal._ import io.circe.parser.parse import io.circe.syntax.EncoderOps import org.apache.commons.io.FileUtils +import org.enso.distribution.{DistributionManager, EditionManager, LanguageHome} +import org.enso.editions.EditionResolver import org.enso.jsonrpc.test.JsonRpcServerTestKit import org.enso.jsonrpc.{ClientControllerFactory, Protocol} +import org.enso.languageserver.TestClock import org.enso.languageserver.boot.resource.{ DirectoriesInitialization, RepoInitialization, @@ -21,6 +21,11 @@ import org.enso.languageserver.effect.ZioExec import org.enso.languageserver.event.InitializedEvent import org.enso.languageserver.filemanager._ import org.enso.languageserver.io._ +import org.enso.languageserver.libraries.{ + EditionReferenceResolver, + LocalLibraryManager, + ProjectSettingsManager +} import org.enso.languageserver.monitoring.IdlenessMonitor import org.enso.languageserver.protocol.json.{ JsonConnectionControllerFactory, @@ -30,22 +35,28 @@ import org.enso.languageserver.refactoring.ProjectNameChangedEvent import org.enso.languageserver.runtime.{ContextRegistry, RuntimeFailureMapper} import org.enso.languageserver.search.SuggestionsHandler import org.enso.languageserver.session.SessionRouter -import org.enso.languageserver.TestClock import org.enso.languageserver.text.BufferRegistry +import org.enso.pkg.PackageManager import org.enso.polyglot.data.TypeGraph import org.enso.polyglot.runtime.Runtime.Api +import org.enso.runtimeversionmanager.test.{FakeEnvironment, HasTestDirectory} import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo, SqlVersionsRepo} import org.enso.testkit.EitherValue import org.enso.text.Sha3_224VersionCalculator import org.scalatest.OptionValues +import java.nio.file +import java.nio.file.Files +import java.util.UUID import scala.concurrent.Await import scala.concurrent.duration._ class BaseServerTest extends JsonRpcServerTestKit with EitherValue - with OptionValues { + with OptionValues + with HasTestDirectory + with FakeEnvironment { import system.dispatcher @@ -68,7 +79,12 @@ class BaseServerTest graph } + private val testDirectory = + Files.createTempDirectory("enso-test").toRealPath() + override def getTestDirectory: file.Path = testDirectory + sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.file)) + sys.addShutdownHook(FileUtils.deleteQuietly(testDirectory.toFile)) def mkConfig: Config = Config( @@ -204,30 +220,93 @@ class BaseServerTest Api.VerifyModulesIndexResponse(Seq()) ) + locally { + val dataRoot = getTestDirectory.resolve("test_data") + val editions = dataRoot.resolve("editions") + Files.createDirectories(editions) + val distribution = file.Path.of("distribution") + val currentEdition = buildinfo.Info.currentEdition + ".yaml" + val dest = editions.resolve(currentEdition) + if (Files.notExists(dest)) { + Files.copy( + distribution.resolve("editions").resolve(currentEdition), + dest + ) + } + } + + val environment = fakeInstalledEnvironment() + val languageHome = LanguageHome.detectFromExecutableLocation(environment) + val distributionManager = new DistributionManager(environment) + + val editionProvider = + EditionManager.makeEditionProvider( + distributionManager, + Some(languageHome) + ) + val editionResolver = EditionResolver(editionProvider) + val editionReferenceResolver = new EditionReferenceResolver( + config.projectContentRoot.file, + editionProvider, + editionResolver + ) + val editionManager = EditionManager(distributionManager, Some(languageHome)) + + val projectSettingsManager = system.actorOf( + ProjectSettingsManager.props( + config.projectContentRoot.file, + editionResolver + ) + ) + + val localLibraryManager = system.actorOf( + LocalLibraryManager.props( + config.projectContentRoot.file, + distributionManager + ) + ) + new JsonConnectionControllerFactory( - initializationComponent, - bufferRegistry, - capabilityRouter, - fileManager, - contentRootManagerActor, - contextRegistry, - suggestionsHandler, - stdOutController, - stdErrController, - stdInController, - runtimeConnectorProbe.ref, - idlenessMonitor, - config + mainComponent = initializationComponent, + bufferRegistry = bufferRegistry, + capabilityRouter = capabilityRouter, + fileManager = fileManager, + contentRootManager = contentRootManagerActor, + contextRegistry = contextRegistry, + suggestionsHandler = suggestionsHandler, + stdOutController = stdOutController, + stdErrController = stdErrController, + stdInController = stdInController, + runtimeConnector = runtimeConnectorProbe.ref, + idlenessMonitor = idlenessMonitor, + projectSettingsManager = projectSettingsManager, + localLibraryManager = localLibraryManager, + editionReferenceResolver = editionReferenceResolver, + editionManager = editionManager, + config = config ) } - def getInitialisedWsClient(): WsTestClient = { - val client = new WsTestClient(address) + /** Specifies if the `package.yaml` at project root should be auto-created. */ + protected def initializeProjectPackage: Boolean = true + + lazy val initPackage: Unit = { + if (initializeProjectPackage) { + PackageManager.Default.create( + config.projectContentRoot.file, + name = "TestProject" + ) + } + } + + def getInitialisedWsClient(debug: Boolean = false): WsTestClient = { + val client = new WsTestClient(address, debugMessages = debug) initSession(client) client } private def initSession(client: WsTestClient): UUID = { + initPackage val clientId = UUID.randomUUID() client.send(json""" { "jsonrpc": "2.0", @@ -252,5 +331,4 @@ class BaseServerTest ) clientId } - } diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala new file mode 100644 index 0000000000..1ad058252b --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala @@ -0,0 +1,245 @@ +package org.enso.languageserver.websocket.json + +import io.circe.literal._ +import io.circe.{Json, JsonObject} +import org.enso.languageserver.libraries.LibraryEntry +import org.enso.languageserver.libraries.LibraryEntry.PublishedLibraryVersion + +class LibrariesTest extends BaseServerTest { + "LocalLibraryManager" should { + "create a library project and include it on the list of local projects" in { + val client = getInitialisedWsClient() + client.send(json""" + { "jsonrpc": "2.0", + "method": "library/listLocal", + "id": 0 + } + """) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id": 0, + "result": { + "localLibraries": [] + } + } + """) + + client.send(json""" + { "jsonrpc": "2.0", + "method": "library/create", + "id": 1, + "params": { + "namespace": "User", + "name": "MyLocalLib", + "authors": [], + "maintainers": [], + "license": "" + } + } + """) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id": 1, + "result": null + } + """) + + client.send(json""" + { "jsonrpc": "2.0", + "method": "library/listLocal", + "id": 2 + } + """) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id": 2, + "result": { + "localLibraries": [ + { + "namespace": "User", + "name": "MyLocalLib", + "version": { + "type": "LocalLibraryVersion" + } + } + ] + } + } + """) + } + + "fail with LibraryAlreadyExists when creating a library that already " + + "existed" ignore { + // TODO [RW] error handling (#1877) + } + } + + "mocked library/preinstall" should { + "send progress notifications" in { + val client = getInitialisedWsClient() + client.send(json""" + { "jsonrpc": "2.0", + "method": "library/preinstall", + "id": 0, + "params": { + "namespace": "Foo", + "name": "Test" + } + } + """) + val messages = + for (_ <- 0 to 3) yield { + val msg = client.expectSomeJson().asObject.value + val method = msg("method").map(_.asString.value).getOrElse("error") + val params = + msg("params").map(_.asObject.value).getOrElse(JsonObject()) + (method, params) + } + + val taskStart = messages.find(_._1 == "task/started").value + val taskId = taskStart._2("taskId").value.asString.value + taskStart + ._2("relatedOperation") + .value + .asString + .value shouldEqual "library/preinstall" + + taskStart._2("unit").value.asString.value shouldEqual "Bytes" + + val updates = messages.filter { case (method, params) => + method == "task/progress-update" && + params("taskId").value.asString.value == taskId + } + + updates should not be empty + updates.head + ._2("message") + .value + .asString + .value shouldEqual "Download Test" + } + } + + "editions/listAvailable" should { + "list editions on the search path" in { + val client = getInitialisedWsClient() + client.send(json""" + { "jsonrpc": "2.0", + "method": "editions/listAvailable", + "id": 0, + "params": { + "update": false + } + } + """) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id": 0, + "result": { + "editionNames": [ + ${buildinfo.Info.currentEdition} + ] + } + } + """) + } + + "update the list of editions if requested" ignore { + // TODO [RW] updating editions + } + } + + "editions/listDefinedLibraries" should { + "include Standard.Base in the list" in { + def containsBase(response: Json): Unit = { + val result = response.asObject.value("result").value + val libs = result.asObject.value("availableLibraries").value + val parsed = libs.asArray.value.map(_.as[LibraryEntry]) + val bases = parsed.collect { + case Right( + LibraryEntry("Standard", "Base", PublishedLibraryVersion(_, _)) + ) => + () + } + bases should have size 1 + } + + val client = getInitialisedWsClient() + client.send(json""" + { "jsonrpc": "2.0", + "method": "editions/listDefinedLibraries", + "id": 0, + "params": { + "edition": { + "type": "CurrentProjectEdition" + } + } + } + """) + containsBase(client.expectSomeJson()) + + val currentEditionName = buildinfo.Info.currentEdition + client.send(json""" + { "jsonrpc": "2.0", + "method": "editions/listDefinedLibraries", + "id": 0, + "params": { + "edition": { + "type": "NamedEdition", + "editionName": $currentEditionName + } + } + } + """) + containsBase(client.expectSomeJson()) + } + } + + "editions/resolve" should { + "resolve the engine version associated with an edition" in { + val currentVersion = buildinfo.Info.ensoVersion + + val client = getInitialisedWsClient() + client.send(json""" + { "jsonrpc": "2.0", + "method": "editions/resolve", + "id": 0, + "params": { + "edition": { + "type": "CurrentProjectEdition" + } + } + } + """) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id": 0, + "result": { + "engineVersion": $currentVersion + } + } + """) + + client.send(json""" + { "jsonrpc": "2.0", + "method": "editions/resolve", + "id": 1, + "params": { + "edition": { + "type": "NamedEdition", + "editionName": ${buildinfo.Info.currentEdition} + } + } + } + """) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id": 1, + "result": { + "engineVersion": $currentVersion + } + } + """) + } + } +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ProjectSettingsManagerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ProjectSettingsManagerTest.scala new file mode 100644 index 0000000000..61ffcbbcee --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ProjectSettingsManagerTest.scala @@ -0,0 +1,105 @@ +package org.enso.languageserver.websocket.json + +import io.circe.literal._ +import org.enso.distribution.FileSystem + +import java.nio.file.Files + +class ProjectSettingsManagerTest extends BaseServerTest { + override def beforeAll(): Unit = { + super.beforeAll() + + val editionsDir = getTestDirectory.resolve("test_data").resolve("editions") + Files.createDirectories(editionsDir) + FileSystem.writeTextFile( + editionsDir.resolve("some-edition.yaml"), + """engine-version: 1.2.3 + |""".stripMargin + ) + + FileSystem.writeTextFile( + editionsDir.resolve("broken.yaml"), + """extends: non-existent + |""".stripMargin + ) + } + + "ProjectSettingsManager" should { + "get default settings" in { + val client = getInitialisedWsClient() + client.send(json""" + { "jsonrpc": "2.0", + "method": "editions/getProjectSettings", + "id": 0 + } + """) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id": 0, + "result": { + "parentEdition": ${buildinfo.Info.currentEdition}, + "preferLocalLibraries": true + } + } + """) + } + + "allow to set local libraries preference and parent edition and reflect " + + "these changes" in { + val client = getInitialisedWsClient() + client.send(json""" + { "jsonrpc": "2.0", + "method": "editions/setProjectLocalLibrariesPreference", + "id": 0, + "params": { + "preferLocalLibraries": false + } + } + """) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id": 0, + "result": { + "needsRestart": true + } + } + """) + + client.send(json""" + { "jsonrpc": "2.0", + "method": "editions/setParentEdition", + "id": 1, + "params": { + "newEditionName": "some-edition" + } + } + """) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id": 1, + "result": { + "needsRestart": true + } + } + """) + + client.send(json""" + { "jsonrpc": "2.0", + "method": "editions/getProjectSettings", + "id": 2 + } + """) + client.expectJson(json""" + { "jsonrpc": "2.0", + "id": 2, + "result": { + "parentEdition": "some-edition", + "preferLocalLibraries": false + } + } + """) + } + + "fail if the provided parent edition is not resolvable" ignore {} + } +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/WorkspaceOperationsTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/WorkspaceOperationsTest.scala index 3ad3affe3e..4a8d84a316 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/WorkspaceOperationsTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/WorkspaceOperationsTest.scala @@ -9,6 +9,8 @@ import java.io.{File, FileOutputStream} class WorkspaceOperationsTest extends BaseServerTest with FlakySpec { + override def initializeProjectPackage: Boolean = false + "workspace/projectInfo" must { val packageConfigName = Config.ensoPackageConfigName val testYamlPath = new File(testContentRoot.file, packageConfigName) diff --git a/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala b/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala index e09af43063..36d1a4f3c0 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala @@ -46,9 +46,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) { } private lazy val configurationManager = new GlobalConfigurationManager(componentsManager, distributionManager) - private lazy val editionManager = new EditionManager( - distributionManager.paths.editionSearchPaths.toList - ) + private lazy val editionManager = EditionManager(distributionManager) private lazy val projectManager = new ProjectManager private lazy val runner = new LauncherRunner( diff --git a/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala b/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala index 775b5a4981..39c1602175 100644 --- a/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala +++ b/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala @@ -31,9 +31,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest { new GlobalConfigurationManager(componentsManager, distributionManager) { override def defaultVersion: SemVer = defaultEngineVersion } - val editionManager = new EditionManager( - distributionManager.paths.editionSearchPaths.toList - ) + val editionManager = EditionManager(distributionManager) val projectManager = new ProjectManager() val cwd = cwdOverride.getOrElse(getTestDirectory) val runner = diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala index 50c7957c63..a67fc1da7e 100644 --- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala @@ -209,6 +209,10 @@ object Runtime { new JsonSubTypes.Type( value = classOf[Api.LibraryLoaded], name = "libraryLoaded" + ), + new JsonSubTypes.Type( + value = classOf[Api.ProgressNotification], + name = "progressNotification" ) ) ) @@ -1350,6 +1354,40 @@ object Runtime { location: File ) extends ApiNotification + /** A notification containing updates on the progress of long-running tasks. + * + * @param payload the actual update contained within this notification + */ + case class ProgressNotification( + payload: ProgressNotification.NotificationType + ) extends ApiNotification + + object ProgressNotification { + sealed trait NotificationType + + /** Indicates that a new task has been started. */ + case class TaskStarted( + taskId: UUID, + relatedOperation: String, + unit: String, + total: Option[Long] + ) extends NotificationType + + /** Indicates that the task has progressed. */ + case class TaskProgressUpdate( + taskId: UUID, + message: Option[String], + done: Long + ) extends NotificationType + + /** Indicates that the task has been finished. */ + case class TaskFinished( + taskId: UUID, + message: Option[String], + success: Boolean + ) extends NotificationType + } + private lazy val mapper = { val factory = new CBORFactory() val mapper = new ObjectMapper(factory) with ScalaObjectMapper diff --git a/engine/runtime/src/main/scala/org/enso/compiler/PackageRepository.scala b/engine/runtime/src/main/scala/org/enso/compiler/PackageRepository.scala index 440438fdcc..8a17740b7d 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/PackageRepository.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/PackageRepository.scala @@ -67,7 +67,7 @@ object PackageRepository { object Error { /** Indicates that a resolution error has happened, for example the package - * was not defined in the selected edition. + * was not defined in the selected edition. */ case class PackageCouldNotBeResolved(cause: Throwable) extends Error { override def toString: String = @@ -95,11 +95,11 @@ object PackageRepository { /** The default [[PackageRepository]] implementation. * - * @param libraryProvider the [[ResolvingLibraryProvider]] which resolves - * which library version should be imported and - * locates them (or downloads if they are missing) - * @param context the language context - * @param builtins the builtins module + * @param libraryProvider the [[ResolvingLibraryProvider]] which resolves + * which library version should be imported and + * locates them (or downloads if they are missing) + * @param context the language context + * @param builtins the builtins module * @param notificationHandler a notification handler */ class Default( @@ -315,12 +315,12 @@ object PackageRepository { * Edition and library search paths are based on the distribution and * language home (if it is provided). * - * @param projectPackage the package of the current project (if ran inside of a project) - * @param languageHome the language home (if set) + * @param projectPackage the package of the current project (if ran inside of a project) + * @param languageHome the language home (if set) * @param distributionManager the distribution manager - * @param context the context reference, needed to add polyglot libraries to - * the classpath - * @param builtins the builtins that are always preloaded + * @param context the context reference, needed to add polyglot libraries to + * the classpath + * @param builtins the builtins that are always preloaded * @param notificationHandler a handler for library addition and progress * notifications * @return an initialized [[PackageRepository]] @@ -337,11 +337,8 @@ object PackageRepository { .flatMap(_.config.edition) .getOrElse(DefaultEdition.getDefaultEdition) - val homeManager = languageHome.map { home => LanguageHome(Path.of(home)) } - val editionSearchPaths = - homeManager.map(_.editions).toList ++ - distributionManager.paths.editionSearchPaths - val editionManager = new EditionManager(editionSearchPaths) + val homeManager = languageHome.map { home => LanguageHome(Path.of(home)) } + val editionManager = EditionManager(distributionManager, homeManager) val edition = editionManager.resolveEdition(rawEdition).get val resolvingLibraryProvider = diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/NotificationHandler.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/NotificationHandler.scala index 81dead350f..ef420792b9 100644 --- a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/NotificationHandler.scala +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/NotificationHandler.scala @@ -2,7 +2,12 @@ package org.enso.interpreter.instrument import com.typesafe.scalalogging.Logger import org.enso.cli.ProgressBar -import org.enso.cli.task.{ProgressReporter, TaskProgress} +import org.enso.cli.task.{ + ProgressNotification, + ProgressNotificationForwarder, + ProgressReporter, + TaskProgress +} import org.enso.editions.{LibraryName, LibraryVersion} import org.enso.polyglot.runtime.Runtime.{Api, ApiResponse} @@ -15,9 +20,9 @@ trait NotificationHandler extends ProgressReporter { /** Called when a library has been loaded. * - * @param libraryName name of the added library + * @param libraryName name of the added library * @param libraryVersion selected version - * @param location path to the location from which the library is loaded + * @param location path to the location from which the library is loaded */ def addedLibrary( libraryName: LibraryName, @@ -80,7 +85,9 @@ object NotificationHandler { * notifications to the Language Server, which then should forward them to * the IDE. */ - class InteractiveMode(endpoint: Endpoint) extends NotificationHandler { + class InteractiveMode(endpoint: Endpoint) + extends NotificationHandler + with ProgressNotificationForwarder { private val logger = Logger[InteractiveMode] private def sendMessage(message: ApiResponse): Unit = { @@ -105,7 +112,16 @@ object NotificationHandler { /** @inheritdoc */ override def trackProgress(message: String, task: TaskProgress[_]): Unit = { logger.info(message) - // TODO [RW] this should be implemented once progress tracking is used by downloads + super.trackProgress(message, task) } + + override def sendProgressNotification( + notification: ProgressNotification + ): Unit = sendMessage( + ProgressNotificationTranslator.translate( + "compiler/downloadingDependencies", + notification + ) + ) } } diff --git a/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ProgressNotificationTranslator.scala b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ProgressNotificationTranslator.scala new file mode 100644 index 0000000000..9de78f5574 --- /dev/null +++ b/engine/runtime/src/main/scala/org/enso/interpreter/instrument/ProgressNotificationTranslator.scala @@ -0,0 +1,49 @@ +package org.enso.interpreter.instrument + +import org.enso.cli.task.{ + ProgressUnit, + ProgressNotification => TaskProgressNotification +} +import org.enso.polyglot.runtime.Runtime.Api.ProgressNotification +import org.enso.polyglot.runtime.Runtime.Api.ProgressNotification._ +import org.enso.polyglot.runtime.Runtime.ApiResponse + +/** A helper for translating notification formats. */ +object ProgressNotificationTranslator { + + /** Translates a notification as defined in the CLI module into the format + * that is used in the API of the runtime connector, so that it can be + * forwarded to the Language Server. + * + * @param relatedOperationName name of a related operation; these were + * originally tied to Project Manager or Language + * Server operations, but they can also be based + * on internal compiler operations + * @param progressNotification the notification to translate + */ + def translate( + relatedOperationName: String, + progressNotification: TaskProgressNotification + ): ApiResponse = { + val payload = progressNotification match { + case TaskProgressNotification.TaskStarted(taskId, total, unit) => + TaskStarted( + taskId = taskId, + relatedOperation = relatedOperationName, + unit = ProgressUnit.toString(unit), + total = total + ) + case TaskProgressNotification.TaskUpdate(taskId, message, done) => + TaskProgressUpdate(taskId, message, done) + case TaskProgressNotification.TaskSuccess(taskId) => + TaskFinished(taskId, message = None, success = true) + case TaskProgressNotification.TaskFailure(taskId, throwable) => + TaskFinished( + taskId, + message = Option(throwable.getMessage), + success = false + ) + } + ProgressNotification(payload) + } +} diff --git a/lib/scala/cli/src/main/scala/org/enso/cli/task/ProgressNotification.scala b/lib/scala/cli/src/main/scala/org/enso/cli/task/ProgressNotification.scala new file mode 100644 index 0000000000..866fb52664 --- /dev/null +++ b/lib/scala/cli/src/main/scala/org/enso/cli/task/ProgressNotification.scala @@ -0,0 +1,44 @@ +package org.enso.cli.task + +import java.util.UUID + +/** Internal representation of progress notifications. */ +sealed trait ProgressNotification +object ProgressNotification { + + /** Singals that a new task with progress has been started. + * + * @param taskId a unique id of the task + * @param total the total amount of units that the task is expected to take + * @param unit unit of that the progress is reported in + */ + case class TaskStarted( + taskId: UUID, + total: Option[Long], + unit: ProgressUnit + ) extends ProgressNotification + + /** Signals an update to task's progress. + * + * @param taskId the task id + * @param message an optional message to display + * @param done indication of how much progress has been done since the task + * started + */ + case class TaskUpdate(taskId: UUID, message: Option[String], done: Long) + extends ProgressNotification + + /** Signals that a task has been finished successfully. + * + * @param taskId the task id + */ + case class TaskSuccess(taskId: UUID) extends ProgressNotification + + /** Signals that a task has failed. + * + * @param taskId the task id + * @param throwable an exception associated with the failure + */ + case class TaskFailure(taskId: UUID, throwable: Throwable) + extends ProgressNotification +} diff --git a/lib/scala/cli/src/main/scala/org/enso/cli/task/ProgressNotificationForwarder.scala b/lib/scala/cli/src/main/scala/org/enso/cli/task/ProgressNotificationForwarder.scala new file mode 100644 index 0000000000..3f82f1ba34 --- /dev/null +++ b/lib/scala/cli/src/main/scala/org/enso/cli/task/ProgressNotificationForwarder.scala @@ -0,0 +1,66 @@ +package org.enso.cli.task + +import java.util.UUID +import scala.util.{Failure, Success, Try} + +/** A [[ProgressReporter]] implementation that tracks tasks and sends + * [[ProgressNotification]]s using a generic interface. + */ +trait ProgressNotificationForwarder extends ProgressReporter { + + /** The callback that is used to send the progress notification. */ + def sendProgressNotification(notification: ProgressNotification): Unit + + /** @inheritdoc */ + override def trackProgress(message: String, task: TaskProgress[_]): Unit = { + var uuid: Option[UUID] = None + + /** Initializes the task on first invocation and just returns the + * generated UUID on further invocations. + */ + def initializeTask(total: Option[Long]): UUID = uuid match { + case Some(value) => value + case None => + val generated = UUID.randomUUID() + uuid = Some(generated) + sendProgressNotification( + ProgressNotification.TaskStarted( + generated, + total, + task.unit + ) + ) + generated + } + + task.addProgressListener(new ProgressListener[Any] { + + /** @inheritdoc */ + override def progressUpdate( + done: Long, + total: Option[Long] + ): Unit = { + val uuid = initializeTask(total) + sendProgressNotification( + ProgressNotification.TaskUpdate( + uuid, + Some(message), + done + ) + ) + } + + /** @inheritdoc */ + override def done(result: Try[Any]): Unit = result match { + case Failure(exception) => + val uuid = initializeTask(None) + sendProgressNotification( + ProgressNotification.TaskFailure(uuid, exception) + ) + case Success(_) => + val uuid = initializeTask(None) + sendProgressNotification(ProgressNotification.TaskSuccess(uuid)) + } + }) + } +} diff --git a/lib/scala/cli/src/main/scala/org/enso/cli/task/ProgressUnit.scala b/lib/scala/cli/src/main/scala/org/enso/cli/task/ProgressUnit.scala index d9123527ce..c4c1263229 100644 --- a/lib/scala/cli/src/main/scala/org/enso/cli/task/ProgressUnit.scala +++ b/lib/scala/cli/src/main/scala/org/enso/cli/task/ProgressUnit.scala @@ -6,8 +6,21 @@ sealed trait ProgressUnit object ProgressUnit { /** Specifies that progress amount is measured in bytes. */ - case object Bytes extends ProgressUnit + case object Bytes extends ProgressUnit { + override val toString: String = "bytes" + } /** Does not specify a particular progress unit. */ - case object Unspecified extends ProgressUnit + case object Unspecified extends ProgressUnit { + override val toString: String = "unspecified" + } + + /** Converts a unit to its string representation. */ + def toString(unit: ProgressUnit): String = unit.toString + + /** Creates a unit from its string representation, falling back to + * [[Unspecified]] if it cannot be recognized. + */ + def fromString(str: String): ProgressUnit = + if (str == Bytes.toString) Bytes else Unspecified } diff --git a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/EditionManager.scala b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/EditionManager.scala index d9f625547d..2eb9d7605b 100644 --- a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/EditionManager.scala +++ b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/EditionManager.scala @@ -1,21 +1,24 @@ package org.enso.distribution +import nl.gn0s1s.bump.SemVer import org.enso.editions -import org.enso.editions.provider.FileSystemEditionProvider -import org.enso.editions.{ - DefaultEnsoVersion, - EditionResolver, - Editions, - EnsoVersion -} +import org.enso.editions.provider.{EditionProvider, FileSystemEditionProvider} +import org.enso.editions.{EditionResolver, Editions} import java.nio.file.Path -import scala.util.{Success, Try} - -/** A helper class for resolving editions. */ -class EditionManager(searchPaths: List[Path]) { - private val editionProvider = FileSystemEditionProvider(searchPaths) +import scala.annotation.unused +import scala.util.Try +/** A helper class for resolving editions. + * + * @param primaryCachePath will be used for updating editions + * @param searchPaths all paths to search for editions, should include + * [[primaryCachePath]] + */ +class EditionManager(@unused primaryCachePath: Path, searchPaths: List[Path]) { + private val editionProvider = new FileSystemEditionProvider( + searchPaths + ) private val editionResolver = EditionResolver(editionProvider) private val engineVersionResolver = editions.EngineVersionResolver(editionProvider) @@ -38,10 +41,46 @@ class EditionManager(searchPaths: List[Path]) { * engine version * @return the resolved engine version */ - def resolveEngineVersion( - edition: Option[Editions.RawEdition] - ): Try[EnsoVersion] = - edition - .map(engineVersionResolver.resolveEnsoVersion(_).toTry) - .getOrElse(Success(DefaultEnsoVersion)) + def resolveEngineVersion(edition: Editions.RawEdition): Try[SemVer] = + engineVersionResolver.resolveEnsoVersion(edition).toTry + + // TODO [RW] download edition updates, part of #1772 + + /** Find all editions available in the [[searchPaths]]. */ + def findAllAvailableEditions(): Seq[String] = + editionProvider.findAvailableEditions() +} + +object EditionManager { + + /** Create an [[EditionProvider]] that can locate editions from the + * distribution and the language home. + */ + def makeEditionProvider( + distributionManager: DistributionManager, + languageHome: Option[LanguageHome] + ): EditionProvider = new FileSystemEditionProvider( + getSearchPaths(distributionManager, languageHome) + ) + + /** Get search paths associated with the distribution and language home. */ + private def getSearchPaths( + distributionManager: DistributionManager, + languageHome: Option[LanguageHome] + ): List[Path] = { + val paths = languageHome.map(_.editions).toList ++ + distributionManager.paths.editionSearchPaths + paths.distinct + } + + /** Create an [[EditionManager]] that can locate editions from the + * distribution and the language home. + */ + def apply( + distributionManager: DistributionManager, + languageHome: Option[LanguageHome] = None + ): EditionManager = new EditionManager( + distributionManager.paths.cachedEditions, + getSearchPaths(distributionManager, languageHome) + ) } diff --git a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/LanguageHome.scala b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/LanguageHome.scala index 7c78fcf26a..5f64972cbb 100644 --- a/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/LanguageHome.scala +++ b/lib/scala/distribution-manager/src/main/scala/org/enso/distribution/LanguageHome.scala @@ -20,3 +20,15 @@ case class LanguageHome(languageHome: Path) { def libraries: Path = rootPath.resolve(DistributionManager.LIBRARIES_DIRECTORY) } + +object LanguageHome { + + /** Finds the [[LanguageHome]] based on the path of the runner JAR. + * + * Only guaranteed to work properly if used in a component that is started by the `engine-runner`. + */ + def detectFromExecutableLocation(environment: Environment): LanguageHome = { + val homePath = environment.getPathToRunningExecutable.getParent + LanguageHome(homePath) + } +} diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/EditionResolutionError.scala b/lib/scala/editions/src/main/scala/org/enso/editions/EditionResolutionError.scala index 73c2813a40..61c919eb37 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/EditionResolutionError.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/EditionResolutionError.scala @@ -29,7 +29,7 @@ object EditionResolutionError { * reference is invalid. */ case class LibraryReferencesUndefinedRepository( - libraryName: String, + libraryName: LibraryName, repositoryName: String ) extends EditionResolutionError( s"A library `$libraryName` references a repository `$repositoryName` " + diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/EditionResolver.scala b/lib/scala/editions/src/main/scala/org/enso/editions/EditionResolver.scala index 217a57ae81..f3379a8728 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/EditionResolver.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/EditionResolver.scala @@ -62,16 +62,16 @@ case class EditionResolver(provider: EditionProvider) { * a mapping of resolved libraries */ private def resolveLibraries( - libraries: Map[String, Editions.Raw.Library], + libraries: Map[LibraryName, Editions.Raw.Library], currentRepositories: Map[String, Editions.Repository], parent: Option[ResolvedEdition] ): Either[ LibraryReferencesUndefinedRepository, - Map[String, Editions.Resolved.Library] + Map[LibraryName, Editions.Resolved.Library] ] = { val resolvedPairs: Either[ LibraryReferencesUndefinedRepository, - List[(String, Editions.Resolved.Library)] + List[(LibraryName, Editions.Resolved.Library)] ] = libraries.toList.traverse { case (name, library) => val resolved = resolveLibrary(library, currentRepositories, parent) @@ -122,7 +122,7 @@ case class EditionResolver(provider: EditionProvider) { case (None, None) => Left( LibraryReferencesUndefinedRepository( - libraryName = library.qualifiedName, + libraryName = library.name, repositoryName = repositoryName ) ) diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/EditionSerialization.scala b/lib/scala/editions/src/main/scala/org/enso/editions/EditionSerialization.scala index 69c90ee5b8..771d75553a 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/EditionSerialization.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/EditionSerialization.scala @@ -48,7 +48,7 @@ object EditionSerialization { implicit val editionDecoder: Decoder[Raw.Edition] = { json => for { parent <- json.get[Option[EditionName]](Fields.parent) - engineVersion <- json.get[Option[EnsoVersion]](Fields.engineVersion) + engineVersion <- json.get[Option[SemVer]](Fields.engineVersion) _ <- if (parent.isEmpty && engineVersion.isEmpty) Left( @@ -64,7 +64,7 @@ object EditionSerialization { libraries <- json.getOrElse[Seq[Raw.Library]](Fields.libraries)(Seq()) res <- { val repositoryMap = Map.from(repositories.map(r => (r.name, r))) - val libraryMap = Map.from(libraries.map(l => (l.qualifiedName, l))) + val libraryMap = Map.from(libraries.map(l => (l.name, l))) if (libraryMap.size != libraries.size) Left( DecodingFailure( @@ -157,7 +157,11 @@ object EditionSerialization { } implicit private val libraryDecoder: Decoder[Raw.Library] = { json => - def makeLibrary(name: String, repository: String, version: Option[SemVer]) = + def makeLibrary( + name: LibraryName, + repository: String, + version: Option[SemVer] + ) = if (repository == Fields.localRepositoryName) if (version.isDefined) Left( @@ -181,7 +185,7 @@ object EditionSerialization { } } for { - name <- json.get[String](Fields.name) + name <- json.get[LibraryName](Fields.name) repository <- json.get[String](Fields.repository) version <- json.get[Option[SemVer]](Fields.version) res <- makeLibrary(name, repository, version) diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/Editions.scala b/lib/scala/editions/src/main/scala/org/enso/editions/Editions.scala index d541b5d399..2f7b979d7f 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/Editions.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/Editions.scala @@ -42,22 +42,22 @@ trait Editions { * It should consist of a prefix followed by a dot an the library name, for * example `Prefix.Library_Name`. */ - def qualifiedName: String + def name: LibraryName } /** Represents a local library. */ - case class LocalLibrary(override val qualifiedName: String) extends Library + case class LocalLibrary(override val name: LibraryName) extends Library /** Represents a specific version of the library that is published in a * repository. * - * @param qualifiedName the qualified name of the library + * @param name the qualified name of the library * @param version the exact version of the library that should be used * @param repository the recommended repository to download the library from, * if it is not yet cached */ case class PublishedLibrary( - override val qualifiedName: String, + override val name: LibraryName, version: SemVer, repository: LibraryRepositoryType ) extends Library @@ -75,9 +75,9 @@ trait Editions { */ case class Edition( parent: Option[NestedEditionType] = None, - engineVersion: Option[EnsoVersion] = None, + engineVersion: Option[SemVer] = None, repositories: Map[String, Editions.Repository] = Map.empty, - libraries: Map[String, Library] = Map.empty + libraries: Map[LibraryName, Library] = Map.empty ) { if (parent.isEmpty && engineVersion.isEmpty) throw new IllegalArgumentException( @@ -134,7 +134,7 @@ object Editions { * is either the version override directly specified in the edition or the * version implied by its parent. */ - def getEngineVersion: EnsoVersion = edition.engineVersion.getOrElse { + def getEngineVersion: SemVer = edition.engineVersion.getOrElse { val parent = edition.parent.getOrElse { throw new IllegalStateException( "Internal error: Resolved edition does not imply an engine version." @@ -142,6 +142,23 @@ object Editions { } parent.getEngineVersion } + + /** Returns a mapping of all libraries defined in the edition, including any + * libraries defined in parent editions (also taking into account the + * overrides). + */ + def getAllDefinedLibraries: Map[LibraryName, LibraryVersion] = { + val parent = + edition.parent.map(_.getAllDefinedLibraries).getOrElse(Map.empty) + edition.libraries.foldLeft(parent) { case (map, (name, lib)) => + val version = lib match { + case Resolved.LocalLibrary(_) => LibraryVersion.Local + case Resolved.PublishedLibrary(_, version, repository) => + LibraryVersion.Published(version, repository) + } + map.updated(name, version) + } + } } /** Syntax helpers for a raw edition. */ diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/EngineVersionResolver.scala b/lib/scala/editions/src/main/scala/org/enso/editions/EngineVersionResolver.scala index 7c29b706ed..5d9ace10fa 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/EngineVersionResolver.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/EngineVersionResolver.scala @@ -1,5 +1,6 @@ package org.enso.editions +import nl.gn0s1s.bump.SemVer import org.enso.editions.Editions.RawEdition import org.enso.editions.provider.EditionProvider @@ -19,7 +20,7 @@ case class EngineVersionResolver(editionProvider: EditionProvider) { */ def resolveEnsoVersion( edition: RawEdition - ): Either[EditionResolutionError, EnsoVersion] = { + ): Either[EditionResolutionError, SemVer] = { for { edition <- editionResolver.resolve(edition) } yield edition.getEngineVersion diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/LibraryName.scala b/lib/scala/editions/src/main/scala/org/enso/editions/LibraryName.scala index e70b06596d..cb3580f809 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/LibraryName.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/LibraryName.scala @@ -1,6 +1,7 @@ package org.enso.editions -import io.circe.{Decoder, DecodingFailure} +import io.circe.syntax.EncoderOps +import io.circe.{Decoder, DecodingFailure, Encoder} /** Represents a library name that should uniquely identify the library. * @@ -31,6 +32,10 @@ object LibraryName { } yield name } + implicit val encoder: Encoder[LibraryName] = { libraryName => + libraryName.toString.asJson + } + private val separator = '.' /** Creates a [[LibraryName]] from its string representation. diff --git a/lib/scala/editions/src/main/scala/org/enso/editions/provider/FileSystemEditionProvider.scala b/lib/scala/editions/src/main/scala/org/enso/editions/provider/FileSystemEditionProvider.scala index 74c210c398..bfd96b8a14 100644 --- a/lib/scala/editions/src/main/scala/org/enso/editions/provider/FileSystemEditionProvider.scala +++ b/lib/scala/editions/src/main/scala/org/enso/editions/provider/FileSystemEditionProvider.scala @@ -5,12 +5,14 @@ import org.enso.editions.{EditionSerialization, Editions} import java.io.FileNotFoundException import java.nio.file.{Files, Path} import scala.annotation.tailrec -import scala.util.{Failure, Success, Try} +import scala.collection.Factory +import scala.jdk.StreamConverters.StreamHasToScala +import scala.util.{Failure, Success, Try, Using} /** An implementation of [[EditionProvider]] that looks for the edition files in * a list of filesystem paths. */ -case class FileSystemEditionProvider(searchPaths: List[Path]) +class FileSystemEditionProvider(searchPaths: List[Path]) extends EditionProvider { /** @inheritdoc */ @@ -60,4 +62,23 @@ case class FileSystemEditionProvider(searchPaths: List[Path]) .map(EditionReadError) } else Left(EditionNotFound) } + + /** Finds all editions available on the [[searchPaths]]. */ + def findAvailableEditions(): Seq[String] = + searchPaths.flatMap(findEditionsAt).distinct + + private def findEditionName(path: Path): Option[String] = { + val name = path.getFileName.toString + if (name.endsWith(editionSuffix)) { + Some(name.stripSuffix(editionSuffix)) + } else None + } + + private def findEditionsAt(path: Path): Seq[String] = + listDir(path).filter(Files.isRegularFile(_)).flatMap(findEditionName) + + private def listDir(dir: Path): Seq[Path] = + if (Files.exists(dir)) + Using(Files.list(dir))(_.toScala(Factory.arrayFactory).toSeq).get + else Seq() } diff --git a/lib/scala/editions/src/test/scala/org/enso/editions/EditionResolverSpec.scala b/lib/scala/editions/src/test/scala/org/enso/editions/EditionResolverSpec.scala index 7f11e2ae37..7c1e1fa528 100644 --- a/lib/scala/editions/src/test/scala/org/enso/editions/EditionResolverSpec.scala +++ b/lib/scala/editions/src/test/scala/org/enso/editions/EditionResolverSpec.scala @@ -20,24 +20,28 @@ class EditionResolverSpec val editions: Map[String, Editions.RawEdition] = Map( "2021.0" -> Editions.Raw.Edition( parent = None, - engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))), + engineVersion = Some(SemVer(1, 2, 3)), repositories = Map( "main" -> mainRepo ), libraries = Map( - "Standard.Base" -> Editions.Raw - .PublishedLibrary("Standard.Base", SemVer(1, 2, 3), "main") + LibraryName("Standard", "Base") -> Editions.Raw + .PublishedLibrary( + LibraryName("Standard", "Base"), + SemVer(1, 2, 3), + "main" + ) ) ), "cycleA" -> Editions.Raw.Edition( parent = Some("cycleB"), - engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))), + engineVersion = Some(SemVer(1, 2, 3)), repositories = Map(), libraries = Map() ), "cycleB" -> Editions.Raw.Edition( parent = Some("cycleA"), - engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))), + engineVersion = Some(SemVer(1, 2, 3)), repositories = Map(), libraries = Map() ) @@ -59,12 +63,14 @@ class EditionResolverSpec val repo = Repository.make("foo", "http://example.com").get val edition = Editions.Raw.Edition( parent = None, - engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))), + engineVersion = Some(SemVer(1, 2, 3)), repositories = Map("foo" -> repo), libraries = Map( - "bar.baz" -> Editions.Raw.LocalLibrary("bar.baz"), - "foo.bar" -> Editions.Raw - .PublishedLibrary("foo.bar", SemVer(1, 2, 3), "foo") + LibraryName("bar", "baz") -> Editions.Raw.LocalLibrary( + LibraryName("bar", "baz") + ), + LibraryName("foo", "bar") -> Editions.Raw + .PublishedLibrary(LibraryName("foo", "bar"), SemVer(1, 2, 3), "foo") ) ) @@ -73,33 +79,47 @@ class EditionResolverSpec resolved.parent should be(empty) resolved.repositories shouldEqual edition.repositories resolved.libraries should have size 2 - resolved.libraries("bar.baz") shouldEqual Editions.Resolved - .LocalLibrary("bar.baz") - resolved.libraries("foo.bar") shouldEqual Editions.Resolved - .PublishedLibrary("foo.bar", SemVer(1, 2, 3), repo) + resolved.libraries( + LibraryName("bar", "baz") + ) shouldEqual Editions.Resolved + .LocalLibrary(LibraryName("bar", "baz")) + resolved.libraries( + LibraryName("foo", "bar") + ) shouldEqual Editions.Resolved + .PublishedLibrary(LibraryName("foo", "bar"), SemVer(1, 2, 3), repo) } } "resolve a nested edition" in { val edition = Editions.Raw.Edition( parent = Some("2021.0"), - engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))), + engineVersion = Some(SemVer(1, 2, 3)), repositories = Map(), libraries = Map( - "bar.baz" -> Editions.Raw.LocalLibrary("bar.baz"), - "foo.bar" -> Editions.Raw - .PublishedLibrary("foo.bar", SemVer(1, 2, 3), "main") + LibraryName("bar", "baz") -> Editions.Raw.LocalLibrary( + LibraryName("bar", "baz") + ), + LibraryName("foo", "bar") -> Editions.Raw + .PublishedLibrary( + LibraryName("foo", "bar"), + SemVer(1, 2, 3), + "main" + ) ) ) inside(resolver.resolve(edition)) { case Right(resolved) => resolved.parent should be(defined) resolved.libraries should have size 2 - resolved.libraries("bar.baz") shouldEqual Editions.Resolved - .LocalLibrary("bar.baz") - resolved.libraries("foo.bar") shouldEqual Editions.Resolved + resolved.libraries( + LibraryName("bar", "baz") + ) shouldEqual Editions.Resolved + .LocalLibrary(LibraryName("bar", "baz")) + resolved.libraries( + LibraryName("foo", "bar") + ) shouldEqual Editions.Resolved .PublishedLibrary( - "foo.bar", + LibraryName("foo", "bar"), SemVer(1, 2, 3), FakeEditionProvider.mainRepo ) @@ -116,24 +136,30 @@ class EditionResolverSpec engineVersion = None, repositories = Map("main" -> localRepo), libraries = Map( - "foo.bar" -> Editions.Raw - .PublishedLibrary("foo.bar", SemVer(1, 2, 3), "main") + LibraryName("foo", "bar") -> Editions.Raw + .PublishedLibrary( + LibraryName("foo", "bar"), + SemVer(1, 2, 3), + "main" + ) ) ) inside(resolver.resolve(edition)) { case Right(resolved) => resolved.parent should be(defined) resolved.libraries should have size 1 - resolved.libraries("foo.bar") shouldEqual + resolved.libraries(LibraryName("foo", "bar")) shouldEqual Editions.Resolved.PublishedLibrary( - "foo.bar", + LibraryName("foo", "bar"), SemVer(1, 2, 3), localRepo ) - resolved.parent.value.libraries("Standard.Base") shouldEqual + resolved.parent.value.libraries( + LibraryName("Standard", "Base") + ) shouldEqual Editions.Resolved.PublishedLibrary( - "Standard.Base", + LibraryName("Standard", "Base"), SemVer(1, 2, 3), FakeEditionProvider.mainRepo ) @@ -143,7 +169,7 @@ class EditionResolverSpec "avoid cycles in the resolution" in { val edition = Editions.Raw.Edition( parent = Some("cycleA"), - engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))), + engineVersion = Some(SemVer(1, 2, 3)), repositories = Map(), libraries = Map() ) diff --git a/lib/scala/editions/src/test/scala/org/enso/editions/EditionSerializationSpec.scala b/lib/scala/editions/src/test/scala/org/enso/editions/EditionSerializationSpec.scala index 08c1b88642..a114581392 100644 --- a/lib/scala/editions/src/test/scala/org/enso/editions/EditionSerializationSpec.scala +++ b/lib/scala/editions/src/test/scala/org/enso/editions/EditionSerializationSpec.scala @@ -54,13 +54,16 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside { .url shouldEqual "http://127.0.0.1:8080/root" edition.libraries.values should contain theSameElementsAs Seq( - Editions.Raw.LocalLibrary("Foo.Local"), - Editions.Raw.PublishedLibrary("Bar.Baz", SemVer(0, 0, 0), "example"), - Editions.Raw.PublishedLibrary("A.B", SemVer(1, 0, 1), "bar") - ) - edition.engineVersion should contain( - SemVerEnsoVersion(SemVer(1, 2, 3, Some("SNAPSHOT"))) + Editions.Raw.LocalLibrary(LibraryName("Foo", "Local")), + Editions.Raw.PublishedLibrary( + LibraryName("Bar", "Baz"), + SemVer(0, 0, 0), + "example" + ), + Editions.Raw + .PublishedLibrary(LibraryName("A", "B"), SemVer(1, 0, 1), "bar") ) + edition.engineVersion should contain(SemVer(1, 2, 3, Some("SNAPSHOT"))) } } @@ -82,7 +85,7 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside { val parsed = EditionSerialization.parseYamlString( """extends: foo |libraries: - |- name: bar + |- name: bar.baz | repository: local | version: 1.2.3-SHOULD-NOT-BE-HERE |""".stripMargin @@ -94,7 +97,7 @@ class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside { val parsed2 = EditionSerialization.parseYamlString( """extends: foo |libraries: - |- name: bar + |- name: bar.baz | repository: something |""".stripMargin ) diff --git a/lib/scala/json-rpc-server-test/src/main/scala/org/enso/jsonrpc/test/JsonRpcServerTestKit.scala b/lib/scala/json-rpc-server-test/src/main/scala/org/enso/jsonrpc/test/JsonRpcServerTestKit.scala index 5114d5c375..f2d62e2e76 100644 --- a/lib/scala/json-rpc-server-test/src/main/scala/org/enso/jsonrpc/test/JsonRpcServerTestKit.scala +++ b/lib/scala/json-rpc-server-test/src/main/scala/org/enso/jsonrpc/test/JsonRpcServerTestKit.scala @@ -118,6 +118,11 @@ abstract class JsonRpcServerTestKit parsed shouldEqual Right(json) } + def expectSomeJson(timeout: FiniteDuration = 5.seconds.dilated): Json = { + val parsed = parse(expectMessage(timeout)) + inside(parsed) { case Right(json) => json } + } + def expectNoMessage(): Unit = outActor.expectNoMessage() } } diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryResolver.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryResolver.scala index 9ef6e27650..0ad50cce9d 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryResolver.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/LibraryResolver.scala @@ -54,7 +54,7 @@ case class LibraryResolver( ): Either[LibraryResolutionError, LibraryVersion] = { import Editions.Resolved._ val immediateResult = - edition.libraries.get(libraryName.qualifiedName).map { + edition.libraries.get(libraryName).map { case LocalLibrary(_) => Right(LibraryVersion.Local) case PublishedLibrary(_, version, repository) => diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala index 3b08605eef..0e25fdb65b 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala @@ -1,7 +1,6 @@ package org.enso.librarymanager.local import com.typesafe.scalalogging.Logger -import org.enso.distribution.FileSystem.PathSyntax import org.enso.editions.LibraryName import org.enso.logger.masking.MaskedPath @@ -31,7 +30,8 @@ class DefaultLocalLibraryProvider(searchPaths: List[Path]) searchPaths: List[Path] ): Option[Path] = searchPaths match { case head :: tail => - val potentialPath = head / libraryName.namespace / libraryName.name + val potentialPath = + LocalLibraryProvider.resolveLibraryPath(head, libraryName) if (Files.exists(potentialPath) && Files.isDirectory(potentialPath)) { logger.trace( s"Found a local $libraryName at " + diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala index 829cc7dd0a..ba376c4311 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala @@ -1,5 +1,6 @@ package org.enso.librarymanager.local +import org.enso.distribution.FileSystem.PathSyntax import org.enso.editions.LibraryName import java.nio.file.Path @@ -12,3 +13,12 @@ trait LocalLibraryProvider { */ def findLibrary(libraryName: LibraryName): Option[Path] } + +object LocalLibraryProvider { + + /** Resolve a path to the package root of a particular library located in one + * of the local library roots. + */ + def resolveLibraryPath(root: Path, libraryName: LibraryName): Path = + root / libraryName.namespace / libraryName.name +} diff --git a/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala b/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala index 6319cdbed2..2cdc407f1d 100644 --- a/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala +++ b/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala @@ -2,12 +2,7 @@ package org.enso.librarymanager import nl.gn0s1s.bump.SemVer import org.enso.editions.Editions.Repository -import org.enso.editions.{ - DefaultEnsoVersion, - Editions, - LibraryName, - LibraryVersion -} +import org.enso.editions.{Editions, LibraryName, LibraryVersion} import org.enso.librarymanager.local.LocalLibraryProvider import org.enso.testkit.EitherValue import org.scalatest.Inside @@ -25,11 +20,15 @@ class LibraryResolverSpec val mainRepo = Repository.make("main", "https://example.com/main").get val parentEdition = Editions.Resolved.Edition( parent = None, - engineVersion = Some(DefaultEnsoVersion), + engineVersion = Some(SemVer(0, 0, 0)), repositories = Map("main" -> mainRepo), libraries = Map( - "Standard.Base" -> Editions.Resolved - .PublishedLibrary("Standard.Base", SemVer(4, 5, 6), mainRepo) + LibraryName("Standard", "Base") -> Editions.Resolved + .PublishedLibrary( + LibraryName("Standard", "Base"), + SemVer(4, 5, 6), + mainRepo + ) ) ) val customRepo = Repository.make("custom", "https://example.com/custom").get @@ -38,24 +37,34 @@ class LibraryResolverSpec engineVersion = None, repositories = Map("custom" -> customRepo), libraries = Map( - "Foo.Main" -> Editions.Resolved - .PublishedLibrary("Foo.Main", SemVer(1, 0, 0), mainRepo), - "Foo.My" -> Editions.Resolved - .PublishedLibrary("Foo.My", SemVer(2, 0, 0), customRepo), - "Foo.Local" -> Editions.Resolved.LocalLibrary("Foo.Local") + LibraryName("Foo", "Main") -> Editions.Resolved + .PublishedLibrary( + LibraryName("Foo", "Main"), + SemVer(1, 0, 0), + mainRepo + ), + LibraryName("Foo", "My") -> Editions.Resolved + .PublishedLibrary( + LibraryName("Foo", "My"), + SemVer(2, 0, 0), + customRepo + ), + LibraryName("Foo", "Local") -> Editions.Resolved.LocalLibrary( + LibraryName("Foo", "Local") + ) ) ) - case class FakeLocalLibraryProvider(fixtures: Map[String, Path]) + case class FakeLocalLibraryProvider(fixtures: Map[LibraryName, Path]) extends LocalLibraryProvider { override def findLibrary(libraryName: LibraryName): Option[Path] = - fixtures.get(libraryName.qualifiedName) + fixtures.get(libraryName) } val localLibraries = Map( - "Foo.My" -> Path.of("./Foo/My"), - "Foo.Local" -> Path.of("./Foo/Local"), - "Standard.Base" -> Path.of("./Standard/Base") + LibraryName("Foo", "My") -> Path.of("./Foo/My"), + LibraryName("Foo", "Local") -> Path.of("./Foo/Local"), + LibraryName("Standard", "Base") -> Path.of("./Standard/Base") ) val resolver = LibraryResolver(FakeLocalLibraryProvider(localLibraries)) diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala index b08842e221..120ae5152c 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala @@ -205,7 +205,26 @@ object Config { val overridesObject = JsonObject( overrides ++ preferLocalOverride: _* ) - originals.remove(JsonFields.ensoVersion).deepMerge(overridesObject).asJson + + /** Fields that should not be inherited from the original set of fields. + * + * `ensoVersion` is dropped, because due to migration it is overridden by + * `edition` and we don't want to have both to avoid inconsistency. + * + * `prefer-local-libraries` cannot be inherited, because if it was set to + * `true` and we have changed it to `false`, overrides will not include it, + * because, as `false` is its default value, we just ignore the field. But + * if we inherit it from original fields, we would get `true` back. If the + * setting is still set to true, it will be included in the overrides, so + * it does not have to be inherited either. + */ + val fieldsToRemoveFromOriginals = + Seq(JsonFields.ensoVersion, JsonFields.preferLocalLibraries) + + val removed = fieldsToRemoveFromOriginals.foldLeft(originals) { + case (obj, key) => obj.remove(key) + } + removed.deepMerge(overridesObject).asJson } /** Tries to parse the [[Config]] from a YAML string. */ @@ -229,7 +248,7 @@ object Config { ensoVersion: SemVer ): Editions.RawEdition = Editions.Raw.Edition( parent = None, - engineVersion = Some(SemVerEnsoVersion(ensoVersion)), + engineVersion = Some(ensoVersion), repositories = Map(), libraries = Map() ) diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala index 411e959f4b..8b4f2a5e2b 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala @@ -48,11 +48,13 @@ case class Package[F]( /** Stores the package metadata on the hard drive. If the package does not exist, * creates the required directory structure. */ - def save(): Unit = { - if (!root.exists) createDirectories() - if (!sourceDir.exists) createSourceDir() - saveConfig() - } + def save(): Try[Unit] = for { + _ <- Try { + if (!root.exists) createDirectories() + if (!sourceDir.exists) createSourceDir() + } + _ <- saveConfig() + } yield () /** Creates the package directory structure. */ @@ -97,11 +99,10 @@ case class Package[F]( /** Saves the config metadata into the package configuration file. */ - def saveConfig(): Unit = { - val writer = configFile.newBufferedWriter - Try(writer.write(config.toYaml)) - writer.close() - } + def saveConfig(): Try[Unit] = + Using(configFile.newBufferedWriter) { writer => + writer.write(config.toYaml) + } /** Gets the location of the package's Main file. * @@ -208,13 +209,14 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) { version: String = "0.0.1", edition: Option[Editions.RawEdition] = None, authors: List[Contact] = List(), - maintainers: List[Contact] = List() + maintainers: List[Contact] = List(), + license: String = "" ): Package[F] = { val config = Config( name = NameValidation.normalizeName(name), namespace = namespace, version = version, - license = "", + license = license, authors = authors, edition = edition, preferLocalLibraries = true, diff --git a/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala b/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala index 1541aeb9e0..94f7658fcd 100644 --- a/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala +++ b/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala @@ -2,7 +2,6 @@ package org.enso.pkg import io.circe.{Json, JsonObject} import nl.gn0s1s.bump.SemVer -import org.enso.editions.SemVerEnsoVersion import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec import org.scalatest.{Inside, OptionValues} @@ -64,9 +63,7 @@ class ConfigSpec |""".stripMargin val parsed = Config.fromYaml(oldFormat).get - parsed.edition.get.engineVersion should contain( - SemVerEnsoVersion(SemVer(1, 2, 3)) - ) + parsed.edition.get.engineVersion should contain(SemVer(1, 2, 3)) val serialized = parsed.toYaml val parsedAgain = Config.fromYaml(serialized).get diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/data/ProgressUnit.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/data/ProgressUnit.scala deleted file mode 100644 index bb25379760..0000000000 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/data/ProgressUnit.scala +++ /dev/null @@ -1,26 +0,0 @@ -package org.enso.projectmanager.data - -import enumeratum._ -import org.enso.cli.task.{TaskProgress, ProgressUnit => TaskProgressUnit} - -/** Represents the unit used by progress updates. */ -sealed trait ProgressUnit extends EnumEntry -object ProgressUnit extends Enum[ProgressUnit] with CirceEnum[ProgressUnit] { - - /** Indicates that progress is measured by amount of bytes processed. */ - case object Bytes extends ProgressUnit - - /** Indicates that progress is measured by some other unit or it is not - * measured at all. - */ - case object Other extends ProgressUnit - - override val values = findValues - - /** Creates a [[ProgressUnit]] from the unit associated with [[TaskProgress]]. - */ - def fromTask(task: TaskProgress[_]): ProgressUnit = task.unit match { - case TaskProgressUnit.Bytes => Bytes - case TaskProgressUnit.Unspecified => Other - } -} diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala index b709174057..f2aa8e90a9 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala @@ -3,6 +3,7 @@ package org.enso.projectmanager.protocol import io.circe.generic.auto._ import org.enso.jsonrpc.Protocol import org.enso.projectmanager.protocol.ProjectManagementApi._ +import org.enso.cli.task.notifications.TaskNotificationApi._ /** Implicits from this module are required for correct serialization. * diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala index a8794f8fd0..a96e8f4df7 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala @@ -9,7 +9,6 @@ import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused} import org.enso.projectmanager.data.{ EngineVersion, MissingComponentAction, - ProgressUnit, ProjectMetadata, Socket } @@ -117,46 +116,6 @@ object ProjectManagementApi { } } - case object TaskStarted extends Method("task/started") { - - case class Params( - taskId: UUID, - relatedOperation: String, - unit: ProgressUnit, - total: Option[Long] - ) - - implicit val hasParams = new HasParams[this.type] { - type Params = TaskStarted.Params - } - } - - case object TaskProgressUpdate extends Method("task/progress-update") { - - case class Params( - taskId: UUID, - message: Option[String], - done: Long - ) - - implicit val hasParams = new HasParams[this.type] { - type Params = TaskProgressUpdate.Params - } - } - - case object TaskFinished extends Method("task/progress-update") { - - case class Params( - taskId: UUID, - message: Option[String], - success: Boolean - ) - - implicit val hasParams = new HasParams[this.type] { - type Params = TaskFinished.Params - } - } - case object EngineListInstalled extends Method("engine/list-installed") { case class Result(versions: Seq[EngineVersion]) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/RequestHandler.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/RequestHandler.scala index 6b47488874..18b1c46425 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/RequestHandler.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/RequestHandler.scala @@ -3,19 +3,11 @@ package org.enso.projectmanager.requesthandler import akka.actor.{Actor, ActorRef, Cancellable, Stash, Status} import akka.pattern.pipe import com.typesafe.scalalogging.{LazyLogging, Logger} +import org.enso.cli.task.ProgressNotification +import org.enso.cli.task.notifications.ActorProgressNotificationForwarder import org.enso.jsonrpc.Errors.ServiceError -import org.enso.jsonrpc.{ - HasParams, - HasResult, - Id, - Method, - Request, - ResponseError, - ResponseResult -} +import org.enso.jsonrpc._ import org.enso.projectmanager.control.effect.Exec -import org.enso.projectmanager.service.versionmanagement.ProgressNotification -import org.enso.projectmanager.service.versionmanagement.ProgressNotification.translateProgressNotification import org.enso.projectmanager.util.UnhandledLogging import scala.annotation.unused @@ -118,7 +110,8 @@ abstract class RequestHandler[ abandonTimeout(id, replyTo, timeoutCancellable) case _ => } - replyTo ! translateProgressNotification(method.name, notification) + replyTo ! ActorProgressNotificationForwarder + .translateProgressNotification(method.name, notification) } /** Cancels the timeout operation. diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala index 9918b21e28..5b2610d573 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala @@ -4,7 +4,7 @@ import akka.actor.ActorRef import cats.MonadError import com.typesafe.scalalogging.Logger import nl.gn0s1s.bump.SemVer -import org.enso.editions.EnsoVersion +import org.enso.editions.DefaultEdition import org.enso.pkg.Config import org.enso.projectmanager.control.core.syntax._ import org.enso.projectmanager.control.core.{ @@ -44,7 +44,6 @@ import org.enso.projectmanager.service.ValidationFailure.{ NameShouldStartWithCapitalLetter } import org.enso.projectmanager.service.config.GlobalConfigServiceApi -import org.enso.projectmanager.service.config.GlobalConfigServiceFailure.ConfigurationFileAccessFailure import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._ import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory import org.enso.projectmanager.versionmanagement.DistributionConfiguration @@ -306,15 +305,7 @@ class ProjectService[ missingComponentAction: MissingComponentAction ): F[ProjectServiceFailure, RunningLanguageServerInfo] = for { version <- resolveProjectVersion(project) - version <- configurationService - .resolveEnsoVersion(version) - .mapError { case ConfigurationFileAccessFailure(message) => - ProjectOpenFailed( - "Could not deduce the default version to use for the project: " + - message - ) - } - _ <- preinstallEngine(progressTracker, version, missingComponentAction) + _ <- preinstallEngine(progressTracker, version, missingComponentAction) sockets <- languageServerGateway .start(progressTracker, clientId, project, version) .mapError { @@ -371,18 +362,7 @@ class ProjectService[ private def resolveProjectMetadata( project: Project ): F[ProjectServiceFailure, ProjectMetadata] = { - val version = for { - version <- resolveProjectVersion(project) - version <- configurationService - .resolveEnsoVersion(version) - .mapError { case ConfigurationFileAccessFailure(message) => - GlobalConfigurationAccessFailure( - "Could not deduce the default version to use for the project: " + - message - ) - } - } yield version - + val version = resolveProjectVersion(project) for { version <- version.map(Some(_)).recover { error => // TODO [RW] We may consider sending this warning to the IDE once @@ -483,11 +463,16 @@ class ProjectService[ private def resolveProjectVersion( project: Project - ): F[ProjectServiceFailure, EnsoVersion] = + ): F[ProjectServiceFailure, SemVer] = Sync[F] .blockingOp { + // TODO [RW] at some point we will need to use the configuration service to get the actual default version, see #1864 + val _ = configurationService + + val edition = + project.edition.getOrElse(DefaultEdition.getDefaultEdition) distributionConfiguration.editionManager - .resolveEngineVersion(project.edition) + .resolveEngineVersion(edition) .get } .mapError { error => diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/versionmanagement/ControllerInterface.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/versionmanagement/ControllerInterface.scala index c4da9ea713..808c4c27d9 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/versionmanagement/ControllerInterface.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/versionmanagement/ControllerInterface.scala @@ -1,18 +1,20 @@ package org.enso.projectmanager.service.versionmanagement -import java.util.UUID import akka.actor.ActorRef import com.typesafe.scalalogging.Logger import nl.gn0s1s.bump.SemVer -import org.enso.cli.task.{ProgressListener, TaskProgress} +import org.enso.cli.task.{ + ProgressNotification, + ProgressNotificationForwarder, + ProgressUnit +} import org.enso.distribution.locking.Resource -import org.enso.projectmanager.data.ProgressUnit import org.enso.runtimeversionmanager.components.{ GraalVMVersion, RuntimeVersionManagementUserInterface } -import scala.util.{Failure, Success, Try} +import java.util.UUID /** A [[RuntimeVersionManagementUserInterface]] that sends * [[ProgressNotification]] to the specified actors (both for usual tasks and @@ -27,54 +29,8 @@ class ControllerInterface( progressTracker: ActorRef, allowMissingComponents: Boolean, allowBrokenComponents: Boolean -) extends RuntimeVersionManagementUserInterface { - - /** @inheritdoc */ - override def trackProgress(message: String, task: TaskProgress[_]): Unit = { - var uuid: Option[UUID] = None - - /** Initializes the task on first invocation and just returns the - * generated UUID on further invocations. - */ - def initializeTask(total: Option[Long]): UUID = uuid match { - case Some(value) => value - case None => - val generated = UUID.randomUUID() - uuid = Some(generated) - val unit = ProgressUnit.fromTask(task) - progressTracker ! ProgressNotification.TaskStarted( - generated, - total, - unit - ) - generated - } - task.addProgressListener(new ProgressListener[Any] { - - /** @inheritdoc */ - override def progressUpdate( - done: Long, - total: Option[Long] - ): Unit = { - val uuid = initializeTask(total) - progressTracker ! ProgressNotification.TaskUpdate( - uuid, - Some(message), - done - ) - } - - /** @inheritdoc */ - override def done(result: Try[Any]): Unit = result match { - case Failure(exception) => - val uuid = initializeTask(None) - progressTracker ! ProgressNotification.TaskFailure(uuid, exception) - case Success(_) => - val uuid = initializeTask(None) - progressTracker ! ProgressNotification.TaskSuccess(uuid) - } - }) - } +) extends RuntimeVersionManagementUserInterface + with ProgressNotificationForwarder { /** @inheritdoc */ override def shouldInstallMissingEngine(version: SemVer): Boolean = @@ -101,7 +57,7 @@ class ControllerInterface( progressTracker ! ProgressNotification.TaskStarted( uuid, None, - ProgressUnit.Other + ProgressUnit.Unspecified ) progressTracker ! ProgressNotification.TaskUpdate( uuid, @@ -117,4 +73,10 @@ class ControllerInterface( progressTracker ! ProgressNotification.TaskSuccess(uuid) } } + + /** @inheritdoc */ + override def sendProgressNotification( + notification: ProgressNotification + ): Unit = + progressTracker ! notification } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/versionmanagement/ProgressNotification.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/versionmanagement/ProgressNotification.scala deleted file mode 100644 index 1a5a7a42bb..0000000000 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/versionmanagement/ProgressNotification.scala +++ /dev/null @@ -1,68 +0,0 @@ -package org.enso.projectmanager.service.versionmanagement - -import java.util.UUID - -import org.enso.jsonrpc.Notification -import org.enso.projectmanager.data.ProgressUnit -import org.enso.projectmanager.protocol.ProjectManagementApi - -/** Internal representation of progress notifications that are sent by the - * [[ControllerInterface]]. - * - * They are translated by the [[RequestHandler]] into protocol progress - * notifications. - */ -sealed trait ProgressNotification -object ProgressNotification { - - /** Singals that a new task with progress has been started. */ - case class TaskStarted( - taskId: UUID, - total: Option[Long], - unit: ProgressUnit - ) extends ProgressNotification - - /** Singals an update to task's progress. */ - case class TaskUpdate(taskId: UUID, message: Option[String], done: Long) - extends ProgressNotification - - /** Singals that a task has been finished successfully. */ - case class TaskSuccess(taskId: UUID) extends ProgressNotification - - /** Singals that a task has failed. */ - case class TaskFailure(taskId: UUID, throwable: Throwable) - extends ProgressNotification - - /** Translates a [[ProgressNotification]] into a protocol message. */ - def translateProgressNotification( - relatedOperationName: String, - progressNotification: ProgressNotification - ): Notification[_, _] = progressNotification match { - case TaskStarted(taskId, total, unit) => - Notification( - ProjectManagementApi.TaskStarted, - ProjectManagementApi.TaskStarted.Params( - taskId = taskId, - relatedOperation = relatedOperationName, - unit = unit, - total = total - ) - ) - case TaskUpdate(taskId, message, done) => - Notification( - ProjectManagementApi.TaskProgressUpdate, - ProjectManagementApi.TaskProgressUpdate.Params(taskId, message, done) - ) - case TaskSuccess(taskId) => - Notification( - ProjectManagementApi.TaskFinished, - ProjectManagementApi.TaskFinished.Params(taskId, None, success = true) - ) - case TaskFailure(taskId, throwable) => - Notification( - ProjectManagementApi.TaskFinished, - ProjectManagementApi.TaskFinished - .Params(taskId, Some(throwable.getMessage), success = false) - ) - } -} diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/versionmanagement/DefaultDistributionConfiguration.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/versionmanagement/DefaultDistributionConfiguration.scala index 950f8033bd..832ce6b7e4 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/versionmanagement/DefaultDistributionConfiguration.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/versionmanagement/DefaultDistributionConfiguration.scala @@ -49,9 +49,7 @@ object DefaultDistributionConfiguration lazy val resourceManager = new ResourceManager(lockManager) /** @inheritdoc */ - lazy val editionManager = new EditionManager( - distributionManager.paths.editionSearchPaths.toList - ) + lazy val editionManager = EditionManager(distributionManager) /** @inheritdoc */ lazy val temporaryDirectoryManager = diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/TestDistributionConfiguration.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/TestDistributionConfiguration.scala index 01ba7ba43d..24564a78d0 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/TestDistributionConfiguration.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/TestDistributionConfiguration.scala @@ -64,9 +64,7 @@ class TestDistributionConfiguration( lazy val resourceManager = new ResourceManager(lockManager) - lazy val editionManager: EditionManager = new EditionManager( - distributionManager.paths.editionSearchPaths.toList - ) + lazy val editionManager: EditionManager = EditionManager(distributionManager) lazy val temporaryDirectoryManager = new TemporaryDirectoryManager(distributionManager, resourceManager) diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenSpecBase.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenSpecBase.scala index 7aba427518..08ef588dab 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenSpecBase.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenSpecBase.scala @@ -4,7 +4,6 @@ import akka.testkit.TestActors.blackholeProps import io.circe.Json import io.circe.literal.JsonStringContext import nl.gn0s1s.bump.SemVer -import org.enso.editions.SemVerEnsoVersion import org.enso.projectmanager.data.MissingComponentAction import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps} import org.enso.testkit.RetrySpec @@ -51,7 +50,7 @@ abstract class ProjectOpenSpecBase val edition = config.edition.get config.copy(edition = Some( - edition.copy(engineVersion = Some(SemVerEnsoVersion(brokenVersion))) + edition.copy(engineVersion = Some(brokenVersion)) ) ) }) diff --git a/lib/scala/runtime-version-manager-test/src/main/scala/org/enso/runtimeversionmanager/test/FakeEnvironment.scala b/lib/scala/runtime-version-manager-test/src/main/scala/org/enso/runtimeversionmanager/test/FakeEnvironment.scala index 17f62d342b..f410ca17ab 100644 --- a/lib/scala/runtime-version-manager-test/src/main/scala/org/enso/runtimeversionmanager/test/FakeEnvironment.scala +++ b/lib/scala/runtime-version-manager-test/src/main/scala/org/enso/runtimeversionmanager/test/FakeEnvironment.scala @@ -44,11 +44,13 @@ trait FakeEnvironment { self: HasTestDirectory => val configDir = getTestDirectory.resolve("test_config") val binDir = getTestDirectory.resolve("test_bin") val runDir = getTestDirectory.resolve("test_run") + val homeDir = getTestDirectory.resolve("test_home") val env = extraOverrides .updated("ENSO_DATA_DIRECTORY", dataDir.toString) .updated("ENSO_CONFIG_DIRECTORY", configDir.toString) .updated("ENSO_BIN_DIRECTORY", binDir.toString) .updated("ENSO_RUNTIME_DIRECTORY", runDir.toString) + .updated("ENSO_HOME", homeDir.toString) val fakeEnvironment = new Environment { override def getPathToRunningExecutable: Path = executable diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala index e421f3c34f..569bcafde5 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala @@ -249,8 +249,11 @@ class Runner( ): SemVer = versionOverride.getOrElse { project match { case Some(project) => - val edition = project.edition - val version = editionManager.resolveEngineVersion(edition).get + // TODO [RW] properly get the default edition, see #1864 + val version = project.edition + .map(edition => editionManager.resolveEngineVersion(edition).get) + .map(SemVerEnsoVersion) + .getOrElse(DefaultEnsoVersion) version match { case DefaultEnsoVersion => globalConfigurationManager.defaultVersion diff --git a/lib/scala/task-progress-notifications/src/main/scala/org/enso/cli/task/notifications/ActorProgressNotificationForwarder.scala b/lib/scala/task-progress-notifications/src/main/scala/org/enso/cli/task/notifications/ActorProgressNotificationForwarder.scala new file mode 100644 index 0000000000..acdac50827 --- /dev/null +++ b/lib/scala/task-progress-notifications/src/main/scala/org/enso/cli/task/notifications/ActorProgressNotificationForwarder.scala @@ -0,0 +1,64 @@ +package org.enso.cli.task.notifications + +import akka.actor.ActorRef +import org.enso.cli.task.{ + ProgressNotification, + ProgressNotificationForwarder, + ProgressReporter +} +import org.enso.cli.task.ProgressNotification.{ + TaskFailure, + TaskStarted, + TaskSuccess, + TaskUpdate +} +import org.enso.jsonrpc.Notification + +object ActorProgressNotificationForwarder { + def translateAndForward( + relatedOperationName: String, + recipient: ActorRef + ): ProgressReporter = + new ProgressNotificationForwarder { + override def sendProgressNotification( + notification: ProgressNotification + ): Unit = { + val translated: Notification[_, _] = + translateProgressNotification(relatedOperationName, notification) + recipient ! translated + } + } + + /** Translates a [[ProgressNotification]] into a protocol message. */ + def translateProgressNotification( + relatedOperationName: String, + progressNotification: ProgressNotification + ): Notification[_, _] = progressNotification match { + case TaskStarted(taskId, total, unit) => + Notification( + TaskNotificationApi.TaskStarted, + TaskNotificationApi.TaskStarted.Params( + taskId = taskId, + relatedOperation = relatedOperationName, + unit = unit, + total = total + ) + ) + case TaskUpdate(taskId, message, done) => + Notification( + TaskNotificationApi.TaskProgressUpdate, + TaskNotificationApi.TaskProgressUpdate.Params(taskId, message, done) + ) + case TaskSuccess(taskId) => + Notification( + TaskNotificationApi.TaskFinished, + TaskNotificationApi.TaskFinished.Params(taskId, None, success = true) + ) + case TaskFailure(taskId, throwable) => + Notification( + TaskNotificationApi.TaskFinished, + TaskNotificationApi.TaskFinished + .Params(taskId, Option(throwable.getMessage), success = false) + ) + } +} diff --git a/lib/scala/task-progress-notifications/src/main/scala/org/enso/cli/task/notifications/SerializableProgressUnit.scala b/lib/scala/task-progress-notifications/src/main/scala/org/enso/cli/task/notifications/SerializableProgressUnit.scala new file mode 100644 index 0000000000..3376bf078b --- /dev/null +++ b/lib/scala/task-progress-notifications/src/main/scala/org/enso/cli/task/notifications/SerializableProgressUnit.scala @@ -0,0 +1,29 @@ +package org.enso.cli.task.notifications + +import enumeratum._ +import org.enso.cli.task.{ProgressUnit => TaskProgressUnit} + +/** Represents the unit used by progress updates. */ +sealed trait SerializableProgressUnit extends EnumEntry +object SerializableProgressUnit + extends Enum[SerializableProgressUnit] + with CirceEnum[SerializableProgressUnit] { + + /** Indicates that progress is measured by amount of bytes processed. */ + case object Bytes extends SerializableProgressUnit + + /** Indicates that progress is measured by some other unit or it is not + * measured at all. + */ + case object Other extends SerializableProgressUnit + + override val values = findValues + + /** Converts a [[TaskProgressUnit]] to [[SerializableProgressUnit]]. + */ + implicit def fromUnit(unit: TaskProgressUnit): SerializableProgressUnit = + unit match { + case TaskProgressUnit.Bytes => Bytes + case TaskProgressUnit.Unspecified => Other + } +} diff --git a/lib/scala/task-progress-notifications/src/main/scala/org/enso/cli/task/notifications/TaskNotificationApi.scala b/lib/scala/task-progress-notifications/src/main/scala/org/enso/cli/task/notifications/TaskNotificationApi.scala new file mode 100644 index 0000000000..41377ba933 --- /dev/null +++ b/lib/scala/task-progress-notifications/src/main/scala/org/enso/cli/task/notifications/TaskNotificationApi.scala @@ -0,0 +1,48 @@ +package org.enso.cli.task.notifications + +import org.enso.jsonrpc.{HasParams, Method} + +import java.util.UUID + +object TaskNotificationApi { + + case object TaskStarted extends Method("task/started") { + + case class Params( + taskId: UUID, + relatedOperation: String, + unit: SerializableProgressUnit, + total: Option[Long] + ) + + implicit val hasParams = new HasParams[this.type] { + type Params = TaskStarted.Params + } + } + + case object TaskProgressUpdate extends Method("task/progress-update") { + + case class Params( + taskId: UUID, + message: Option[String], + done: Long + ) + + implicit val hasParams = new HasParams[this.type] { + type Params = TaskProgressUpdate.Params + } + } + + case object TaskFinished extends Method("task/finished") { + + case class Params( + taskId: UUID, + message: Option[String], + success: Boolean + ) + + implicit val hasParams = new HasParams[this.type] { + type Params = TaskFinished.Params + } + } +} diff --git a/tools/legal-review/engine/report-state b/tools/legal-review/engine/report-state index 833019d253..5ec1cd7af9 100644 --- a/tools/legal-review/engine/report-state +++ b/tools/legal-review/engine/report-state @@ -1,3 +1,3 @@ -E4A61C4649AD6FB148D4779D9B20B395AEAB73475EE13D20EFB55C163724A77B +C03EC922F039EB5CC96F93A489453ADDD4FA6EBBF75713B05FE67B48CFF6ACBF 64FE86A276F737CE2B4D352D8F35F790CA0DA18F329A48A467F34FC9C3CAF07D 0 diff --git a/tools/legal-review/project-manager/report-state b/tools/legal-review/project-manager/report-state index a868585c94..93606a4a92 100644 --- a/tools/legal-review/project-manager/report-state +++ b/tools/legal-review/project-manager/report-state @@ -1,3 +1,3 @@ -0BA0D3694722E724BABC3D4D860FA5D11DA541B20632F18175E1F45BF44DE717 +0AED341E22D16C5722BF722FD5F92039464E9FE3CCD3288D3643F05E09AF62FB B7948AB0E996317E7F073260BA28A63D14215436079C09E793F803CF999D607C 0