From 9f8e60b7c7ae3df5506f4f56a92081c634ecf7a6 Mon Sep 17 00:00:00 2001 From: "scala-center-steward[bot]" <111975575+scala-center-steward[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 00:30:27 +0000 Subject: [PATCH] Reformat with scalafmt 3.8.3 Executed command: scalafmt --non-interactive --- .../com.olegych.scastie.api/ApiModels.scala | 100 ++--- .../CompilerInfo.scala | 25 +- .../ConsoleOutput.scala | 22 +- .../com.olegych.scastie.api/Inputs.scala | 62 ++- .../Instrumentation.scala | 32 +- .../com.olegych.scastie.api/Outputs.scala | 31 +- .../ProcessOutput.scala | 19 +- .../RuntimeError.scala | 36 +- .../com.olegych.scastie.api/SbtState.scala | 18 +- .../ScalaJsResult.scala | 21 +- .../com.olegych.scastie.api/ScalaTarget.scala | 133 +++--- .../ScalaTargetType.scala | 18 +- .../ScalaVersions.scala | 20 +- .../com.olegych.scastie.api/SnippetId.scala | 18 +- .../SnippetProgress.scala | 65 +-- .../StatusProgress.scala | 30 +- .../com.olegych.scastie.api/TaskId.scala | 4 +- .../DispatchActor.scala | 129 +++--- .../LoadBalancer.scala | 33 +- .../ProgressActor.scala | 33 +- .../com.olegych.scastie.balancer/Server.scala | 21 +- .../StatusActor.scala | 37 +- .../LoadBalancerRecoveryTest.scala | 134 +++--- .../LoadBalancerTest.scala | 30 +- .../LoadBalancerTestUtils.scala | 32 +- .../TestUtils.scala | 2 + .../AnsiColorFormatter.scala | 56 +-- .../ClientMain.scala | 71 +-- .../ConsoleState.scala | 10 +- .../EmbeddedOptions.scala | 166 +++---- .../EventStream.scala | 35 +- .../com.olegych.scastie.client/Global.scala | 72 ++- .../HTMLFormatter.scala | 21 +- .../LocalStorage.scala | 4 +- .../ModalState.scala | 27 +- .../RestApiClient.scala | 59 ++- .../com.olegych.scastie.client/Routing.scala | 86 ++-- .../RoutingADT.scala | 43 +- .../ScastieBackend.scala | 416 ++++++++---------- .../ScastieState.scala | 369 +++++++--------- .../StatusState.scala | 2 +- .../com.olegych.scastie.client/Views.scala | 23 +- .../components/BuildSettings.scala | 54 ++- .../components/ClearButton.scala | 20 +- .../components/CodeSnippets.scala | 145 +++--- .../components/Console.scala | 56 ++- .../components/CopyModal.scala | 2 +- .../components/DesktopButton.scala | 16 +- .../components/DownloadButton.scala | 26 +- .../components/EditorTopBar.scala | 141 +++--- .../components/EmbeddedOverlay.scala | 17 +- .../components/FormatButton.scala | 18 +- .../components/HelpModal.scala | 45 +- .../components/LoginModal.scala | 20 +- .../components/MainPanel.scala | 309 +++++++------ .../components/MetalsStatusIndicator.scala | 34 +- .../components/MobileBar.scala | 47 +- .../components/NewButton.scala | 29 +- .../components/PrivacyPolicyModal.scala | 14 +- .../components/PrivacyPolicyPrompt.scala | 41 +- .../components/PromptModal.scala | 38 +- .../components/RunButton.scala | 39 +- .../components/ScaladexSearch.scala | 288 ++++++------ .../components/Scastie.scala | 315 +++++++------ .../components/SideBar.scala | 86 ++-- .../components/Status.scala | 58 ++- .../components/TargetSelector.scala | 45 +- .../components/TopBar.scala | 104 +++-- .../components/VersionSelector.scala | 119 ++--- .../components/ViewToggleButton.scala | 37 +- .../components/WorksheetButton.scala | 35 +- .../components/editor/CodeEditor.scala | 195 ++++---- .../editor/DebouncingCapabilities.scala | 19 +- .../editor/DecorationProvider.scala | 95 ++-- .../components/editor/Editor.scala | 27 +- .../components/editor/EditorKeymaps.scala | 79 ++-- .../components/editor/EditorTextOps.scala | 7 +- .../editor/InteractiveProvider.scala | 39 +- .../editor/MetalsAutocompletion.scala | 157 ++++--- .../components/editor/MetalsClient.scala | 74 ++-- .../components/editor/MetalsHover.scala | 44 +- .../components/editor/OnChangeHandler.scala | 6 +- .../components/editor/SimpleEditor.scala | 95 ++-- .../editor/SyntaxHighlightingHandler.scala | 40 +- .../editor/SyntaxHighlightingPlugin.scala | 59 +-- .../editor/SyntaxHighlightingTheme.scala | 5 +- .../components/editor/TreesitterParser.scala | 9 +- .../components/package.scala | 92 ++-- .../com.olegych.scastie.client/package.scala | 10 +- deployment/announce.scala | 46 +- .../Instrument.scala | 112 +++-- .../InstrumentedInputs.scala | 30 +- .../Patch.scala | 17 +- .../Diff.scala | 39 +- .../InstrumentSpecs.scala | 16 +- project/CopyRecursively.scala | 15 +- project/Deployment.scala | 311 ++++++------- project/DockerHelper.scala | 32 +- project/GenerateProjects.scala | 23 +- .../SharedRuntime.scala | 2 + .../package.scala | 4 +- .../DomHook.scala | 7 +- .../Runtime.scala | 28 +- .../Runtime.scala | 9 +- .../Runtime.scala | 8 +- .../Runtime.scala | 11 +- .../com.olegych.scastie.sbt/FormatActor.scala | 24 +- .../OutputExtractor.scala | 85 ++-- .../com.olegych.scastie.sbt/SbtActor.scala | 54 +-- .../com.olegych.scastie.sbt/SbtMain.scala | 29 +- .../com.olegych.scastie.sbt/SbtProcess.scala | 250 +++++------ .../FormatActorTest.scala | 6 +- .../SbtActorTest.scala | 96 ++-- .../CompilerReporter.scala | 79 ++-- .../RuntimeErrorLogger.scala | 101 +++-- .../SbtScastiePlugin.scala | 32 +- .../src/main/scala/sbt/ScastieTrapExit.scala | 316 +++++++------ .../PlayJsonSupport.scala | 81 ++-- .../RestApiServer.scala | 43 +- .../com.olegych.scastie.web/ServerMain.scala | 14 +- .../oauth2/Github.scala | 33 +- .../oauth2/GithubUserSession.scala | 38 +- .../oauth2/InMemoryRefreshTokenStorage.scala | 35 +- .../oauth2/UserDirectives.scala | 17 +- .../routes/ApiRoutes.scala | 128 +++--- .../routes/DownloadRoutes.scala | 36 +- .../routes/FrontPageRoutes.scala | 88 ++-- .../routes/OAuth2Routes.scala | 117 +++-- .../routes/ProgressRoutes.scala | 22 +- .../routes/ScalaJsRoutes.scala | 65 ++- .../routes/ScalaLangRoutes.scala | 24 +- .../routes/StatusRoutes.scala | 77 ++-- .../routes/package.scala | 61 ++- .../SnippetIdMatcherTests.scala | 57 ++- .../OldScastieConverter.scala | 91 ++-- .../SnippetsContainer.scala | 66 +-- .../filesystem/FilesystemContainer.scala | 3 +- .../FilesystemSnippetsContainer.scala | 124 +++--- .../filesystem/FilesystemUsersContainer.scala | 30 +- .../GenericFilesystemContainer.scala | 1 + .../inmemory/InMemoryContainer.scala | 7 +- .../inmemory/InMemorySnippetsContainer.scala | 42 +- .../inmemory/InMemoryUsersContainer.scala | 7 +- .../mongodb/GenericMongoContainer.scala | 1 + .../mongodb/MongoDBContainer.scala | 7 +- .../mongodb/MongoDBSnippetsContainer.scala | 43 +- .../mongodb/MongoDBStoredClasses.scala | 28 +- .../mongodb/MongoDBUsersContainer.scala | 11 +- .../ContainerTest.scala | 120 ++--- .../com.olegych.scastie/util/Base64UUID.scala | 9 +- .../util/BlockingProcess.scala | 241 +++++----- .../util/GraphStageForwarder.scala | 10 +- .../util/GraphStageLogicForwarder.scala | 29 +- .../util/ProcessActor.scala | 65 ++- .../util/ReconnectingActor.scala | 21 +- .../com.olegych.scastie/util/SbtTask.scala | 3 +- .../util/ScastieFileUtil.scala | 6 +- .../ProcessActorTest.scala | 16 +- 158 files changed, 4666 insertions(+), 4848 deletions(-) diff --git a/api/src/main/scala/com.olegych.scastie.api/ApiModels.scala b/api/src/main/scala/com.olegych.scastie.api/ApiModels.scala index 2e7c3682b..9c4017ed6 100644 --- a/api/src/main/scala/com.olegych.scastie.api/ApiModels.scala +++ b/api/src/main/scala/com.olegych.scastie.api/ApiModels.scala @@ -9,39 +9,37 @@ case class SbtRunnerConnect(hostname: String, port: Int) case object ActorConnected object SnippetSummary { - implicit val formatSnippetSummary: OFormat[SnippetSummary] = - Json.format[SnippetSummary] + implicit val formatSnippetSummary: OFormat[SnippetSummary] = Json.format[SnippetSummary] } case class SnippetSummary( - snippetId: SnippetId, - summary: String, - time: Long + snippetId: SnippetId, + summary: String, + time: Long ) object FormatRequest { - implicit val formatFormatRequest: OFormat[FormatRequest] = - Json.format[FormatRequest] + implicit val formatFormatRequest: OFormat[FormatRequest] = Json.format[FormatRequest] } case class FormatRequest( - code: String, - isWorksheetMode: Boolean, - scalaTarget: ScalaTarget + code: String, + isWorksheetMode: Boolean, + scalaTarget: ScalaTarget ) object FormatResponse { + implicit object FormatResponseFormat extends Format[FormatResponse] { + def writes(response: FormatResponse): JsValue = { response.result match { - case Left(error) => - JsObject( + case Left(error) => JsObject( Seq( "Left" -> JsString(error) ) ) - case Right(formatedCode) => - JsObject( + case Right(formatedCode) => JsObject( Seq( "Right" -> JsString(formatedCode) ) @@ -51,51 +49,55 @@ object FormatResponse { def reads(json: JsValue): JsResult[FormatResponse] = { json match { - case JsObject(v) => - v.toList match { - case List(("Left", JsString(error))) => - JsSuccess(FormatResponse(Left(error))) + case JsObject(v) => v.toList match { + case List(("Left", JsString(error))) => JsSuccess(FormatResponse(Left(error))) - case List(("Right", JsString(formatedCode))) => - JsSuccess(FormatResponse(Right(formatedCode))) + case List(("Right", JsString(formatedCode))) => JsSuccess(FormatResponse(Right(formatedCode))) - case _ => - JsError(Seq()) + case _ => JsError(Seq()) } - case _ => - JsError(Seq()) + case _ => JsError(Seq()) } } + } -} +} object EitherFormat { import play.api.libs.functional.syntax._ + implicit object JsEither { - implicit def eitherReads[A, B](implicit A: Reads[A], B: Reads[B]): Reads[Either[A, B]] = { + implicit def eitherReads[A, B]( + implicit A: Reads[A], + B: Reads[B] + ): Reads[Either[A, B]] = { (JsPath \ "Left" \ "value").read[A].map(Left(_)) or (JsPath \ "Right" \ "value").read[B].map(Right(_)) } - implicit def eitherWrites[A, B](implicit A: Writes[A], B: Writes[B]): Writes[Either[A, B]] = Writes[Either[A, B]] { + implicit def eitherWrites[A, B]( + implicit A: Writes[A], + B: Writes[B] + ): Writes[Either[A, B]] = Writes[Either[A, B]] { case Left(value) => Json.obj("Left" -> Json.toJson(value)) case Right(value) => Json.obj("Right" -> Json.toJson(value)) } - } -} + } +} case class FormatResponse( - result: Either[String, String] + result: Either[String, String] ) object FetchResult { implicit val formatFetchResult: OFormat[FetchResult] = Json.format[FetchResult] - def create(inputs: Inputs, progresses: List[SnippetProgress]) = FetchResult(inputs, progresses.sortBy(p => (p.id, p.ts))) + def create(inputs: Inputs, progresses: List[SnippetProgress]) = + FetchResult(inputs, progresses.sortBy(p => (p.id, p.ts))) } case class FetchResult private (inputs: Inputs, progresses: List[SnippetProgress]) @@ -110,19 +112,17 @@ case class FetchScalaSource(snippetId: SnippetId) case class FetchResultScalaSource(content: String) object ScalaDependency { - implicit val formatScalaDependency: OFormat[ScalaDependency] = - Json.format[ScalaDependency] + implicit val formatScalaDependency: OFormat[ScalaDependency] = Json.format[ScalaDependency] } case class ScalaDependency( - groupId: String, - artifact: String, - target: ScalaTarget, - version: String + groupId: String, + artifact: String, + target: ScalaTarget, + version: String ) { - def matches(sd: ScalaDependency): Boolean = - sd.groupId == this.groupId && - sd.artifact == this.artifact + def matches(sd: ScalaDependency): Boolean = sd.groupId == this.groupId && + sd.artifact == this.artifact override def toString: String = target.renderSbt(this) } @@ -139,7 +139,7 @@ sealed trait FailureType { val msg: String } -case class NoResult(msg: String) extends FailureType +case class NoResult(msg: String) extends FailureType case class PresentationCompilerFailure(msg: String) extends FailureType object FailureType { @@ -151,7 +151,8 @@ object NoResult { } object PresentationCompilerFailure { - implicit val presentationCompilerFailureFormat: OFormat[PresentationCompilerFailure] = Json.format[PresentationCompilerFailure] + implicit val presentationCompilerFailureFormat: OFormat[PresentationCompilerFailure] = + Json.format[PresentationCompilerFailure] } object ScastieOffsetParams { @@ -181,7 +182,6 @@ case class CompletionItemDTO( symbol: Option[String] ) - case class HoverDTO(from: Int, to: Int, content: String) case class CompletionsDTO(items: Set[CompletionItemDTO]) @@ -195,7 +195,8 @@ object InsertInstructions { } object AdditionalInsertInstructions { - implicit val additionalInsertInstructionsFormat: OFormat[AdditionalInsertInstructions] = Json.format[AdditionalInsertInstructions] + implicit val additionalInsertInstructionsFormat: OFormat[AdditionalInsertInstructions] = + Json.format[AdditionalInsertInstructions] } object ScalaCompletionList { @@ -219,15 +220,14 @@ object HoverDTO { } object Project { - implicit val formatProject: OFormat[Project] = - Json.format[Project] + implicit val formatProject: OFormat[Project] = Json.format[Project] } case class Project( - organization: String, - repository: String, - logo: Option[String], - artifacts: List[String] + organization: String, + repository: String, + logo: Option[String], + artifacts: List[String] ) // Keep websocket connection diff --git a/api/src/main/scala/com.olegych.scastie.api/CompilerInfo.scala b/api/src/main/scala/com.olegych.scastie.api/CompilerInfo.scala index 3807457d0..5af129508 100644 --- a/api/src/main/scala/com.olegych.scastie.api/CompilerInfo.scala +++ b/api/src/main/scala/com.olegych.scastie.api/CompilerInfo.scala @@ -3,13 +3,14 @@ package com.olegych.scastie.api import play.api.libs.json._ object Severity { + implicit object SeverityFormat extends Format[Severity] { - def writes(severity: Severity): JsValue = - severity match { - case Info => JsString("Info") - case Warning => JsString("Warning") - case Error => JsString("Error") - } + + def writes(severity: Severity): JsValue = severity match { + case Info => JsString("Info") + case Warning => JsString("Warning") + case Error => JsString("Error") + } def reads(json: JsValue): JsResult[Severity] = { json match { @@ -19,20 +20,22 @@ object Severity { case _ => JsError(Seq()) } } + } + } sealed trait Severity -case object Info extends Severity +case object Info extends Severity case object Warning extends Severity -case object Error extends Severity +case object Error extends Severity object Problem { implicit val formatProblem: OFormat[Problem] = Json.format[Problem] } case class Problem( - severity: Severity, - line: Option[Int], - message: String + severity: Severity, + line: Option[Int], + message: String ) diff --git a/api/src/main/scala/com.olegych.scastie.api/ConsoleOutput.scala b/api/src/main/scala/com.olegych.scastie.api/ConsoleOutput.scala index ffa500676..c78656a2e 100644 --- a/api/src/main/scala/com.olegych.scastie.api/ConsoleOutput.scala +++ b/api/src/main/scala/com.olegych.scastie.api/ConsoleOutput.scala @@ -8,6 +8,7 @@ sealed trait ConsoleOutput { } object ConsoleOutput { + case class SbtOutput(output: ProcessOutput) extends ConsoleOutput { def show: String = s"sbt: ${output.line}" } @@ -22,13 +23,12 @@ object ConsoleOutput { implicit object ConsoleOutputFormat extends Format[ConsoleOutput] { val formatSbtOutput: OFormat[SbtOutput] = Json.format[SbtOutput] - private val formatUserOutput = Json.format[UserOutput] - private val formatScastieOutput = Json.format[ScastieOutput] + private val formatUserOutput = Json.format[UserOutput] + private val formatScastieOutput = Json.format[ScastieOutput] def writes(output: ConsoleOutput): JsValue = { output match { - case sbtOutput: SbtOutput => - formatSbtOutput.writes(sbtOutput) ++ JsObject(Seq("tpe" -> JsString("SbtOutput"))) + case sbtOutput: SbtOutput => formatSbtOutput.writes(sbtOutput) ++ JsObject(Seq("tpe" -> JsString("SbtOutput"))) case userOutput: UserOutput => formatUserOutput.writes(userOutput) ++ JsObject(Seq("tpe" -> JsString("UserOutput"))) case scastieOutput: ScastieOutput => @@ -41,18 +41,18 @@ object ConsoleOutput { case obj: JsObject => val vs = obj.value vs.get("tpe").orElse(vs.get("$type")) match { - case Some(tpe) => - tpe match { - case JsString("SbtOutput") => formatSbtOutput.reads(json) - case JsString("UserOutput") => formatUserOutput.reads(json) - case JsString("ScastieOutput") => - formatScastieOutput.reads(json) - case _ => JsError(Seq()) + case Some(tpe) => tpe match { + case JsString("SbtOutput") => formatSbtOutput.reads(json) + case JsString("UserOutput") => formatUserOutput.reads(json) + case JsString("ScastieOutput") => formatScastieOutput.reads(json) + case _ => JsError(Seq()) } case None => JsError(Seq()) } case _ => JsError(Seq()) } } + } + } diff --git a/api/src/main/scala/com.olegych.scastie.api/Inputs.scala b/api/src/main/scala/com.olegych.scastie.api/Inputs.scala index 1bba80b7f..2b11ab1b1 100644 --- a/api/src/main/scala/com.olegych.scastie.api/Inputs.scala +++ b/api/src/main/scala/com.olegych.scastie.api/Inputs.scala @@ -1,8 +1,7 @@ package com.olegych.scastie.api -import play.api.libs.json._ import com.olegych.scastie.buildinfo.BuildInfo - +import play.api.libs.json._ import System.{lineSeparator => nl} sealed trait BaseInputs { @@ -56,22 +55,23 @@ object Inputs { f ) } + } case class Inputs( - _isWorksheetMode: Boolean, - code: String, - target: ScalaTarget, - libraries: Set[ScalaDependency], - librariesFromList: List[(ScalaDependency, Project)], - sbtConfigExtra: String, - sbtConfigSaved: Option[String], - sbtPluginsConfigExtra: String, - sbtPluginsConfigSaved: Option[String], - isShowingInUserProfile: Boolean, - forked: Option[SnippetId] = None + _isWorksheetMode: Boolean, + code: String, + target: ScalaTarget, + libraries: Set[ScalaDependency], + librariesFromList: List[(ScalaDependency, Project)], + sbtConfigExtra: String, + sbtConfigSaved: Option[String], + sbtPluginsConfigExtra: String, + sbtPluginsConfigSaved: Option[String], + isShowingInUserProfile: Boolean, + forked: Option[SnippetId] = None ) extends BaseInputs { - val isWorksheetMode = _isWorksheetMode && target.hasWorksheetMode + val isWorksheetMode = _isWorksheetMode && target.hasWorksheetMode val librariesFrom: Map[ScalaDependency, Project] = librariesFromList.toMap private lazy val sbtInputs: (String, String) = (sbtConfig, sbtPluginsConfig) @@ -111,9 +111,11 @@ case class Inputs( lazy val isDefault: Boolean = copy(code = "").withSavedConfig == Inputs.default.copy(code = "").withSavedConfig - def modifyConfig(inputs: Inputs => Inputs): Inputs = inputs(this).copy(sbtConfigSaved = None, sbtPluginsConfigSaved = None) + def modifyConfig(inputs: Inputs => Inputs): Inputs = + inputs(this).copy(sbtConfigSaved = None, sbtPluginsConfigSaved = None) - def withSavedConfig: Inputs = copy(sbtConfigSaved = Some(sbtConfigGenerated), sbtPluginsConfigSaved = Some(sbtPluginsConfigGenerated)) + def withSavedConfig: Inputs = + copy(sbtConfigSaved = Some(sbtConfigGenerated), sbtPluginsConfigSaved = Some(sbtPluginsConfigGenerated)) def clearDependencies: Inputs = { modifyConfig { @@ -144,11 +146,10 @@ case class Inputs( def updateScalaDependency(scalaDependency: ScalaDependency, version: String): Inputs = { val newScalaDependency = scalaDependency.copy(version = version) - val newLibraries = libraries.filterNot(_.matches(scalaDependency)) + newScalaDependency + val newLibraries = libraries.filterNot(_.matches(scalaDependency)) + newScalaDependency val newLibrariesFromList = librariesFromList.collect { - case (l, p) if l.matches(scalaDependency) => - newScalaDependency -> p - case (l, p) => l -> p + case (l, p) if l.matches(scalaDependency) => newScalaDependency -> p + case (l, p) => l -> p } modifyConfig { _.copy( @@ -158,8 +159,7 @@ case class Inputs( } } - lazy val sbtConfig: String = - mapToConfig(sbtConfigGenerated, sbtConfigExtra) + lazy val sbtConfig: String = mapToConfig(sbtConfigGenerated, sbtConfigExtra) lazy val sbtConfigGenerated: String = sbtConfigSaved.getOrElse { val targetConfig = target.sbtConfig @@ -168,15 +168,14 @@ case class Inputs( if (target.hasWorksheetMode) target.runtimeDependency else None - val allLibraries = - optionalTargetDependency.map(libraries + _).getOrElse(libraries) + val allLibraries = optionalTargetDependency.map(libraries + _).getOrElse(libraries) val librariesConfig = if (allLibraries.isEmpty) "" else if (allLibraries.size == 1) { s"libraryDependencies += " + target.renderSbt(allLibraries.head) } else { - val nl = "\n" + val nl = "\n" val tab = " " "libraryDependencies ++= " + allLibraries @@ -191,31 +190,28 @@ case class Inputs( mapToConfig(targetConfig, librariesConfig) } - lazy val sbtPluginsConfig: String = - mapToConfig(sbtPluginsConfigGenerated, sbtPluginsConfigExtra) + lazy val sbtPluginsConfig: String = mapToConfig(sbtPluginsConfigGenerated, sbtPluginsConfigExtra) lazy val sbtPluginsConfigGenerated: String = sbtPluginsConfigSaved.getOrElse { sbtPluginsConfig0(withSbtScastie = true) } - private def mapToConfig(parts: String*): String = - parts.filter(_.nonEmpty).mkString("\n") + private def mapToConfig(parts: String*): String = parts.filter(_.nonEmpty).mkString("\n") private def sbtPluginsConfig0(withSbtScastie: Boolean): String = { val targetConfig = target.sbtPluginsConfig val sbtScastie = - if (withSbtScastie) - s"""addSbtPlugin("org.scastie" % "sbt-scastie" % "${BuildInfo.versionRuntime}")""" + if (withSbtScastie) s"""addSbtPlugin("org.scastie" % "sbt-scastie" % "${BuildInfo.versionRuntime}")""" else "" mapToConfig(targetConfig, sbtScastie) } + } object EditInputs { - implicit val formatEditInputs: OFormat[EditInputs] = - Json.format[EditInputs] + implicit val formatEditInputs: OFormat[EditInputs] = Json.format[EditInputs] } case class EditInputs(snippetId: SnippetId, inputs: Inputs) diff --git a/api/src/main/scala/com.olegych.scastie.api/Instrumentation.scala b/api/src/main/scala/com.olegych.scastie.api/Instrumentation.scala index 1176c018b..31543aef3 100644 --- a/api/src/main/scala/com.olegych.scastie.api/Instrumentation.scala +++ b/api/src/main/scala/com.olegych.scastie.api/Instrumentation.scala @@ -3,19 +3,17 @@ package com.olegych.scastie.api import play.api.libs.json._ object Render { + implicit object RenderFormat extends Format[Render] { - private val formatValue = Json.format[Value] - private val formatHtml = Json.format[Html] + private val formatValue = Json.format[Value] + private val formatHtml = Json.format[Html] private val formatAttachedDom = Json.format[AttachedDom] def writes(render: Render): JsValue = { render match { - case v: Value => - formatValue.writes(v) ++ JsObject(Seq("tpe" -> JsString("Value"))) - case h: Html => - formatHtml.writes(h) ++ JsObject(Seq("tpe" -> JsString("Html"))) - case a: AttachedDom => - formatAttachedDom.writes(a) ++ JsObject(Seq("tpe" -> JsString("AttachedDom"))) + case v: Value => formatValue.writes(v) ++ JsObject(Seq("tpe" -> JsString("Value"))) + case h: Html => formatHtml.writes(h) ++ JsObject(Seq("tpe" -> JsString("Html"))) + case a: AttachedDom => formatAttachedDom.writes(a) ++ JsObject(Seq("tpe" -> JsString("AttachedDom"))) } } @@ -24,8 +22,7 @@ object Render { case obj: JsObject => val vs = obj.value vs.get("tpe").orElse(vs.get("$type")) match { - case Some(tpe) => - tpe match { + case Some(tpe) => tpe match { case JsString("Value") => formatValue.reads(json) case JsString("Html") => formatHtml.reads(json) case JsString("AttachedDom") => formatAttachedDom.reads(json) @@ -36,26 +33,29 @@ object Render { case _ => JsError(Seq()) } } + } + } sealed trait Render case class Value(v: String, className: String) extends Render + case class Html(a: String, folded: Boolean = false) extends Render { def stripMargin: Html = copy(a = a.stripMargin) - def fold: Html = copy(folded = true) + def fold: Html = copy(folded = true) } + case class AttachedDom(uuid: String, folded: Boolean = false) extends Render { def fold: AttachedDom = copy(folded = true) } object Instrumentation { - val instrumentedObject = "Playground" - implicit val formatInstrumentation: OFormat[Instrumentation] = - Json.format[Instrumentation] + val instrumentedObject = "Playground" + implicit val formatInstrumentation: OFormat[Instrumentation] = Json.format[Instrumentation] } case class Instrumentation( - position: Position, - render: Render + position: Position, + render: Render ) diff --git a/api/src/main/scala/com.olegych.scastie.api/Outputs.scala b/api/src/main/scala/com.olegych.scastie.api/Outputs.scala index 6db35c81a..30c2c1b04 100644 --- a/api/src/main/scala/com.olegych.scastie.api/Outputs.scala +++ b/api/src/main/scala/com.olegych.scastie.api/Outputs.scala @@ -3,8 +3,7 @@ package com.olegych.scastie.api import play.api.libs.json._ object ReleaseOptions { - implicit val formatReleaseOptions: OFormat[ReleaseOptions] = - Json.format[ReleaseOptions] + implicit val formatReleaseOptions: OFormat[ReleaseOptions] = Json.format[ReleaseOptions] } case class ReleaseOptions(groupId: String, versions: List[String], version: String) @@ -12,8 +11,7 @@ case class ReleaseOptions(groupId: String, versions: List[String], version: Stri // case class MavenReference(groupId: String, artifactId: String, version: String) object Outputs { - implicit val formatOutputs: OFormat[Outputs] = - Json.format[Outputs] + implicit val formatOutputs: OFormat[Outputs] = Json.format[Outputs] def default: Outputs = Outputs( consoleOutputs = Vector(), @@ -22,27 +20,28 @@ object Outputs { runtimeError = None, sbtError = false ) + } + case class Outputs( - consoleOutputs: Vector[ConsoleOutput], - compilationInfos: Set[Problem], - instrumentations: Set[Instrumentation], - runtimeError: Option[RuntimeError], - sbtError: Boolean + consoleOutputs: Vector[ConsoleOutput], + compilationInfos: Set[Problem], + instrumentations: Set[Instrumentation], + runtimeError: Option[RuntimeError], + sbtError: Boolean ) { def console: String = consoleOutputs.map(_.show).mkString("\n") - def isClearable: Boolean = - consoleOutputs.nonEmpty || - compilationInfos.nonEmpty || - instrumentations.nonEmpty || - runtimeError.isDefined + def isClearable: Boolean = consoleOutputs.nonEmpty || + compilationInfos.nonEmpty || + instrumentations.nonEmpty || + runtimeError.isDefined + } object Position { - implicit val formatPosition: OFormat[Position] = - Json.format[Position] + implicit val formatPosition: OFormat[Position] = Json.format[Position] } case class Position(start: Int, end: Int) diff --git a/api/src/main/scala/com.olegych.scastie.api/ProcessOutput.scala b/api/src/main/scala/com.olegych.scastie.api/ProcessOutput.scala index b0a61a063..93f3ec381 100644 --- a/api/src/main/scala/com.olegych.scastie.api/ProcessOutput.scala +++ b/api/src/main/scala/com.olegych.scastie.api/ProcessOutput.scala @@ -3,20 +3,21 @@ package com.olegych.scastie.api import play.api.libs.json._ trait ProcessOutputType + object ProcessOutputType { case object StdOut extends ProcessOutputType case object StdErr extends ProcessOutputType implicit object ProcessOutputTypeFormat extends Format[ProcessOutputType] { + def writes(processOutputType: ProcessOutputType): JsValue = { JsString(processOutputType.toString) } - private val values = - List( - StdOut, - StdErr - ).map(v => (v.toString, v)).toMap + private val values = List( + StdOut, + StdErr + ).map(v => (v.toString, v)).toMap def reads(json: JsValue): JsResult[ProcessOutputType] = { json match { @@ -29,7 +30,9 @@ object ProcessOutputType { case _ => JsError(Seq()) } } + } + } object ProcessOutput { @@ -37,7 +40,7 @@ object ProcessOutput { } case class ProcessOutput( - line: String, - tpe: ProcessOutputType, - id: Option[Long] + line: String, + tpe: ProcessOutputType, + id: Option[Long] ) diff --git a/api/src/main/scala/com.olegych.scastie.api/RuntimeError.scala b/api/src/main/scala/com.olegych.scastie.api/RuntimeError.scala index 89b128cd2..714c7a47f 100644 --- a/api/src/main/scala/com.olegych.scastie.api/RuntimeError.scala +++ b/api/src/main/scala/com.olegych.scastie.api/RuntimeError.scala @@ -1,35 +1,32 @@ package com.olegych.scastie.api import java.io.{PrintWriter, StringWriter} + import play.api.libs.json._ case class RuntimeError( - message: String, - line: Option[Int], - fullStack: String + message: String, + line: Option[Int], + fullStack: String ) object RuntimeError { - implicit val formatRuntimeError: OFormat[RuntimeError] = - Json.format[RuntimeError] + implicit val formatRuntimeError: OFormat[RuntimeError] = Json.format[RuntimeError] def wrap[T](in: => T): Either[Option[RuntimeError], T] = { try { Right(in) } catch { - case ex: Exception => - Left(RuntimeError.fromThrowable(ex, fromScala = false)) + case ex: Exception => Left(RuntimeError.fromThrowable(ex, fromScala = false)) } } def fromThrowable(t: Throwable, fromScala: Boolean = true): Option[RuntimeError] = { def search(e: Throwable) = { e.getStackTrace - .find( - trace => - if (fromScala) - trace.getFileName == "main.scala" && trace.getLineNumber != -1 - else true + .find(trace => + if (fromScala) trace.getFileName == "main.scala" && trace.getLineNumber != -1 + else true ) .map(v => (e, Some(v.getLineNumber))) } @@ -41,20 +38,19 @@ object RuntimeError { else s } - loop(t).map { - case (err, line) => - val errors = new StringWriter() - t.printStackTrace(new PrintWriter(errors)) - val fullStack = errors.toString + loop(t).map { case (err, line) => + val errors = new StringWriter() + t.printStackTrace(new PrintWriter(errors)) + val fullStack = errors.toString - RuntimeError(err.toString, line, fullStack) + RuntimeError(err.toString, line, fullStack) } } + } object RuntimeErrorWrap { - implicit val formatRuntimeErrorWrap: OFormat[RuntimeErrorWrap] = - Json.format[RuntimeErrorWrap] + implicit val formatRuntimeErrorWrap: OFormat[RuntimeErrorWrap] = Json.format[RuntimeErrorWrap] } case class RuntimeErrorWrap(error: Option[RuntimeError]) diff --git a/api/src/main/scala/com.olegych.scastie.api/SbtState.scala b/api/src/main/scala/com.olegych.scastie.api/SbtState.scala index a6ca61c31..99317996d 100644 --- a/api/src/main/scala/com.olegych.scastie.api/SbtState.scala +++ b/api/src/main/scala/com.olegych.scastie.api/SbtState.scala @@ -3,27 +3,29 @@ package com.olegych.scastie.api import play.api.libs.json._ sealed trait SbtState extends ServerState + object SbtState { + case object Unknown extends SbtState { override def toString: String = "Unknown" - def isReady: Boolean = true + def isReady: Boolean = true } case object Disconnected extends SbtState { override def toString: String = "Disconnected" - def isReady: Boolean = false + def isReady: Boolean = false } implicit object SbtStateFormat extends Format[SbtState] { + def writes(state: SbtState): JsValue = { JsString(state.toString) } - private val values = - List( - Unknown, - Disconnected - ).map(v => (v.toString, v)).toMap + private val values = List( + Unknown, + Disconnected + ).map(v => (v.toString, v)).toMap def reads(json: JsValue): JsResult[SbtState] = { json match { @@ -36,5 +38,7 @@ object SbtState { case _ => JsError(Seq()) } } + } + } diff --git a/api/src/main/scala/com.olegych.scastie.api/ScalaJsResult.scala b/api/src/main/scala/com.olegych.scastie.api/ScalaJsResult.scala index cb9658d07..6856bd488 100644 --- a/api/src/main/scala/com.olegych.scastie.api/ScalaJsResult.scala +++ b/api/src/main/scala/com.olegych.scastie.api/ScalaJsResult.scala @@ -3,7 +3,7 @@ package com.olegych.scastie.api import play.api.libs.json._ case class ScalaJsResult( - in: Either[Option[RuntimeError], List[Instrumentation]] + in: Either[Option[RuntimeError], List[Instrumentation]] ) object ScalaJsResult { @@ -11,15 +11,13 @@ object ScalaJsResult { private case class Instrumentations(instrs: List[Instrumentation]) implicit object ScalaJsResultFormat extends Format[ScalaJsResult] { - private val formatLeft = Json.format[Error] + private val formatLeft = Json.format[Error] private val formatRight = Json.format[Instrumentations] def writes(result: ScalaJsResult): JsValue = { result.in match { - case Left(err) => - formatLeft.writes(Error(err)) ++ JsObject(Seq("tpe" -> JsString("Left"))) - case Right(instrs) => - formatRight.writes(Instrumentations(instrs)) ++ JsObject(Seq("tpe" -> JsString("Right"))) + case Left(err) => formatLeft.writes(Error(err)) ++ JsObject(Seq("tpe" -> JsString("Left"))) + case Right(instrs) => formatRight.writes(Instrumentations(instrs)) ++ JsObject(Seq("tpe" -> JsString("Right"))) } } @@ -28,12 +26,9 @@ object ScalaJsResult { case obj: JsObject => val vs = obj.value vs.get("tpe").orElse(vs.get("$type")) match { - case Some(JsString(tpe)) => - tpe match { - case "Left" => - formatLeft.reads(json).map(v => ScalaJsResult(Left(v.er))) - case "Right" => - formatRight + case Some(JsString(tpe)) => tpe match { + case "Left" => formatLeft.reads(json).map(v => ScalaJsResult(Left(v.er))) + case "Right" => formatRight .reads(json) .map(v => ScalaJsResult(Right(v.instrs))) case _ => JsError(Seq()) @@ -43,5 +38,7 @@ object ScalaJsResult { case _ => JsError(Seq()) } } + } + } diff --git a/api/src/main/scala/com.olegych.scastie.api/ScalaTarget.scala b/api/src/main/scala/com.olegych.scastie.api/ScalaTarget.scala index f8035eed8..d7dcdcb5a 100644 --- a/api/src/main/scala/com.olegych.scastie.api/ScalaTarget.scala +++ b/api/src/main/scala/com.olegych.scastie.api/ScalaTarget.scala @@ -11,13 +11,11 @@ sealed trait ScalaTarget { def sbtPluginsConfig: String def sbtRunCommand(worksheetMode: Boolean): String - def runtimeDependency: Option[ScalaDependency] = - ScalaTarget.runtimeDependencyFrom(this) + def runtimeDependency: Option[ScalaDependency] = ScalaTarget.runtimeDependencyFrom(this) def hasWorksheetMode: Boolean = true - protected def sbtConfigScalaVersion: String = - s"""scalaVersion := "$scalaVersion"""" + protected def sbtConfigScalaVersion: String = s"""scalaVersion := "$scalaVersion"""" protected def renderSbtDouble(lib: ScalaDependency): String = { import lib._ @@ -42,10 +40,11 @@ object ScalaTarget { import play.api.libs.json._ implicit object ScalaTargetFormat extends Format[ScalaTarget] { - private val formatJvm = Json.format[Jvm] - private val formatJs = Json.format[Js] + private val formatJvm = Json.format[Jvm] + private val formatJs = Json.format[Js] private val formatTypelevel = Json.format[Typelevel] - private val formatNative = Json.format[Native] + private val formatNative = Json.format[Native] + private val formatScala3: OFormat[Scala3] = { // Scala3.dottyVersion has been renamed to Scala3.scalaVersion // so we use the default write @@ -66,16 +65,11 @@ object ScalaTarget { def writes(target: ScalaTarget): JsValue = { target match { - case jvm: Jvm => - formatJvm.writes(jvm) ++ JsObject(Seq("tpe" -> JsString("Jvm"))) - case js: Js => - formatJs.writes(js) ++ JsObject(Seq("tpe" -> JsString("Js"))) - case typelevel: Typelevel => - formatTypelevel.writes(typelevel) ++ JsObject(Seq("tpe" -> JsString("Typelevel"))) - case native: Native => - formatNative.writes(native) ++ JsObject(Seq("tpe" -> JsString("Native"))) - case dotty: Scala3 => - formatScala3.writes(dotty) ++ JsObject(Seq("tpe" -> JsString("Scala3"))) + case jvm: Jvm => formatJvm.writes(jvm) ++ JsObject(Seq("tpe" -> JsString("Jvm"))) + case js: Js => formatJs.writes(js) ++ JsObject(Seq("tpe" -> JsString("Js"))) + case typelevel: Typelevel => formatTypelevel.writes(typelevel) ++ JsObject(Seq("tpe" -> JsString("Typelevel"))) + case native: Native => formatNative.writes(native) ++ JsObject(Seq("tpe" -> JsString("Native"))) + case dotty: Scala3 => formatScala3.writes(dotty) ++ JsObject(Seq("tpe" -> JsString("Scala3"))) } } @@ -84,8 +78,7 @@ object ScalaTarget { case obj: JsObject => val vs = obj.value vs.get("tpe").orElse(vs.get("$type")) match { - case Some(JsString(tpe)) => - tpe match { + case Some(JsString(tpe)) => tpe match { case "Jvm" => formatJvm.reads(json) case "Js" => formatJs.reads(json) case "Typelevel" => formatTypelevel.reads(json) @@ -98,6 +91,7 @@ object ScalaTarget { case _ => JsError(Seq()) } } + } private def runtimeDependencyFrom(target: ScalaTarget): Option[ScalaDependency] = Some( @@ -114,6 +108,7 @@ object ScalaTarget { } private def partialUnificationSbtPlugin = """addSbtPlugin("org.lyranthe.sbt" % "partial-unification" % "1.1.2")""" + private def hktScalacOptions(scalaVersion: String) = { val (kpOrg, kpVersion, kpCross) = if (scalaVersion == "2.13.0-M5") ("org.spire-math", "0.9.9", "binary") @@ -133,19 +128,16 @@ object ScalaTarget { case class Jvm(scalaVersion: String) extends ScalaTarget { - def targetType: ScalaTargetType = - ScalaTargetType.Scala2 + def targetType: ScalaTargetType = ScalaTargetType.Scala2 - def scaladexRequest: Map[String, String] = - Map("target" -> "JVM", "scalaVersion" -> binaryScalaVersion) + def scaladexRequest: Map[String, String] = Map("target" -> "JVM", "scalaVersion" -> binaryScalaVersion) - def renderSbt(lib: ScalaDependency): String = - renderSbtDouble(lib) + def renderSbt(lib: ScalaDependency): String = renderSbtDouble(lib) def sbtConfig: String = { val base = sbtConfigScalaVersion + "\n" + hktScalacOptions(scalaVersion) if (scalaVersion.startsWith("2.13") || scalaVersion.startsWith("2.12")) - base + "\n" + "scalacOptions += \"-Ydelambdafy:inline\"" //workaround https://github.com/scala/bug/issues/10782 + base + "\n" + "scalacOptions += \"-Ydelambdafy:inline\"" // workaround https://github.com/scala/bug/issues/10782 else base } @@ -157,20 +149,16 @@ object ScalaTarget { } object Typelevel { - def default: ScalaTarget = - ScalaTarget.Typelevel(scalaVersion = "2.12.3-bin-typelevel-4") + def default: ScalaTarget = ScalaTarget.Typelevel(scalaVersion = "2.12.3-bin-typelevel-4") } case class Typelevel(scalaVersion: String) extends ScalaTarget { - def targetType: ScalaTargetType = - ScalaTargetType.Typelevel + def targetType: ScalaTargetType = ScalaTargetType.Typelevel - def scaladexRequest: Map[String, String] = - Map("target" -> "JVM", "scalaVersion" -> scalaVersion) + def scaladexRequest: Map[String, String] = Map("target" -> "JVM", "scalaVersion" -> scalaVersion) - def renderSbt(lib: ScalaDependency): String = - renderSbtDouble(lib) + def renderSbt(lib: ScalaDependency): String = renderSbtDouble(lib) def sbtConfig: String = { s"""|$sbtConfigScalaVersion @@ -185,31 +173,30 @@ object ScalaTarget { } object Js { - val targetFilename = "fastopt.js" + val targetFilename = "fastopt.js" val sourceMapFilename: String = targetFilename + ".map" - val sourceFilename = "main.scala" - val sourceUUID = "file:///tmp/LxvjvKARSa2U5ctNis9LIA" + val sourceFilename = "main.scala" + val sourceUUID = "file:///tmp/LxvjvKARSa2U5ctNis9LIA" def default = ScalaTarget.Js( scalaVersion = BuildInfo.jsScalaVersion, scalaJsVersion = BuildInfo.defaultScalaJsVersion ) + } case class Js(scalaVersion: String, scalaJsVersion: String) extends ScalaTarget { - def targetType: ScalaTargetType = - ScalaTargetType.JS + def targetType: ScalaTargetType = ScalaTargetType.JS def scaladexRequest: Map[String, String] = Map( - "target" -> "JS", + "target" -> "JS", "scalaVersion" -> binaryScalaVersion, "scalaJsVersion" -> (if (scalaJsVersion.startsWith("0.")) scalaJsVersion.split('.').init.mkString(".") else scalaJsVersion.split('.').head) ) - def renderSbt(lib: ScalaDependency): String = - s"${renderSbtCross(lib)} cross CrossVersion.for3Use2_13" + def renderSbt(lib: ScalaDependency): String = s"${renderSbtCross(lib)} cross CrossVersion.for3Use2_13" def sbtConfig: String = { s"""|$sbtConfigScalaVersion @@ -219,13 +206,13 @@ object ScalaTarget { |scalacOptions += { | val from = (LocalRootProject / baseDirectory).value.toURI.toString | val to = "${ScalaTarget.Js.sourceUUID}/" - | "-${if (scalaVersion.startsWith("3")) "scalajs-mapSourceURI" else "P:scalajs:mapSourceURI"}:" + from + "->" + to + | "-${if (scalaVersion.startsWith("3")) "scalajs-mapSourceURI" + else "P:scalajs:mapSourceURI"}:" + from + "->" + to |}""".stripMargin } - def sbtPluginsConfig: String = - s"""addSbtPlugin("org.scala-js" % "sbt-scalajs" % "$scalaJsVersion")""" + "\n" + - (if (!scalaVersion.startsWith("3")) partialUnificationSbtPlugin else "") + def sbtPluginsConfig: String = s"""addSbtPlugin("org.scala-js" % "sbt-scalajs" % "$scalaJsVersion")""" + "\n" + + (if (!scalaVersion.startsWith("3")) partialUnificationSbtPlugin else "") def sbtRunCommand(worksheetMode: Boolean): String = "fastOptJS" @@ -233,55 +220,49 @@ object ScalaTarget { } object Native { - def default: Native = - ScalaTarget.Native( - scalaVersion = "2.11.11", - scalaNativeVersion = "0.3.3" - ) + + def default: Native = ScalaTarget.Native( + scalaVersion = "2.11.11", + scalaNativeVersion = "0.3.3" + ) + } case class Native(scalaVersion: String, scalaNativeVersion: String) extends ScalaTarget { - def targetType: ScalaTargetType = - ScalaTargetType.Native + def targetType: ScalaTargetType = ScalaTargetType.Native - def scaladexRequest: Map[String, String] = - Map( - "target" -> "NATIVE", - "scalaVersion" -> binaryScalaVersion, - "scalaNativeVersion" -> scalaNativeVersion - ) + def scaladexRequest: Map[String, String] = Map( + "target" -> "NATIVE", + "scalaVersion" -> binaryScalaVersion, + "scalaNativeVersion" -> scalaNativeVersion + ) - def renderSbt(lib: ScalaDependency): String = - renderSbtCross(lib) + def renderSbt(lib: ScalaDependency): String = renderSbtCross(lib) def sbtConfig: String = sbtConfigScalaVersion - def sbtPluginsConfig: String = - s"""addSbtPlugin("org.scala-native" % "sbt-scala-native" % "$scalaNativeVersion")""" + def sbtPluginsConfig: String = s"""addSbtPlugin("org.scala-native" % "sbt-scala-native" % "$scalaNativeVersion")""" def sbtRunCommand(worksheetMode: Boolean): String = if (worksheetMode) "fgRunMain Main" else "fgRun" - override def toString: String = - s"Scala-Native $scalaVersion $scalaNativeVersion" + override def toString: String = s"Scala-Native $scalaVersion $scalaNativeVersion" } object Scala3 { def default: ScalaTarget = Scala3(BuildInfo.stableLTS) - def defaultCode: String = - """|// You can find more examples here: - |// https://github.com/lampepfl/dotty-example-project - |println("Hi Scala 3!") - |""".stripMargin + def defaultCode: String = """|// You can find more examples here: + |// https://github.com/lampepfl/dotty-example-project + |println("Hi Scala 3!") + |""".stripMargin + } case class Scala3(scalaVersion: String) extends ScalaTarget { - def targetType: ScalaTargetType = - ScalaTargetType.Scala3 + def targetType: ScalaTargetType = ScalaTargetType.Scala3 - def scaladexRequest: Map[String, String] = - Map("target" -> "JVM", "scalaVersion" -> binaryScalaVersion) + def scaladexRequest: Map[String, String] = Map("target" -> "JVM", "scalaVersion" -> binaryScalaVersion) def renderSbt(lib: ScalaDependency): String = { if (Some(lib) == runtimeDependency) renderSbtDouble(lib) @@ -296,7 +277,7 @@ object ScalaTarget { def sbtRunCommand(worksheetMode: Boolean): String = if (worksheetMode) "fgRunMain Main" else "fgRun" - override def toString: String = - s"Scala $scalaVersion" + override def toString: String = s"Scala $scalaVersion" } + } diff --git a/api/src/main/scala/com.olegych.scastie.api/ScalaTargetType.scala b/api/src/main/scala/com.olegych.scastie.api/ScalaTargetType.scala index 7bc640e9e..17a3ee513 100644 --- a/api/src/main/scala/com.olegych.scastie.api/ScalaTargetType.scala +++ b/api/src/main/scala/com.olegych.scastie.api/ScalaTargetType.scala @@ -20,18 +20,18 @@ object ScalaTargetType { } implicit object ScalaTargetTypeFormat extends Format[ScalaTargetType] { + def writes(scalaTargetType: ScalaTargetType): JsValue = { JsString(scalaTargetType.toString) } - private val values = - List( - Scala2, - Scala3, - JS, - Native, - Typelevel - ).map(v => (v.toString, v)).toMap + private val values = List( + Scala2, + Scala3, + JS, + Native, + Typelevel + ).map(v => (v.toString, v)).toMap def reads(json: JsValue): JsResult[ScalaTargetType] = { json match { @@ -44,6 +44,7 @@ object ScalaTargetType { case _ => JsError(Seq()) } } + } case object Scala2 extends ScalaTargetType { @@ -65,4 +66,5 @@ object ScalaTargetType { case object Typelevel extends ScalaTargetType { def defaultScalaTarget: ScalaTarget = ScalaTarget.Typelevel.default } + } diff --git a/api/src/main/scala/com.olegych.scastie.api/ScalaVersions.scala b/api/src/main/scala/com.olegych.scastie.api/ScalaVersions.scala index 86f09288d..21b334a82 100644 --- a/api/src/main/scala/com.olegych.scastie.api/ScalaVersions.scala +++ b/api/src/main/scala/com.olegych.scastie.api/ScalaVersions.scala @@ -3,19 +3,20 @@ package com.olegych.scastie.api import com.olegych.scastie.buildinfo.BuildInfo object ScalaVersions { + def suggestedScalaVersions(tpe: ScalaTargetType): List[String] = { val versions = tpe match { case ScalaTargetType.Scala3 => List(BuildInfo.stableLTS, BuildInfo.stableNext) - case ScalaTargetType.JS => List(BuildInfo.stableLTS, BuildInfo.stableNext, BuildInfo.latest213, BuildInfo.latest212) - case _ => List(BuildInfo.latest213, BuildInfo.latest212) + case ScalaTargetType.JS => + List(BuildInfo.stableLTS, BuildInfo.stableNext, BuildInfo.latest213, BuildInfo.latest212) + case _ => List(BuildInfo.latest213, BuildInfo.latest212) } versions.distinct } def allVersions(tpe: ScalaTargetType): List[String] = { val versions = tpe match { - case ScalaTargetType.Scala3 => - List( + case ScalaTargetType.Scala3 => List( BuildInfo.latestNext, BuildInfo.stableNext, BuildInfo.latestLTS, @@ -39,10 +40,10 @@ object ScalaVersions { "3.0.1", "3.0.0" ) - case ScalaTargetType.JS => - allVersions(ScalaTargetType.Scala3) ++ allVersions(ScalaTargetType.Scala2).filter(v => v.startsWith("2.12") || v.startsWith("2.13")) - case _ => - List( + case ScalaTargetType.JS => allVersions(ScalaTargetType.Scala3) ++ allVersions(ScalaTargetType.Scala2).filter(v => + v.startsWith("2.12") || v.startsWith("2.13") + ) + case _ => List( BuildInfo.latest213, "2.13.13", "2.13.12", @@ -100,6 +101,5 @@ object ScalaVersions { versions.distinct } - def find(tpe: ScalaTargetType, sv: String): String = - allVersions(tpe).find(_.startsWith(sv)).getOrElse(sv) + def find(tpe: ScalaTargetType, sv: String): String = allVersions(tpe).find(_.startsWith(sv)).getOrElse(sv) } diff --git a/api/src/main/scala/com.olegych.scastie.api/SnippetId.scala b/api/src/main/scala/com.olegych.scastie.api/SnippetId.scala index f1e2685be..615f2dd51 100644 --- a/api/src/main/scala/com.olegych.scastie.api/SnippetId.scala +++ b/api/src/main/scala/com.olegych.scastie.api/SnippetId.scala @@ -14,23 +14,21 @@ case class User(login: String, name: Option[String], avatar_url: String) { } object SnippetUserPart { - implicit val formatSnippetUserPart: OFormat[SnippetUserPart] = - Json.format[SnippetUserPart] + implicit val formatSnippetUserPart: OFormat[SnippetUserPart] = Json.format[SnippetUserPart] } case class SnippetUserPart(login: String, update: Int = 0) object SnippetId { - implicit val formatSnippetId: OFormat[SnippetId] = - Json.format[SnippetId] + implicit val formatSnippetId: OFormat[SnippetId] = Json.format[SnippetId] } case class SnippetId(base64UUID: String, user: Option[SnippetUserPart]) { + def isOwnedBy(user2: Option[User]): Boolean = { (user, user2) match { - case (Some(SnippetUserPart(snippetLogin, _)), Some(User(userLogin, _, _))) => - snippetLogin == userLogin - case _ => false + case (Some(SnippetUserPart(snippetLogin, _)), Some(User(userLogin, _, _))) => snippetLogin == userLogin + case _ => false } } @@ -38,9 +36,8 @@ case class SnippetId(base64UUID: String, user: Option[SnippetUserPart]) { def url: String = { this match { - case SnippetId(uuid, None) => uuid - case SnippetId(uuid, Some(SnippetUserPart(login, update))) => - s"$login/$uuid/$update" + case SnippetId(uuid, None) => uuid + case SnippetId(uuid, Some(SnippetUserPart(login, update))) => s"$login/$uuid/$update" } } @@ -48,4 +45,5 @@ case class SnippetId(base64UUID: String, user: Option[SnippetUserPart]) { val middle = url s"/api/${Shared.scalaJsHttpPathPrefix}/$middle/$end" } + } diff --git a/api/src/main/scala/com.olegych.scastie.api/SnippetProgress.scala b/api/src/main/scala/com.olegych.scastie.api/SnippetProgress.scala index 9db7b39b3..314b2d3ef 100644 --- a/api/src/main/scala/com.olegych.scastie.api/SnippetProgress.scala +++ b/api/src/main/scala/com.olegych.scastie.api/SnippetProgress.scala @@ -3,44 +3,45 @@ package com.olegych.scastie.api import play.api.libs.json._ object SnippetProgress { - def default: SnippetProgress = - SnippetProgress( - ts = None, - id = None, - snippetId = None, - userOutput = None, - sbtOutput = None, - compilationInfos = Nil, - instrumentations = Nil, - runtimeError = None, - scalaJsContent = None, - scalaJsSourceMapContent = None, - isDone = true, - isTimeout = false, - isSbtError = false, - isForcedProgramMode = false - ) + + def default: SnippetProgress = SnippetProgress( + ts = None, + id = None, + snippetId = None, + userOutput = None, + sbtOutput = None, + compilationInfos = Nil, + instrumentations = Nil, + runtimeError = None, + scalaJsContent = None, + scalaJsSourceMapContent = None, + isDone = true, + isTimeout = false, + isSbtError = false, + isForcedProgramMode = false + ) implicit val formatSnippetProgress: OFormat[SnippetProgress] = Json.format[SnippetProgress] } case class SnippetProgress( - ts: Option[Long], - id: Option[Long], - snippetId: Option[SnippetId], - userOutput: Option[ProcessOutput], - sbtOutput: Option[ProcessOutput], - compilationInfos: List[Problem], - instrumentations: List[Instrumentation], - runtimeError: Option[RuntimeError], - scalaJsContent: Option[String], - scalaJsSourceMapContent: Option[String], - isDone: Boolean, - isTimeout: Boolean, - isSbtError: Boolean, - isForcedProgramMode: Boolean + ts: Option[Long], + id: Option[Long], + snippetId: Option[SnippetId], + userOutput: Option[ProcessOutput], + sbtOutput: Option[ProcessOutput], + compilationInfos: List[Problem], + instrumentations: List[Instrumentation], + runtimeError: Option[RuntimeError], + scalaJsContent: Option[String], + scalaJsSourceMapContent: Option[String], + isDone: Boolean, + isTimeout: Boolean, + isSbtError: Boolean, + isForcedProgramMode: Boolean ) { - def isFailure: Boolean = isTimeout || isSbtError || runtimeError.nonEmpty || compilationInfos.exists(_.severity == Error) + def isFailure: Boolean = + isTimeout || isSbtError || runtimeError.nonEmpty || compilationInfos.exists(_.severity == Error) override def toString: String = Json.toJsObject(this).toString() } diff --git a/api/src/main/scala/com.olegych.scastie.api/StatusProgress.scala b/api/src/main/scala/com.olegych.scastie.api/StatusProgress.scala index 55bcaa229..f97007c2c 100644 --- a/api/src/main/scala/com.olegych.scastie.api/StatusProgress.scala +++ b/api/src/main/scala/com.olegych.scastie.api/StatusProgress.scala @@ -3,25 +3,26 @@ package com.olegych.scastie.api import play.api.libs.json._ object SbtRunnerState { - implicit val formatSbtRunnerState: OFormat[SbtRunnerState] = - Json.format[SbtRunnerState] + implicit val formatSbtRunnerState: OFormat[SbtRunnerState] = Json.format[SbtRunnerState] } case class SbtRunnerState( - config: Inputs, - tasks: Vector[TaskId], - sbtState: SbtState + config: Inputs, + tasks: Vector[TaskId], + sbtState: SbtState ) + sealed trait StatusProgress + object StatusProgress { + implicit object StatusProgressFormat extends Format[StatusProgress] { private val formatSbt = Json.format[StatusProgress.Sbt] def writes(status: StatusProgress): JsValue = { status match { - case StatusProgress.KeepAlive => - JsObject(Seq("tpe" -> JsString("StatusProgress.KeepAlive"))) + case StatusProgress.KeepAlive => JsObject(Seq("tpe" -> JsString("StatusProgress.KeepAlive"))) case runners: StatusProgress.Sbt => formatSbt.writes(runners) ++ JsObject(Seq("tpe" -> JsString("StatusProgress.Sbt"))) } @@ -29,15 +30,11 @@ object StatusProgress { def reads(json: JsValue): JsResult[StatusProgress] = { json match { - case obj: JsObject => - obj.value.get("tpe").orElse(obj.value.get("$type")) match { - case Some(tpe) => - tpe match { - case JsString("StatusProgress.KeepAlive") => - JsSuccess(StatusProgress.KeepAlive) + case obj: JsObject => obj.value.get("tpe").orElse(obj.value.get("$type")) match { + case Some(tpe) => tpe match { + case JsString("StatusProgress.KeepAlive") => JsSuccess(StatusProgress.KeepAlive) - case JsString("StatusProgress.Sbt") => - formatSbt.reads(json) + case JsString("StatusProgress.Sbt") => formatSbt.reads(json) case _ => JsError(Seq()) } @@ -46,8 +43,9 @@ object StatusProgress { case _ => JsError(Seq()) } } + } - case object KeepAlive extends StatusProgress + case object KeepAlive extends StatusProgress case class Sbt(runners: Vector[SbtRunnerState]) extends StatusProgress } diff --git a/api/src/main/scala/com.olegych.scastie.api/TaskId.scala b/api/src/main/scala/com.olegych.scastie.api/TaskId.scala index e811b87cf..3a1c5122c 100644 --- a/api/src/main/scala/com.olegych.scastie.api/TaskId.scala +++ b/api/src/main/scala/com.olegych.scastie.api/TaskId.scala @@ -1,12 +1,10 @@ package com.olegych.scastie.api import play.api.libs.json._ - import play.api.libs.json.OFormat object TaskId { - implicit val formatSbtRunTaskId: OFormat[TaskId] = - Json.format[TaskId] + implicit val formatSbtRunTaskId: OFormat[TaskId] = Json.format[TaskId] } case class TaskId(snippetId: SnippetId) diff --git a/balancer/src/main/scala/com.olegych.scastie.balancer/DispatchActor.scala b/balancer/src/main/scala/com.olegych.scastie.balancer/DispatchActor.scala index 3f0181550..80520cd63 100644 --- a/balancer/src/main/scala/com.olegych.scastie.balancer/DispatchActor.scala +++ b/balancer/src/main/scala/com.olegych.scastie.balancer/DispatchActor.scala @@ -1,5 +1,11 @@ package com.olegych.scastie.balancer +import java.nio.file.Paths +import java.time.Instant +import java.util.concurrent.Executors +import scala.concurrent._ +import scala.concurrent.duration._ + import akka.actor.Actor import akka.actor.ActorLogging import akka.actor.ActorRef @@ -19,12 +25,6 @@ import com.olegych.scastie.storage.mongodb._ import com.olegych.scastie.util._ import com.typesafe.config.ConfigFactory -import java.nio.file.Paths -import java.time.Instant -import java.util.concurrent.Executors -import scala.concurrent._ -import scala.concurrent.duration._ - case class Address(host: String, port: Int) case class SbtConfig(config: String) @@ -63,27 +63,25 @@ case object Ping class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) // extends PersistentActor with AtLeastOnceDelivery - extends Actor - with ActorLogging { + extends Actor + with ActorLogging { - override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() { - case e => - log.error(e, "failure") - SupervisorStrategy.resume + override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() { case e => + log.error(e, "failure") + SupervisorStrategy.resume } - private val config = - ConfigFactory.load().getConfig("com.olegych.scastie.balancer") - private val host = config.getString("remote-hostname") + private val config = ConfigFactory.load().getConfig("com.olegych.scastie.balancer") + private val host = config.getString("remote-hostname") private val sbtPortsStart = config.getInt("remote-sbt-ports-start") - private val sbtPortsSize = config.getInt("remote-sbt-ports-size") + private val sbtPortsSize = config.getInt("remote-sbt-ports-size") private val sbtPorts = (0 until sbtPortsSize).map(sbtPortsStart + _) private def connectRunner( - runnerName: String, - actorName: String, - host: String + runnerName: String, + actorName: String, + host: String )(port: Int): ((String, Int), ActorSelection) = { val path = s"akka://$runnerName@$host:$port/user/$actorName" log.info(s"Connecting to ${path}") @@ -92,14 +90,12 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) (host, port) -> selection } - private var remoteSbtSelections = - sbtPorts.map(connectRunner("SbtRunner", "SbtActor", host)).toMap + private var remoteSbtSelections = sbtPorts.map(connectRunner("SbtRunner", "SbtActor", host)).toMap private var sbtLoadBalancer: SbtBalancer = { - val sbtServers = remoteSbtSelections.to(Vector).map { - case (_, ref) => - val state: SbtState = SbtState.Unknown - Server(ref, Inputs.default, state) + val sbtServers = remoteSbtSelections.to(Vector).map { case (_, ref) => + val state: SbtState = SbtState.Unknown + Server(ref, Inputs.default, state) } LoadBalancer(servers = sbtServers) @@ -124,19 +120,19 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) val containerType = config.getString("snippets-storage") - private val container = - containerType match { - case "memory" => new InMemoryContainer() - case "mongo" => new MongoDBContainer()(ExecutionContext.fromExecutor(Executors.newWorkStealingPool())) - case "mongo-local" => new MongoDBContainer(defaultConfig = false)(ExecutionContext.fromExecutor(Executors.newWorkStealingPool())) - case "files" => new FilesystemContainer( + private val container = containerType match { + case "memory" => new InMemoryContainer() + case "mongo" => new MongoDBContainer()(ExecutionContext.fromExecutor(Executors.newWorkStealingPool())) + case "mongo-local" => + new MongoDBContainer(defaultConfig = false)(ExecutionContext.fromExecutor(Executors.newWorkStealingPool())) + case "files" => new FilesystemContainer( Paths.get(config.getString("snippets-dir")), Paths.get(config.getString("old-snippets-dir")) )(ExecutionContext.fromExecutorService(Executors.newCachedThreadPool())) - case _ => - println("fallback to in-memory container") - new InMemoryContainer - } + case _ => + println("fallback to in-memory container") + new InMemoryContainer + } private def updateSbtBalancer(newSbtBalancer: SbtBalancer): Unit = { if (sbtLoadBalancer != newSbtBalancer) { @@ -146,11 +142,12 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) () } - //can be called from future + // can be called from future private def run(inputsWithIpAndUser: InputsWithIpAndUser, snippetId: SnippetId): Unit = { self ! Run(inputsWithIpAndUser, snippetId) } - //cannot be called from future + + // cannot be called from future private def run0(inputsWithIpAndUser: InputsWithIpAndUser, snippetId: SnippetId): Unit = { val InputsWithIpAndUser(inputs, UserTrace(ip, user)) = inputsWithIpAndUser @@ -172,8 +169,8 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) } private def logError[T](f: Future[T]) = { - f.recover { - case e => log.error(e, "failed future") + f.recover { case e => + log.error(e, "failed future") } } @@ -188,7 +185,7 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) case x @ RunSnippet(inputsWithIpAndUser) => log.info(s"starting ${x}") val InputsWithIpAndUser(inputs, UserTrace(_, user)) = inputsWithIpAndUser - val sender = this.sender() + val sender = this.sender() logError(container.create(inputs, user.map(u => UserLogin(u.login))).map { snippetId => sender ! snippetId run(inputsWithIpAndUser, snippetId) @@ -197,7 +194,7 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) case SaveSnippet(inputsWithIpAndUser) => val InputsWithIpAndUser(inputs, UserTrace(_, user)) = inputsWithIpAndUser - val sender = this.sender() + val sender = this.sender() logError(container.save(inputs, user.map(u => UserLogin(u.login))).map { snippetId => sender ! snippetId run(inputsWithIpAndUser, snippetId) @@ -207,15 +204,12 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) val sender = this.sender() logError(container.update(snippetId, inputsWithIpAndUser.inputs).map { updatedSnippetId => sender ! updatedSnippetId - updatedSnippetId.foreach( - snippetIdU => run(inputsWithIpAndUser, snippetIdU) - ) + updatedSnippetId.foreach(snippetIdU => run(inputsWithIpAndUser, snippetIdU)) }) case ForkSnippet(snippetId, inputsWithIpAndUser) => - val InputsWithIpAndUser(inputs, UserTrace(_, user)) = - inputsWithIpAndUser - val sender = this.sender() + val InputsWithIpAndUser(inputs, UserTrace(_, user)) = inputsWithIpAndUser + val sender = this.sender() logError( container .fork(snippetId, inputs, user.map(u => UserLogin(u.login))) @@ -277,38 +271,33 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) logError( container .appendOutput(progress) - .recover { - case e => - log.error(e, s"failed to save $progress from $sender") - e + .recover { case e => + log.error(e, s"failed to save $progress from $sender") + e } .map(sender ! _) ) - case done: Done => - done.progress.snippetId.foreach { sid => + case done: Done => done.progress.snippetId.foreach { sid => val newBalancer = sbtLoadBalancer.done(TaskId(sid)) newBalancer match { - case Some(newBalancer) => - updateSbtBalancer(newBalancer) + case Some(newBalancer) => updateSbtBalancer(newBalancer) case None => if (done.retries >= 0) { system.scheduler.scheduleOnce(1.second) { self ! done.copy(retries = done.retries - 1) } } else { - val taskIds = - sbtLoadBalancer.servers.flatMap(_.mailbox.map(_.taskId)) + val taskIds = sbtLoadBalancer.servers.flatMap(_.mailbox.map(_.taskId)) log.error(s"stopped retrying to update ${taskIds} with ${done}") } } } - case event: DisassociatedEvent => - for { + case event: DisassociatedEvent => for { host <- event.remoteAddress.host port <- event.remoteAddress.port - ref <- remoteSbtSelections.get((host, port)) + ref <- remoteSbtSelections.get((host, port)) } { log.warning("removing disconnected: {}", ref) val previousRemoteSbtSelections = remoteSbtSelections @@ -318,11 +307,9 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) } } - case SbtUp => - log.info("SbtUp") + case SbtUp => log.info("SbtUp") - case Replay(SbtRun(snippetId, inputs, progressActor, snippetActor)) => - log.info("Replay: " + inputs.code) + case Replay(SbtRun(snippetId, inputs, progressActor, snippetActor)) => log.info("Replay: " + inputs.code) case SbtRunnerConnect(runnerHostname, runnerAkkaPort) => if (!remoteSbtSelections.contains((runnerHostname, runnerAkkaPort))) { @@ -344,14 +331,11 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) ) } - case ReceiveStatus(requester) => - sender() ! LoadBalancerInfo(sbtLoadBalancer, requester) + case ReceiveStatus(requester) => sender() ! LoadBalancerInfo(sbtLoadBalancer, requester) - case statusProgress: StatusProgress => - statusActor ! statusProgress + case statusProgress: StatusProgress => statusActor ! statusProgress - case run: Run => - run0(run.inputsWithIpAndUser, run.snippetId) + case run: Run => run0(run.inputsWithIpAndUser, run.snippetId) case ping: Ping.type => implicit val timeout: Timeout = Timeout(10.seconds) logError(Future.sequence { @@ -360,10 +344,11 @@ class DispatchActor(progressActor: ActorRef, statusActor: ActorRef) .map { _ => log.info(s"pinged ${s.ref} server") } - .recover { - case e => log.error(e, s"couldn't ping ${s} server") + .recover { case e => + log.error(e, s"couldn't ping ${s} server") } } }) } + } diff --git a/balancer/src/main/scala/com.olegych.scastie.balancer/LoadBalancer.scala b/balancer/src/main/scala/com.olegych.scastie.balancer/LoadBalancer.scala index 950f0f473..66efe5ce0 100644 --- a/balancer/src/main/scala/com.olegych.scastie.balancer/LoadBalancer.scala +++ b/balancer/src/main/scala/com.olegych.scastie.balancer/LoadBalancer.scala @@ -1,23 +1,25 @@ package com.olegych.scastie.balancer -import java.time.Instant import java.time.temporal.ChronoUnit +import java.time.Instant +import scala.util.Random import com.olegych.scastie.api._ import org.slf4j.LoggerFactory -import scala.util.Random - case class Ip(v: String) case class Task(config: Inputs, ip: Ip, taskId: TaskId, ts: Instant) case class TaskHistory(data: Vector[Task], maxSize: Int) { + def add(task: Task): TaskHistory = { val cappedData = if (data.length < maxSize) data else data.drop(1) copy(data = cappedData :+ task) } + } + case class LoadBalancer[R, S <: ServerState](servers: Vector[Server[R, S]]) { private val log = LoggerFactory.getLogger(getClass) @@ -41,21 +43,26 @@ case class LoadBalancer[R, S <: ServerState](servers: Vector[Server[R, S]]) { def add(task: Task): Option[(Server[R, S], LoadBalancer[R, S])] = { log.info("Task added: {}", task.taskId) - val (availableServers, unavailableServers) = - servers.partition(_.state.isReady) + val (availableServers, unavailableServers) = servers.partition(_.state.isReady) def lastTenMinutes(v: Vector[Task]) = v.filter(_.ts.isAfter(Instant.now.minus(10, ChronoUnit.MINUTES))) - def lastWithIp(v: Vector[Task]) = lastTenMinutes(v.filter(_.ip == task.ip)).lastOption + def lastWithIp(v: Vector[Task]) = lastTenMinutes(v.filter(_.ip == task.ip)).lastOption if (availableServers.nonEmpty) { val selectedServer = availableServers.maxBy { s => ( - s.mailbox.length < 3, //allow reload if server gets busy - !s.currentConfig.needsReload(task.config), //pick those without need for reload - -s.mailbox.length, //then those least busy - lastTenMinutes(s.mailbox ++ s.history.data).exists(!_.config.needsReload(task.config)), //then those which use(d) this config - lastWithIp(s.mailbox).orElse(lastWithIp(s.history.data)).map(_.ts.toEpochMilli), //then one most recently used by this ip, if any - s.mailbox.lastOption.orElse(s.history.data.lastOption).map(-_.ts.toEpochMilli).getOrElse(0L) //then one least recently used + s.mailbox.length < 3, // allow reload if server gets busy + !s.currentConfig.needsReload(task.config), // pick those without need for reload + -s.mailbox.length, // then those least busy + lastTenMinutes(s.mailbox ++ s.history.data) + .exists(!_.config.needsReload(task.config)), // then those which use(d) this config + lastWithIp(s.mailbox) + .orElse(lastWithIp(s.history.data)) + .map(_.ts.toEpochMilli), // then one most recently used by this ip, if any + s.mailbox.lastOption + .orElse(s.history.data.lastOption) + .map(-_.ts.toEpochMilli) + .getOrElse(0L) // then one least recently used ) } val updatedServers = availableServers.map(old => if (old.id == selectedServer.id) old.add(task) else old) @@ -63,7 +70,7 @@ case class LoadBalancer[R, S <: ServerState](servers: Vector[Server[R, S]]) { ( selectedServer, copy( - servers = updatedServers ++ unavailableServers, + servers = updatedServers ++ unavailableServers // history = updatedHistory ) ) diff --git a/balancer/src/main/scala/com.olegych.scastie.balancer/ProgressActor.scala b/balancer/src/main/scala/com.olegych.scastie.balancer/ProgressActor.scala index bde039147..162c59f32 100644 --- a/balancer/src/main/scala/com.olegych.scastie.balancer/ProgressActor.scala +++ b/balancer/src/main/scala/com.olegych.scastie.balancer/ProgressActor.scala @@ -1,15 +1,15 @@ package com.olegych.scastie package balancer -import akka.NotUsed +import scala.collection.mutable.{Map => MMap, Queue => MQueue} +import scala.concurrent.duration.DurationLong + import akka.actor.{Actor, ActorRef} import akka.stream.scaladsl.Source +import akka.NotUsed import com.olegych.scastie.api._ import com.olegych.scastie.util.GraphStageForwarder -import scala.collection.mutable.{Map => MMap, Queue => MQueue} -import scala.concurrent.duration.DurationLong - case class SubscribeProgress(snippetId: SnippetId) private case class Cleanup(snippetId: SnippetId) @@ -25,15 +25,16 @@ class ProgressActor extends Actor { val (source, _) = getOrCreateNewSubscriberInfo(snippetId, self) sender() ! source - case snippetProgress: SnippetProgress => - snippetProgress.snippetId.foreach { snippetId => + case snippetProgress: SnippetProgress => snippetProgress.snippetId.foreach { snippetId => getOrCreateNewSubscriberInfo(snippetId, self) queuedMessages.getOrElseUpdate(snippetId, MQueue()).enqueue(snippetProgress) sendQueuedMessages(snippetId, self) } case (snippedId: SnippetId, graphStageForwarderActor: ActorRef) => - subscribers.get(snippedId).foreach(s => subscribers.update(snippedId, s.copy(_2 = Some(graphStageForwarderActor)))) + subscribers + .get(snippedId) + .foreach(s => subscribers.update(snippedId, s.copy(_2 = Some(graphStageForwarderActor)))) sendQueuedMessages(snippedId, self) case Cleanup(snippetId) => @@ -48,13 +49,13 @@ class ProgressActor extends Actor { ) } - private def sendQueuedMessages(snippetId: SnippetId, self: ActorRef): Unit = - for { - messageQueue <- queuedMessages.get(snippetId).toSeq - (_, Some(graphStageForwarderActor)) <- subscribers.get(snippetId).toSeq - message <- messageQueue.dequeueAll(_ => true) - } yield { - graphStageForwarderActor ! message - if (message.isDone) context.system.scheduler.scheduleOnce(3.seconds, self, Cleanup(snippetId))(context.dispatcher) - } + private def sendQueuedMessages(snippetId: SnippetId, self: ActorRef): Unit = for { + messageQueue <- queuedMessages.get(snippetId).toSeq + (_, Some(graphStageForwarderActor)) <- subscribers.get(snippetId).toSeq + message <- messageQueue.dequeueAll(_ => true) + } yield { + graphStageForwarderActor ! message + if (message.isDone) context.system.scheduler.scheduleOnce(3.seconds, self, Cleanup(snippetId))(context.dispatcher) + } + } diff --git a/balancer/src/main/scala/com.olegych.scastie.balancer/Server.scala b/balancer/src/main/scala/com.olegych.scastie.balancer/Server.scala index 217995bd6..a1513a87e 100644 --- a/balancer/src/main/scala/com.olegych.scastie.balancer/Server.scala +++ b/balancer/src/main/scala/com.olegych.scastie.balancer/Server.scala @@ -1,31 +1,32 @@ package com.olegych.scastie.balancer -import com.olegych.scastie.api._ - import scala.util.Random +import com.olegych.scastie.api._ + case class Server[R, S]( - ref: R, - lastConfig: Inputs, - state: S, - mailbox: Vector[Task] = Vector.empty, - history: TaskHistory = TaskHistory(Vector.empty, 1000), - id: Int = Random.nextInt(), + ref: R, + lastConfig: Inputs, + state: S, + mailbox: Vector[Task] = Vector.empty, + history: TaskHistory = TaskHistory(Vector.empty, 1000), + id: Int = Random.nextInt() ) { def currentTaskId: Option[TaskId] = mailbox.headOption.map(_.taskId) - def currentConfig: Inputs = mailbox.headOption.map(_.config).getOrElse(lastConfig) + def currentConfig: Inputs = mailbox.headOption.map(_.config).getOrElse(lastConfig) def done(taskId: TaskId): Server[R, S] = { val (newMailbox, done) = mailbox.partition(_.taskId != taskId) copy( lastConfig = done.headOption.map(_.config).getOrElse(lastConfig), mailbox = newMailbox, - history = done.foldLeft(history)(_.add(_)), + history = done.foldLeft(history)(_.add(_)) ) } def add(task: Task): Server[R, S] = { copy(mailbox = mailbox :+ task) } + } diff --git a/balancer/src/main/scala/com.olegych.scastie.balancer/StatusActor.scala b/balancer/src/main/scala/com.olegych.scastie.balancer/StatusActor.scala index 23b39dbdf..3761fc1af 100644 --- a/balancer/src/main/scala/com.olegych.scastie.balancer/StatusActor.scala +++ b/balancer/src/main/scala/com.olegych.scastie.balancer/StatusActor.scala @@ -1,14 +1,12 @@ package com.olegych.scastie.balancer -import com.olegych.scastie.api._ - -import akka.actor.{Actor, ActorLogging, ActorRef, Props} -import akka.stream.scaladsl.Source import java.util.concurrent.TimeUnit - import scala.collection.mutable import scala.concurrent.duration._ +import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import akka.stream.scaladsl.Source +import com.olegych.scastie.api._ import com.olegych.scastie.util.GraphStageForwarder case object SubscribeStatus @@ -21,6 +19,7 @@ case class SetDispatcher(dispatchActor: ActorRef) object StatusActor { def props: Props = Props(new StatusActor) } + class StatusActor private () extends Actor with ActorLogging { private var publishers = mutable.Buffer.empty[ActorRef] @@ -29,16 +28,14 @@ class StatusActor private () extends Actor with ActorLogging { override def receive: Receive = { case SubscribeStatus => { - val publisherGraphStage = - new GraphStageForwarder("StatusActor-GraphStageForwarder", self, None) + val publisherGraphStage = new GraphStageForwarder("StatusActor-GraphStageForwarder", self, None) - val source = - Source - .fromGraph(publisherGraphStage) - .keepAlive( - FiniteDuration(1, TimeUnit.SECONDS), - () => StatusProgress.KeepAlive - ) + val source = Source + .fromGraph(publisherGraphStage) + .keepAlive( + FiniteDuration(1, TimeUnit.SECONDS), + () => StatusProgress.KeepAlive + ) sender() ! source } @@ -63,14 +60,14 @@ class StatusActor private () extends Actor with ActorLogging { private def convertSbt(newSbtBalancer: SbtBalancer): StatusProgress = { StatusProgress.Sbt( - newSbtBalancer.servers.map( - server => - SbtRunnerState( - config = server.lastConfig, - tasks = server.mailbox.map(_.taskId), - sbtState = server.state + newSbtBalancer.servers.map(server => + SbtRunnerState( + config = server.lastConfig, + tasks = server.mailbox.map(_.taskId), + sbtState = server.state ) ) ) } + } diff --git a/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerRecoveryTest.scala b/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerRecoveryTest.scala index 13208d021..79940b588 100644 --- a/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerRecoveryTest.scala +++ b/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerRecoveryTest.scala @@ -1,5 +1,8 @@ package com.olegych.scastie.balancer +import scala.concurrent._ +import scala.concurrent.duration._ + import akka.actor.{ActorSystem, Props} import akka.pattern.ask import akka.testkit.{ImplicitSender, TestKit, TestProbe} @@ -8,31 +11,27 @@ import com.olegych.scastie.api._ import com.olegych.scastie.sbt._ import com.olegych.scastie.util.ReconnectInfo import com.typesafe.config.{Config, ConfigFactory} -import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike - -import scala.concurrent._ -import scala.concurrent.duration._ +import org.scalatest.BeforeAndAfterAll class LoadBalancerRecoveryTest() - extends TestKit( - ActorSystem("LoadBalancerRecoveryTest", RemotePortConfig(0)) - ) - with ImplicitSender - with AnyFunSuiteLike - with BeforeAndAfterAll { + extends TestKit( + ActorSystem("LoadBalancerRecoveryTest", RemotePortConfig(0)) + ) + with ImplicitSender + with AnyFunSuiteLike + with BeforeAndAfterAll { // import system.dispatcher implicit val timeout: Timeout = Timeout(25.seconds) test("recover from crash") { - val crash = - """|val f = classOf[sun.misc.Unsafe].getDeclaredField("theUnsafe") - |f.setAccessible(true) - |val unsafe = f.get(null).asInstanceOf[sun.misc.Unsafe] - |println("TRYING TO CRASH JVM") - |unsafe.putLong(0, 0) - |println("SHOULD HAVE CRASHED!")""".stripMargin + val crash = """|val f = classOf[sun.misc.Unsafe].getDeclaredField("theUnsafe") + |f.setAccessible(true) + |val unsafe = f.get(null).asInstanceOf[sun.misc.Unsafe] + |println("TRYING TO CRASH JVM") + |unsafe.putLong(0, 0) + |println("SHOULD HAVE CRASHED!")""".stripMargin val code1 = "println(1)" val code3 = "println(2)" @@ -54,39 +53,37 @@ class LoadBalancerRecoveryTest() } private val serverAkkaPort = 15000 - private val webSystem = ActorSystem("Web", RemotePortConfig(serverAkkaPort)) + private val webSystem = ActorSystem("Web", RemotePortConfig(serverAkkaPort)) private val sbtAkkaPort = 5150 - private val sbtSystem = - ActorSystem("SbtRunner", RemotePortConfig(sbtAkkaPort)) + private val sbtSystem = ActorSystem("SbtRunner", RemotePortConfig(sbtAkkaPort)) - private val progressActor = TestProbe() - private val statusActor = TestProbe() + private val progressActor = TestProbe() + private val statusActor = TestProbe() private val sbtActorReadyProbe = TestProbe() private val localhost = "127.0.0.1" - private val sbtActor = - sbtSystem.actorOf( - Props( - new SbtActor( - system = sbtSystem, - runTimeout = 10.seconds, - sbtReloadTimeout = 20.seconds, - isProduction = false, - readyRef = Some(sbtActorReadyProbe.ref), - reconnectInfo = Some( - ReconnectInfo( - serverHostname = localhost, - serverAkkaPort = serverAkkaPort, - actorHostname = localhost, - actorAkkaPort = sbtAkkaPort - ) + private val sbtActor = sbtSystem.actorOf( + Props( + new SbtActor( + system = sbtSystem, + runTimeout = 10.seconds, + sbtReloadTimeout = 20.seconds, + isProduction = false, + readyRef = Some(sbtActorReadyProbe.ref), + reconnectInfo = Some( + ReconnectInfo( + serverHostname = localhost, + serverAkkaPort = serverAkkaPort, + actorHostname = localhost, + actorAkkaPort = sbtAkkaPort ) ) - ), - name = "SbtActor" - ) + ) + ), + name = "SbtActor" + ) sbtActorReadyProbe.fishForMessage(60.seconds) { case SbtActorReady => { @@ -101,20 +98,19 @@ class LoadBalancerRecoveryTest() } } - private val dispatchActor = - webSystem.actorOf( - Props(new DispatchActor(progressActor.ref, statusActor.ref)), - name = "DispatchActor" - ) + private val dispatchActor = webSystem.actorOf( + Props(new DispatchActor(progressActor.ref, statusActor.ref)), + name = "DispatchActor" + ) private var id = 0 + private def run(code: String): SnippetId = { - val wrapped = - s"""|object Main { - | def main(args: Array[String]): Unit = { - | $code - | } - |}""".stripMargin + val wrapped = s"""|object Main { + | def main(args: Array[String]): Unit = { + | $code + | } + |}""".stripMargin val inputs = Inputs.default.copy(code = wrapped, _isWorksheetMode = false) @@ -131,7 +127,7 @@ class LoadBalancerRecoveryTest() } private def waitFor(sid: SnippetId, ret: Map[SnippetId, String])( - f: SnippetProgress => Boolean + f: SnippetProgress => Boolean ): Unit = { progressActor.fishForMessage(50.seconds) { @@ -159,22 +155,24 @@ class LoadBalancerRecoveryTest() TestKit.shutdownActorSystem(sbtSystem) TestKit.shutdownActorSystem(system) } + } object RemotePortConfig { - def apply(port: Int): Config = - ConfigFactory.parseString( - s"""|akka { - | actor { - | provider = cluster - | allow-java-serialization = on - | } - | remote { - | artery.canonical { - | hostname = "127.0.0.1" - | port = $port - | } - | } - |}""".stripMargin - ) + + def apply(port: Int): Config = ConfigFactory.parseString( + s"""|akka { + | actor { + | provider = cluster + | allow-java-serialization = on + | } + | remote { + | artery.canonical { + | hostname = "127.0.0.1" + | port = $port + | } + | } + |}""".stripMargin + ) + } diff --git a/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerTest.scala b/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerTest.scala index 3d0dc2c70..3679f2436 100644 --- a/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerTest.scala +++ b/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerTest.scala @@ -11,7 +11,7 @@ class LoadBalancerTest extends LoadBalancerTestUtils { 1 * "c2", 1 * "c3", 1 * "c4" - ), + ) ) assertConfigs(add(balancer, sbtConfig("c8")))( @@ -27,7 +27,7 @@ class LoadBalancerTest extends LoadBalancerTestUtils { val balancer = LoadBalancer( servers( 5 * "c1" - ), + ) ) assertConfigs(add(balancer, sbtConfig("c1")))( @@ -39,7 +39,7 @@ class LoadBalancerTest extends LoadBalancerTestUtils { val balancer = LoadBalancer( servers( 5 * "c1" - ), + ) ) assertConfigs(add(balancer, sbtConfig("c2")))( 4 * "c1", @@ -49,13 +49,13 @@ class LoadBalancerTest extends LoadBalancerTestUtils { test("server notify when it's done") { val balancer = LoadBalancer( - servers(1 * "c1"), + servers(1 * "c1") ) val server = balancer.servers.head assert(server.mailbox.isEmpty) - val c1 = sbtConfig("c1") + val c1 = sbtConfig("c1") val taskId = TestTaskId(1) val (assigned, balancer0) = balancer.add(Task(c1, nextIp, taskId, Instant.now)).get @@ -69,16 +69,15 @@ class LoadBalancerTest extends LoadBalancerTestUtils { test("run two tasks") { val balancer = LoadBalancer( - servers(1 * "c1"), + servers(1 * "c1") ) val server = balancer.servers.head assert(server.mailbox.isEmpty) assert(server.currentTaskId.isEmpty) - val taskId1 = TestTaskId(1) - val (assigned0, balancer0) = - balancer.add(Task(sbtConfig("c1"), nextIp, taskId1, Instant.now)).get + val taskId1 = TestTaskId(1) + val (assigned0, balancer0) = balancer.add(Task(sbtConfig("c1"), nextIp, taskId1, Instant.now)).get val server0 = balancer0.servers.head @@ -86,21 +85,20 @@ class LoadBalancerTest extends LoadBalancerTestUtils { assert(server0.mailbox.size == 1) assert(server0.currentTaskId.contains(taskId1)) - val taskId2 = TestTaskId(2) - val (assigned1, balancer1) = - balancer0.add(Task(sbtConfig("c2"), nextIp, taskId2, Instant.now)).get + val taskId2 = TestTaskId(2) + val (assigned1, balancer1) = balancer0.add(Task(sbtConfig("c2"), nextIp, taskId2, Instant.now)).get val server1 = balancer1.servers.head assert(server1.mailbox.size == 2) assert(server1.currentTaskId.contains(taskId1)) val balancer2 = balancer1.done(taskId1).get - val server2 = balancer2.servers.head + val server2 = balancer2.servers.head assert(server2.mailbox.size == 1) assert(server2.currentTaskId.contains(taskId2)) val balancer3 = balancer2.done(taskId2).get - val server3 = balancer3.servers.head + val server3 = balancer3.servers.head assert(server3.mailbox.isEmpty) assert(server3.currentTaskId.isEmpty) } @@ -109,7 +107,7 @@ class LoadBalancerTest extends LoadBalancerTestUtils { val ref = TestServerRef(1) val balancer = LoadBalancer( - Vector(Server(ref, sbtConfig("c1"), TestState("default-state"))), + Vector(Server(ref, sbtConfig("c1"), TestState("default-state"))) ) assert(balancer.removeServer(ref).servers.isEmpty) } @@ -117,7 +115,7 @@ class LoadBalancerTest extends LoadBalancerTestUtils { test("empty balancer") { val emptyBalancer = LoadBalancer( - servers = Vector(), + servers = Vector() ) val task = Task(code("c1"), nextIp, TestTaskId(1), Instant.now) diff --git a/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerTestUtils.scala b/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerTestUtils.scala index eb69872de..a4953d8a2 100644 --- a/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerTestUtils.scala +++ b/balancer/src/test/scala/com.olegych.scastie.balancer/LoadBalancerTestUtils.scala @@ -3,14 +3,15 @@ package com.olegych.scastie.balancer import java.time.Instant import com.olegych.scastie.api._ -import org.scalatest.Assertion import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.Assertion object TestTaskId { def apply(i: Int) = TaskId(SnippetId(i.toString, None)) } case class TestServerRef(id: Int) + case class TestState(state: String, ready: Boolean = true) extends ServerState { def isReady: Boolean = ready } @@ -21,6 +22,7 @@ trait LoadBalancerTestUtils extends AnyFunSuite with TestUtils { type TestLoadBalancer0 = LoadBalancer[TestServerRef, TestState] @transient private var taskId = 1000 + def add(balancer: TestLoadBalancer0, config: Inputs): TestLoadBalancer0 = synchronized { val (_, balancer0) = balancer.add(Task(config, nextIp, TestTaskId(taskId), Instant.now)).get taskId += 1 @@ -29,20 +31,22 @@ trait LoadBalancerTestUtils extends AnyFunSuite with TestUtils { // Ordering only for debug purposes object Multiset { - def apply[T: Ordering](xs: Seq[T]): Multiset[T] = - Multiset(xs.groupBy(x => x).map { case (k, vs) => (k, vs.size) }) + def apply[T: Ordering](xs: Seq[T]): Multiset[T] = Multiset(xs.groupBy(x => x).map { case (k, vs) => (k, vs.size) }) } + case class Multiset[T: Ordering](inner: Map[T, Int]) { + override def toString: String = { val size = inner.values.sum inner.toList .sortBy { case (k, v) => (-v, k) } - .map { - case (k, v) => s"$k($v)" + .map { case (k, v) => + s"$k($v)" } .mkString("Multiset(", ", ", s") {$size}") } + } def assertConfigs(balancer: TestLoadBalancer0)(columns: Seq[String]*): Assertion = { @@ -54,10 +58,11 @@ trait LoadBalancerTestUtils extends AnyFunSuite with TestUtils { } @transient private var serverId = 0 + def server( - c: String, - mailbox: Vector[Task] = Vector(), - state: TestState = TestState("default-state") + c: String, + mailbox: Vector[Task] = Vector(), + state: TestState = TestState("default-state") ): TestServer0 = synchronized { val t = Server(TestServerRef(serverId), sbtConfig(c), state, mailbox) serverId += 1 @@ -69,6 +74,7 @@ trait LoadBalancerTestUtils extends AnyFunSuite with TestUtils { } @transient private var currentIp = 0 + def nextIp: Ip = synchronized { val t = Ip("ip" + currentIp) currentIp += 1 @@ -77,13 +83,17 @@ trait LoadBalancerTestUtils extends AnyFunSuite with TestUtils { def server(v: Int): TestServerRef = TestServerRef(v) - def code(code: String) = Inputs.default.copy(code = code) + def code(code: String) = Inputs.default.copy(code = code) def sbtConfig(sbtConfig: String) = Inputs.default.copy(sbtConfigExtra = sbtConfig) def history(columns: Seq[String]*): TaskHistory = { - val records = - columns.to(Vector).flatten.map(i => Task(Inputs.default.copy(code = i.toString), nextIp, TestTaskId(1), Instant.now)).reverse + val records = columns + .to(Vector) + .flatten + .map(i => Task(Inputs.default.copy(code = i.toString), nextIp, TestTaskId(1), Instant.now)) + .reverse TaskHistory(Vector(records: _*), maxSize = 20) } + } diff --git a/balancer/src/test/scala/com.olegych.scastie.balancer/TestUtils.scala b/balancer/src/test/scala/com.olegych.scastie.balancer/TestUtils.scala index 7fc7bd3b9..68aba860a 100644 --- a/balancer/src/test/scala/com.olegych.scastie.balancer/TestUtils.scala +++ b/balancer/src/test/scala/com.olegych.scastie.balancer/TestUtils.scala @@ -2,7 +2,9 @@ package com.olegych.scastie package balancer trait TestUtils { + implicit class IntExtension(n: Int) { def *[T](v: T): Seq[T] = List.fill(n)(v) } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/AnsiColorFormatter.scala b/client/src/main/scala/com.olegych.scastie.client/AnsiColorFormatter.scala index 3fbbe22a3..e0f11d6f7 100644 --- a/client/src/main/scala/com.olegych.scastie.client/AnsiColorFormatter.scala +++ b/client/src/main/scala/com.olegych.scastie.client/AnsiColorFormatter.scala @@ -5,41 +5,41 @@ import scala.io.AnsiColor object AnsiColorFormatter extends AnsiColor { private val colors = Map( - BLACK -> "ansi-color-black", - RED -> "ansi-color-red", - GREEN -> "ansi-color-green", - YELLOW -> "ansi-color-yellow", - BLUE -> "ansi-color-blue", - MAGENTA -> "ansi-color-magenta", - CYAN -> "ansi-color-cyan", - WHITE -> "ansi-color-white", - BLACK_B -> "ansi-bg-color-black", - RED_B -> "ansi-bg-color-red", - GREEN_B -> "ansi-bg-color-green", - YELLOW_B -> "ansi-bg-color-yellow", - BLUE_B -> "ansi-bg-color-blue", + BLACK -> "ansi-color-black", + RED -> "ansi-color-red", + GREEN -> "ansi-color-green", + YELLOW -> "ansi-color-yellow", + BLUE -> "ansi-color-blue", + MAGENTA -> "ansi-color-magenta", + CYAN -> "ansi-color-cyan", + WHITE -> "ansi-color-white", + BLACK_B -> "ansi-bg-color-black", + RED_B -> "ansi-bg-color-red", + GREEN_B -> "ansi-bg-color-green", + YELLOW_B -> "ansi-bg-color-yellow", + BLUE_B -> "ansi-bg-color-blue", MAGENTA_B -> "ansi-bg-color-magenta", - CYAN_B -> "ansi-bg-color-cyan", - WHITE_B -> "ansi-bg-color-white", - RESET -> "", - BLINK -> "ansi-blink", - BOLD -> "ansi-bold", - REVERSED -> "ansi-reversed", + CYAN_B -> "ansi-bg-color-cyan", + WHITE_B -> "ansi-bg-color-white", + RESET -> "", + BLINK -> "ansi-blink", + BOLD -> "ansi-bold", + REVERSED -> "ansi-reversed", INVISIBLE -> "ansi-invisible" ) def formatToHtml(unformatted: String): String = { unformatted - .foldLeft("" -> 0) { - case ((_r, d), c) => - val r = _r + c - val replaced = colors.collectFirst { - case (ansiCode, replacement) if r.endsWith(ansiCode) => - if (ansiCode == RESET) r.replace(ansiCode, "" * d) -> 0 - else r.replace(ansiCode, s"""""") -> (d + 1) - } - replaced.getOrElse(r -> d) + .foldLeft("" -> 0) { case ((_r, d), c) => + val r = _r + c + val replaced = colors.collectFirst { + case (ansiCode, replacement) if r.endsWith(ansiCode) => + if (ansiCode == RESET) r.replace(ansiCode, "" * d) -> 0 + else r.replace(ansiCode, s"""""") -> (d + 1) + } + replaced.getOrElse(r -> d) } ._1 } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/ClientMain.scala b/client/src/main/scala/com.olegych.scastie.client/ClientMain.scala index 82bc734f2..189d76a86 100644 --- a/client/src/main/scala/com.olegych.scastie.client/ClientMain.scala +++ b/client/src/main/scala/com.olegych.scastie.client/ClientMain.scala @@ -1,17 +1,16 @@ package com.olegych.scastie.client import java.util.UUID +import scala.scalajs.js +import scala.scalajs.js.{|, UndefOr} +import scala.scalajs.js.annotation.{JSExport, _} import com.olegych.scastie.api.SnippetId import com.olegych.scastie.client.components._ import japgolly.scalajs.react.component.Generic import japgolly.scalajs.react.extra.router._ import org.scalajs.dom -import org.scalajs.dom.{HTMLElement, HTMLDivElement, HTMLLinkElement, Node} - -import scala.scalajs.js -import scala.scalajs.js.annotation.{JSExport, _} -import scala.scalajs.js.{UndefOr, |} +import org.scalajs.dom.{HTMLDivElement, HTMLElement, HTMLLinkElement, Node} @js.native @JSGlobal("ScastieSettings") @@ -25,20 +24,24 @@ object Exports { val ScastieMain = com.olegych.scastie.client.ScastieMain @JSExport val ClientMain = com.olegych.scastie.client.ScastieClientMain + @JSExport - def Embedded(selector: UndefOr[String | Node], options: UndefOr[EmbeddedOptionsJs]) = ScastieEmbedded.embedded(selector, options) + def Embedded(selector: UndefOr[String | Node], options: UndefOr[EmbeddedOptionsJs]) = + ScastieEmbedded.embedded(selector, options) + @JSExport def EmbeddedResource(options: UndefOr[EmbeddedResourceOptionsJs]) = ScastieEmbedded.embeddedResource(options) } + /* Entry point for the website */ object ScastieMain { + @JSExport def main(): Unit = { dom.document.body.className = "scastie" - val container = - dom.document.createElement("div").asInstanceOf[HTMLDivElement] + val container = dom.document.createElement("div").asInstanceOf[HTMLDivElement] container.className = "scastie" dom.document.body.appendChild(container) @@ -54,11 +57,13 @@ object ScastieMain { () } + } /* Entry point for Scala.js runtime */ object ScastieClientMain { + @JSExport def signal(instrumentations: String, attachedDoms: js.Array[HTMLElement], rawId: String): Unit = { Global.signal(instrumentations, attachedDoms, rawId) @@ -68,30 +73,28 @@ object ScastieClientMain { def error(er: js.Error, rawId: String): Unit = { Global.error(er, rawId) } + } /* Entry point for ressource embedding and code embedding */ object ScastieEmbedded { + def embedded(selector: UndefOr[String | Node], options: UndefOr[EmbeddedOptionsJs]): Unit = { - val embeddedOptions = - options.toOption - .map(EmbeddedOptions.fromJs(Settings.defaultServerUrl)) - .getOrElse(EmbeddedOptions.empty(Settings.defaultServerUrl)) - - val nodes = - selector.toOption match { - case Some(sel) => { - (sel: Any) match { - case cssSelector: String => - dom.document.querySelectorAll(cssSelector).toList - case node: Node => - List(node) - } + val embeddedOptions = options.toOption + .map(EmbeddedOptions.fromJs(Settings.defaultServerUrl)) + .getOrElse(EmbeddedOptions.empty(Settings.defaultServerUrl)) + + val nodes = selector.toOption match { + case Some(sel) => { + (sel: Any) match { + case cssSelector: String => dom.document.querySelectorAll(cssSelector).toList + case node: Node => List(node) } - case None => List() } + case None => List() + } if (nodes.nonEmpty) { addStylesheet(embeddedOptions.serverUrl) @@ -118,16 +121,14 @@ object ScastieEmbedded { } def embeddedResource(options: UndefOr[EmbeddedResourceOptionsJs]): Unit = { - val embeddedOptions = - options.toOption - .map(EmbeddedOptions.fromJsRessource(Settings.defaultServerUrl)) - .getOrElse(EmbeddedOptions.empty(Settings.defaultServerUrl)) - - val container = - renderScastie( - embeddedOptions = embeddedOptions, - snippetId = embeddedOptions.snippetId - ) + val embeddedOptions = options.toOption + .map(EmbeddedOptions.fromJsRessource(Settings.defaultServerUrl)) + .getOrElse(EmbeddedOptions.empty(Settings.defaultServerUrl)) + + val container = renderScastie( + embeddedOptions = embeddedOptions, + snippetId = embeddedOptions.snippetId + ) embeddedOptions.injectId match { case Some(id) => { @@ -146,6 +147,7 @@ object ScastieEmbedded { } } } + def addStylesheet(baseUrl: String): Unit = { val link = dom.document .createElement("link") @@ -175,9 +177,10 @@ object ScastieEmbedded { targetType = None, tryLibrary = None, code = None, - inputs = None, + inputs = None ).render.renderIntoDOM(container) container } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/ConsoleState.scala b/client/src/main/scala/com.olegych.scastie.client/ConsoleState.scala index 031f6bea6..1a07df79d 100644 --- a/client/src/main/scala/com.olegych.scastie.client/ConsoleState.scala +++ b/client/src/main/scala/com.olegych.scastie.client/ConsoleState.scala @@ -3,18 +3,18 @@ package com.olegych.scastie.client import play.api.libs.json._ object ConsoleState { - implicit val formatConsoleState: OFormat[ConsoleState] = - Json.format[ConsoleState] + implicit val formatConsoleState: OFormat[ConsoleState] = Json.format[ConsoleState] def default: ConsoleState = ConsoleState( consoleIsOpen = false, consoleHasUserOutput = false, userOpenedConsole = false ) + } case class ConsoleState( - consoleIsOpen: Boolean, - consoleHasUserOutput: Boolean, - userOpenedConsole: Boolean + consoleIsOpen: Boolean, + consoleHasUserOutput: Boolean, + userOpenedConsole: Boolean ) diff --git a/client/src/main/scala/com.olegych.scastie.client/EmbeddedOptions.scala b/client/src/main/scala/com.olegych.scastie.client/EmbeddedOptions.scala index e9eba5770..2ca33fc8c 100644 --- a/client/src/main/scala/com.olegych.scastie.client/EmbeddedOptions.scala +++ b/client/src/main/scala/com.olegych.scastie.client/EmbeddedOptions.scala @@ -1,10 +1,10 @@ package com.olegych.scastie.client -import com.olegych.scastie.api.{Inputs, SnippetId, SnippetUserPart, ScalaTarget, ScalaTargetType} - import scala.scalajs.js import scala.scalajs.js.UndefOr +import com.olegych.scastie.api.{Inputs, ScalaTarget, ScalaTargetType, SnippetId, SnippetUserPart} + trait SharedEmbeddedOptions extends js.Object { val serverUrl: UndefOr[String] val theme: UndefOr[String] @@ -33,19 +33,23 @@ trait EmbeddedOptionsJs extends js.Object with SharedEmbeddedOptions { // val scalaNativeVersion: UndefOr[String] not yet supported } -case class EmbeddedOptions(snippetId: Option[SnippetId], - injectId: Option[String], - inputs: Option[Inputs], - theme: Option[String], - serverUrl: String) { +case class EmbeddedOptions( + snippetId: Option[SnippetId], + injectId: Option[String], + inputs: Option[Inputs], + theme: Option[String], + serverUrl: String +) { def setCode(code: String): EmbeddedOptions = { val inputs0 = inputs.getOrElse(Inputs.default) copy(inputs = Some(inputs0.copy(code = code))) } + } object EmbeddedOptions { + def empty(defaultServerUrl: String): EmbeddedOptions = { EmbeddedOptions( snippetId = None, @@ -57,22 +61,21 @@ object EmbeddedOptions { } private def extractSnippetId( - options: SharedEmbeddedOptions + options: SharedEmbeddedOptions ): Option[SnippetId] = { import options._ - base64UUID.toOption.map( - uuid => - SnippetId( - uuid, - user.toOption - .map(u => SnippetUserPart(u, update.toOption.getOrElse(0))) + base64UUID.toOption.map(uuid => + SnippetId( + uuid, + user.toOption + .map(u => SnippetUserPart(u, update.toOption.getOrElse(0))) ) ) } def fromJsRessource( - defaultServerUrl: String + defaultServerUrl: String )(options: EmbeddedResourceOptionsJs): EmbeddedOptions = { import options._ @@ -95,87 +98,85 @@ object EmbeddedOptions { } def fromJs( - defaultServerUrl: String + defaultServerUrl: String )(options: EmbeddedOptionsJs): EmbeddedOptions = { import options._ - val scalaTarget = - (targetType.toOption, - scalaVersion.toOption, - None: Option[String], // scalaJsVersion.toOption, - None: Option[String] // scalaNativeVersion.toOption - ) match { - - case (Some("jvm"), _, None, None) => { - Some( - scalaVersion - .map(version => ScalaTarget.Jvm(version)) - .getOrElse(ScalaTarget.Jvm.default) - ) - } - - case (Some("dotty" | "scala3"), _, None, None) => { - Some( - scalaVersion - .map(version => ScalaTarget.Scala3(version)) - .getOrElse(ScalaTarget.Scala3.default) - ) - } - - case (Some("typelevel"), _, None, None) => { - Some( - scalaVersion - .map(version => ScalaTarget.Typelevel(version)) - .getOrElse(ScalaTarget.Typelevel.default) - ) - } - - case (Some("js"), None, None, None) => { - Some(ScalaTarget.Js.default) - } - - case (tpe, Some(scalaV), Some(jsV), None) if (tpe.contains("js") || tpe.isEmpty) => { - - Some(ScalaTarget.Js(scalaV, jsV)) - } - - case (Some("native"), None, None, None) => { - Some(ScalaTarget.Native.default) - } - - case (tpe, Some(scalaV), None, Some(nativeV)) if (tpe.contains("native") || tpe.isEmpty) => { - Some(ScalaTarget.Native(scalaV, nativeV)) - } - - case (None, None, None, None) => None - - case (a, b, c, d) => { - sys.error( - s"invalid scala target combination: $a | $b | $c | $d" - ) - } + val scalaTarget = ( + targetType.toOption, + scalaVersion.toOption, + None: Option[String], // scalaJsVersion.toOption, + None: Option[String] // scalaNativeVersion.toOption + ) match { + + case (Some("jvm"), _, None, None) => { + Some( + scalaVersion + .map(version => ScalaTarget.Jvm(version)) + .getOrElse(ScalaTarget.Jvm.default) + ) + } + + case (Some("dotty" | "scala3"), _, None, None) => { + Some( + scalaVersion + .map(version => ScalaTarget.Scala3(version)) + .getOrElse(ScalaTarget.Scala3.default) + ) + } + + case (Some("typelevel"), _, None, None) => { + Some( + scalaVersion + .map(version => ScalaTarget.Typelevel(version)) + .getOrElse(ScalaTarget.Typelevel.default) + ) + } + + case (Some("js"), None, None, None) => { + Some(ScalaTarget.Js.default) + } + + case (tpe, Some(scalaV), Some(jsV), None) if (tpe.contains("js") || tpe.isEmpty) => { + + Some(ScalaTarget.Js(scalaV, jsV)) + } + + case (Some("native"), None, None, None) => { + Some(ScalaTarget.Native.default) + } + + case (tpe, Some(scalaV), None, Some(nativeV)) if (tpe.contains("native") || tpe.isEmpty) => { + Some(ScalaTarget.Native(scalaV, nativeV)) } + case (None, None, None, None) => None + + case (a, b, c, d) => { + sys.error( + s"invalid scala target combination: $a | $b | $c | $d" + ) + } + } + val inputs = if (scalaTarget.isDefined || code.isDefined) { val default = Inputs.default - val isScala3 = - scalaTarget - .map(_.targetType == ScalaTargetType.Scala3) - .getOrElse(false) + val isScala3 = scalaTarget + .map(_.targetType == ScalaTargetType.Scala3) + .getOrElse(false) val defaultCode = if (isScala3) ScalaTarget.Scala3.defaultCode else default.code - val inputs0 = - default.copy( - _isWorksheetMode = isWorksheetMode.getOrElse(default.isWorksheetMode), - code = code.getOrElse(defaultCode), - target = scalaTarget.getOrElse(default.target), - sbtConfigExtra = sbtConfig.getOrElse(default.sbtConfigExtra) - ) + val inputs0 = default.copy( + _isWorksheetMode = isWorksheetMode.getOrElse(default.isWorksheetMode), + code = code.getOrElse(defaultCode), + target = scalaTarget.getOrElse(default.target), + sbtConfigExtra = sbtConfig.getOrElse(default.sbtConfigExtra) + ) Some(inputs0) } else { None @@ -197,4 +198,5 @@ object EmbeddedOptions { serverUrl = serverUrl.toOption.getOrElse(defaultServerUrl) ) } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/EventStream.scala b/client/src/main/scala/com.olegych.scastie.client/EventStream.scala index 5cdda64fb..e5e45d0e5 100644 --- a/client/src/main/scala/com.olegych.scastie.client/EventStream.scala +++ b/client/src/main/scala/com.olegych.scastie.client/EventStream.scala @@ -1,19 +1,19 @@ package com.olegych.scastie.client +import scala.util.Failure +import scala.util.Success + import japgolly.scalajs.react.Callback import japgolly.scalajs.react.CallbackTo +import org.scalajs.dom.window import org.scalajs.dom.CloseEvent import org.scalajs.dom.Event import org.scalajs.dom.EventSource import org.scalajs.dom.MessageEvent import org.scalajs.dom.WebSocket -import org.scalajs.dom.window import play.api.libs.json.Json import play.api.libs.json.Reads -import scala.util.Failure -import scala.util.Success - abstract class EventStream[T: Reads](handler: EventStreamHandler[T]) { var closing = false @@ -27,8 +27,9 @@ abstract class EventStream[T: Reads](handler: EventStreamHandler[T]) { } } } - def onOpen(): Unit = handler.onOpen() - def onError(error: String): Unit = handler.onError(error) + + def onOpen(): Unit = handler.onOpen() + def onError(error: String): Unit = handler.onError(error) def onClose(reason: Option[String]): Unit = handler.onClose(reason) def close(force: Boolean = false): Unit = { @@ -37,6 +38,7 @@ abstract class EventStream[T: Reads](handler: EventStreamHandler[T]) { onClose(None) } } + } trait EventStreamHandler[T] { @@ -50,17 +52,16 @@ trait EventStreamHandler[T] { } object EventStream { + def connect[T: Reads](eventSourceUri: String, websocketUri: String, handler: EventStreamHandler[T]): Callback = { - def connectEventSource = - CallbackTo[EventStream[T]]( - new EventSourceStream(eventSourceUri, handler) - ) + def connectEventSource = CallbackTo[EventStream[T]]( + new EventSourceStream(eventSourceUri, handler) + ) - def connectWebSocket = - CallbackTo[EventStream[T]]( - new WebSocketStream(websocketUri, handler) - ) + def connectWebSocket = CallbackTo[EventStream[T]]( + new WebSocketStream(websocketUri, handler) + ) connectEventSource.attemptTry.flatMap { case Success(eventSource) => { @@ -79,6 +80,7 @@ object EventStream { } } } + } class WebSocketStream[T: Reads](uri: String, handler: EventStreamHandler[T]) extends EventStream[T](handler) { @@ -100,9 +102,8 @@ class WebSocketStream[T: Reads](uri: String, handler: EventStreamHandler[T]) ext socket.close() } - val protocol: String = - if (window.location.protocol == "https:") "wss" else "ws" - val fullUri: String = s"$protocol://${window.location.host}${uri}" + val protocol: String = if (window.location.protocol == "https:") "wss" else "ws" + val fullUri: String = s"$protocol://${window.location.host}${uri}" val socket: WebSocket = new WebSocket(uri) socket.onopen = onOpen _ diff --git a/client/src/main/scala/com.olegych.scastie.client/Global.scala b/client/src/main/scala/com.olegych.scastie.client/Global.scala index 1fb150b40..6ffc96a44 100644 --- a/client/src/main/scala/com.olegych.scastie.client/Global.scala +++ b/client/src/main/scala/com.olegych.scastie.client/Global.scala @@ -1,20 +1,16 @@ package com.olegych.scastie.client -import com.olegych.scastie.api._ -import com.olegych.scastie.client.components.Scastie - -import scala.scalajs.js +import java.util.UUID import scala.collection.mutable.{Map => MMap} -import scala.util.{Try, Failure, Success} - -import org.scalajs.dom.HTMLElement +import scala.scalajs.js +import scala.util.{Failure, Success, Try} +import com.olegych.scastie.api._ +import com.olegych.scastie.client.components.Scastie import japgolly.scalajs.react._ - +import org.scalajs.dom.HTMLElement import play.api.libs.json.Json -import java.util.UUID - object Global { type Scope = BackendScope[Scastie, ScastieState] @@ -30,33 +26,31 @@ object Global { def error(er: js.Error, rawId: String): Unit = { withScope(rawId)( - _.withEffectsImpure.modState( - state => - state - .copyAndSave( - outputs = state.outputs.copy( - runtimeError = Some( - RuntimeError( - message = er.toString, - line = None, - fullStack = "" - ) + _.withEffectsImpure.modState(state => + state + .copyAndSave( + outputs = state.outputs.copy( + runtimeError = Some( + RuntimeError( + message = er.toString, + line = None, + fullStack = "" ) ) ) - .setRunning(false) + ) + .setRunning(false) ) ) } def signal(instrumentationsRaw: String, attachedDoms: js.Array[HTMLElement], rawId: String): Unit = { - val result = - Json - .fromJson[ScalaJsResult]( - Json.parse(instrumentationsRaw) - ) - .asOpt + val result = Json + .fromJson[ScalaJsResult]( + Json.parse(instrumentationsRaw) + ) + .asOpt val (instr, runtimeError) = result.map(_.in) match { case Some(Left(maybeRuntimeError)) => (Nil, maybeRuntimeError) @@ -65,18 +59,17 @@ object Global { } withScope(rawId)( - _.withEffectsImpure.modState( - state => - state - .copyAndSave( - outputs = state.outputs.copy( - instrumentations = state.outputs.instrumentations ++ instr.toSet, - runtimeError = runtimeError - ) + _.withEffectsImpure.modState(state => + state + .copyAndSave( + outputs = state.outputs.copy( + instrumentations = state.outputs.instrumentations ++ instr.toSet, + runtimeError = runtimeError ) - .setRunning(false) - .copy( - attachedDoms = attachedDoms.map(dom => (dom.getAttribute("uuid"), dom)).toMap + ) + .setRunning(false) + .copy( + attachedDoms = attachedDoms.map(dom => (dom.getAttribute("uuid"), dom)).toMap ) ) ) @@ -92,4 +85,5 @@ object Global { case Failure(e) => e.printStackTrace() } } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/HTMLFormatter.scala b/client/src/main/scala/com.olegych.scastie.client/HTMLFormatter.scala index 106e16f3f..f230aee5e 100644 --- a/client/src/main/scala/com.olegych.scastie.client/HTMLFormatter.scala +++ b/client/src/main/scala/com.olegych.scastie.client/HTMLFormatter.scala @@ -1,22 +1,21 @@ package com.olegych.scastie.client object HTMLFormatter { - private val escapeMap = - Map('&' -> "&", '"' -> """, '<' -> "<", '>' -> ">") + private val escapeMap = Map('&' -> "&", '"' -> """, '<' -> "<", '>' -> ">") - private def escape(text: String): String = - text.iterator - .foldLeft(new StringBuilder()) { (s, c) => - escapeMap.get(c) match { - case Some(str) => s ++= str - case _ if c >= ' ' || "\n\r\t\u001b".contains(c) => s += c - case _ => s // noop - } + private def escape(text: String): String = text.iterator + .foldLeft(new StringBuilder()) { (s, c) => + escapeMap.get(c) match { + case Some(str) => s ++= str + case _ if c >= ' ' || "\n\r\t\u001b".contains(c) => s += c + case _ => s // noop } - .toString + } + .toString def format(notEscapedAndUnformatted: String) = { val escaped = escape(notEscapedAndUnformatted) AnsiColorFormatter.formatToHtml(escaped) } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/LocalStorage.scala b/client/src/main/scala/com.olegych.scastie.client/LocalStorage.scala index 9d3b64f78..3866e566a 100644 --- a/client/src/main/scala/com.olegych.scastie.client/LocalStorage.scala +++ b/client/src/main/scala/com.olegych.scastie.client/LocalStorage.scala @@ -1,10 +1,9 @@ package com.olegych.scastie package client -import play.api.libs.json.Json - import org.scalajs.dom import org.scalajs.dom.window.localStorage +import play.api.libs.json.Json object LocalStorage { private val stateKey = "state" @@ -23,4 +22,5 @@ object LocalStorage { None } } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/ModalState.scala b/client/src/main/scala/com.olegych.scastie.client/ModalState.scala index 461e3601e..5c428cc7a 100644 --- a/client/src/main/scala/com.olegych.scastie.client/ModalState.scala +++ b/client/src/main/scala/com.olegych.scastie.client/ModalState.scala @@ -1,10 +1,8 @@ package com.olegych.scastie.client import com.olegych.scastie.api.SnippetId - -import play.api.libs.json._ - import japgolly.scalajs.react._ +import play.api.libs.json._ object ModalState { implicit val formatModalState: OFormat[ModalState] = Json.format[ModalState] @@ -30,22 +28,21 @@ object ModalState { isEmbeddedClosed = true, isLoginModalClosed = true ) + } case class ModalState( - isHelpModalClosed: Boolean, - isPrivacyPolicyModalClosed: Boolean, - @deprecated("Scheduled for removal", "2023-04-30") - isPrivacyPolicyPromptClosed: Boolean, - shareModalSnippetId: Option[SnippetId], - isResetModalClosed: Boolean, - isNewSnippetModalClosed: Boolean, - isEmbeddedClosed: Boolean, - isLoginModalClosed: Boolean + isHelpModalClosed: Boolean, + isPrivacyPolicyModalClosed: Boolean, + @deprecated("Scheduled for removal", "2023-04-30") + isPrivacyPolicyPromptClosed: Boolean, + shareModalSnippetId: Option[SnippetId], + isResetModalClosed: Boolean, + isNewSnippetModalClosed: Boolean, + isEmbeddedClosed: Boolean, + isLoginModalClosed: Boolean ) { val isShareModalClosed: SnippetId ~=> Boolean = - Reusable.fn( - shareModalSnippetId2 => !shareModalSnippetId.contains(shareModalSnippetId2) - ) + Reusable.fn(shareModalSnippetId2 => !shareModalSnippetId.contains(shareModalSnippetId2)) } diff --git a/client/src/main/scala/com.olegych.scastie.client/RestApiClient.scala b/client/src/main/scala/com.olegych.scastie.client/RestApiClient.scala index 8f5235705..6557e2a4f 100644 --- a/client/src/main/scala/com.olegych.scastie.client/RestApiClient.scala +++ b/client/src/main/scala/com.olegych.scastie.client/RestApiClient.scala @@ -1,41 +1,40 @@ package com.olegych.scastie.client +import scala.concurrent.Future +import scala.util.Try + import com.olegych.scastie.api._ import org.scalajs.dom import org.scalajs.dom.XMLHttpRequest import play.api.libs.json._ - -import scala.concurrent.Future -import scala.util.Try - import scalajs.concurrent.JSExecutionContext.Implicits.queue -import scalajs.js.Thenable.Implicits._ import scalajs.js +import scalajs.js.Thenable.Implicits._ class RestApiClient(serverUrl: Option[String]) extends RestApi { val apiBase: String = serverUrl.getOrElse("") - def tryParse[T: Reads](response: XMLHttpRequest): Option[T] = - tryParse(response.responseText) + def tryParse[T: Reads](response: XMLHttpRequest): Option[T] = tryParse(response.responseText) - def tryParse[T: Reads](response: dom.Response): Future[Option[T]] = - response.text().map(tryParse(_)) + def tryParse[T: Reads](response: dom.Response): Future[Option[T]] = response.text().map(tryParse(_)) def tryParse[T: Reads](text: String): Option[T] = { - Option.when(text.nonEmpty)(text).flatMap(t => - Try(Json.parse(t)).toOption.flatMap(Json.fromJson[T](_).asOpt) - ) + Option.when(text.nonEmpty)(text).flatMap(t => Try(Json.parse(t)).toOption.flatMap(Json.fromJson[T](_).asOpt)) } def get[T: Reads](url: String): Future[Option[T]] = { val header = new dom.Headers(js.Dictionary("Accept" -> "application/json")) dom - .fetch(apiBase + "/api" + url, js.Dynamic.literal(headers = header, method = dom.HttpMethod.GET).asInstanceOf[dom.RequestInit]) + .fetch( + apiBase + "/api" + url, + js.Dynamic.literal(headers = header, method = dom.HttpMethod.GET).asInstanceOf[dom.RequestInit] + ) .flatMap(tryParse[T](_)) } class Post[O: Reads]() { + def using[I: Writes](url: String, data: I, async: Boolean = true): Future[Option[O]] = { val header = new dom.Headers(js.Dictionary("Accept" -> "application/json", "Content-Type" -> "application/json")) dom @@ -47,18 +46,16 @@ class RestApiClient(serverUrl: Option[String]) extends RestApi { ) .flatMap(tryParse[O](_)) } + } def post[O: Reads]: Post[O] = new Post[O] - def run(inputs: Inputs): Future[SnippetId] = - post[SnippetId].using("/run", inputs).map(_.get) + def run(inputs: Inputs): Future[SnippetId] = post[SnippetId].using("/run", inputs).map(_.get) - def format(request: FormatRequest): Future[FormatResponse] = - post[FormatResponse].using("/format", request).map(_.get) + def format(request: FormatRequest): Future[FormatResponse] = post[FormatResponse].using("/format", request).map(_.get) - def save(inputs: Inputs): Future[SnippetId] = - post[SnippetId].using("/save", inputs).map(_.get) + def save(inputs: Inputs): Future[SnippetId] = post[SnippetId].using("/save", inputs).map(_.get) def saveBlocking(inputs: Inputs): Option[SnippetId] = { val req = new dom.XMLHttpRequest() @@ -83,23 +80,17 @@ class RestApiClient(serverUrl: Option[String]) extends RestApi { snippetId } - def update(editInputs: EditInputs): Future[Option[SnippetId]] = - post[SnippetId].using("/update", editInputs) + def update(editInputs: EditInputs): Future[Option[SnippetId]] = post[SnippetId].using("/update", editInputs) - def fork(editInputs: EditInputs): Future[Option[SnippetId]] = - post[SnippetId].using("/fork", editInputs) + def fork(editInputs: EditInputs): Future[Option[SnippetId]] = post[SnippetId].using("/fork", editInputs) - def delete(snippetId: SnippetId): Future[Boolean] = - post[Boolean].using("/delete", snippetId).map(_.getOrElse(false)) + def delete(snippetId: SnippetId): Future[Boolean] = post[Boolean].using("/delete", snippetId).map(_.getOrElse(false)) - def fetch(snippetId: SnippetId): Future[Option[FetchResult]] = - get[FetchResult]("/snippets/" + snippetId.url) + def fetch(snippetId: SnippetId): Future[Option[FetchResult]] = get[FetchResult]("/snippets/" + snippetId.url) - def fetchOld(id: Int): Future[Option[FetchResult]] = - get[FetchResult](s"/old-snippets/$id") + def fetchOld(id: Int): Future[Option[FetchResult]] = get[FetchResult](s"/old-snippets/$id") - def fetchUser(): Future[Option[User]] = - get[User]("/user/settings") + def fetchUser(): Future[Option[User]] = get[User]("/user/settings") @deprecated("Scheduled for removal", "2023-04-30") def getPrivacyPolicyStatus(): Future[Boolean] = @@ -107,15 +98,15 @@ class RestApiClient(serverUrl: Option[String]) extends RestApi { @deprecated("Scheduled for removal", "2023-04-30") def acceptPrivacyPolicy(): Future[Boolean] = - post[Boolean].using("/user/acceptPrivacyPolicy", "", async=false).map(_.getOrElse(false)) + post[Boolean].using("/user/acceptPrivacyPolicy", "", async = false).map(_.getOrElse(false)) @deprecated("Scheduled for removal", "2023-04-30") def removeAllUserSnippets(): Future[Boolean] = - post[Boolean].using("/user/removeAllUserSnippets", "", async=false).map(_.getOrElse(false)) + post[Boolean].using("/user/removeAllUserSnippets", "", async = false).map(_.getOrElse(false)) @deprecated("Scheduled for removal", "2023-04-30") def removeUserFromPolicyStatus(): Future[Boolean] = - post[Boolean].using("/user/removeUserFromPolicyStatus", "", async=false).map(_.getOrElse(false)) + post[Boolean].using("/user/removeUserFromPolicyStatus", "", async = false).map(_.getOrElse(false)) def fetchUserSnippets(): Future[List[SnippetSummary]] = get[List[SnippetSummary]]("/user/snippets").map(_.getOrElse(Nil)) diff --git a/client/src/main/scala/com.olegych.scastie.client/Routing.scala b/client/src/main/scala/com.olegych.scastie.client/Routing.scala index 0c31a099a..bb90dce35 100644 --- a/client/src/main/scala/com.olegych.scastie.client/Routing.scala +++ b/client/src/main/scala/com.olegych.scastie.client/Routing.scala @@ -1,28 +1,28 @@ package com.olegych.scastie.client +import java.util.UUID + import com.olegych.scastie.api.{Inputs, Project, ScalaDependency, ScalaTarget, ScalaTargetType, ScalaVersions, SnippetId, SnippetUserPart} import com.olegych.scastie.client.components._ -import japgolly.scalajs.react._ -import vdom.all._ import extra.router._ +import japgolly.scalajs.react._ import play.api.libs.json.Json - -import java.util.UUID +import vdom.all._ class Routing(defaultServerUrl: String) { + val config: RouterConfig[Page] = RouterConfigDsl[Page].buildConfig { dsl => import dsl._ - val embedded = "embedded" - val alpha = string("[a-zA-Z0-9-]+") + val embedded = "embedded" + val alpha = string("[a-zA-Z0-9-]+") val snippetId = string("[a-zA-Z0-9-]{22}") val targetType = queryToMap.pmap { map => ( map.get("target"), - map.get("c"), + map.get("c") ) match { - case (Some(target), c) => - ScalaTargetType.parse(target.toUpperCase).map(target => TargetTypePage(target, c)) - case _ => None + case (Some(target), c) => ScalaTargetType.parse(target.toUpperCase).map(target => TargetTypePage(target, c)) + case _ => None } }(p => Map("target" -> p.targetType.toString) ++ p.code.map("c" -> _)) @@ -30,11 +30,14 @@ class Routing(defaultServerUrl: String) { map.get("inputs").flatMap { inputs => Json .fromJson[Inputs](Json.parse(inputs)) - .fold({ e => - println(s"failed to parse ${inputs}") - println(e) - None - }, inputs => Some(InputsPage(inputs))) + .fold( + { e => + println(s"failed to parse ${inputs}") + println(e) + None + }, + inputs => Some(InputsPage(inputs)) + ) } }(p => Map("inputs" -> Json.toJson(p.inputs).toString().replace("{", "%7B").replace("}", "%7D"))) @@ -45,14 +48,12 @@ class Routing(defaultServerUrl: String) { map.get("v"), map.get("o"), map.get("r"), - map.get("c"), + map.get("c") ) match { case (Some(g), Some(a), Some(v), o, r, c) => val target = map.get("t").flatMap(ScalaTargetType.parse) match { - case Some(t @ ScalaTargetType.Scala2) => - map.get("sv").map(sv => ScalaTarget.Jvm(ScalaVersions.find(t, sv))) - case Some(t @ ScalaTargetType.JS) => - (map.get("sv"), map.get("sjsv")) match { + case Some(t @ ScalaTargetType.Scala2) => map.get("sv").map(sv => ScalaTarget.Jvm(ScalaVersions.find(t, sv))) + case Some(t @ ScalaTargetType.JS) => (map.get("sv"), map.get("sjsv")) match { case (Some(sv), sjsv) => Some(ScalaTarget.Js(ScalaVersions.find(t, sv), sjsv.getOrElse(ScalaTarget.Js.default.scalaJsVersion))) case _ => None @@ -79,45 +80,45 @@ class Routing(defaultServerUrl: String) { "a" -> dep.dependency.artifact, "v" -> dep.dependency.version, "r" -> dep.project.repository, - "o" -> dep.project.organization, + "o" -> dep.project.organization ) } val tryLibrary = queryToMap.pmap(parseTryLibrary)(renderTryLibrary) - val anon = snippetId - val user = alpha / snippetId + val anon = snippetId + val user = alpha / snippetId val userUpdate = alpha / snippetId / int - val oldId = int + val oldId = int ( trimSlashes | staticRoute(root, Home) ~> - renderR(renderScastieDefault) + renderR(renderScastieDefault) | dynamicRouteCT("try" ~ tryLibrary) ~> - dynRenderR((page, router) => renderTryLibraryPage(page, router)) + dynRenderR((page, router) => renderTryLibraryPage(page, router)) | dynamicRouteCT(inputs) ~> - dynRenderR((page, router) => renderInputs(page, router)) + dynRenderR((page, router) => renderInputs(page, router)) | dynamicRouteCT(targetType) ~> - dynRenderR((page, router) => renderTargetTypePage(page, router)) + dynRenderR((page, router) => renderTargetTypePage(page, router)) | dynamicRouteCT(oldId.caseClass[OldSnippetIdPage]) ~> - dynRenderR((page, router) => renderOldSnippetIdPage(page, router)) + dynRenderR((page, router) => renderOldSnippetIdPage(page, router)) | dynamicRouteCT(anon.caseClass[AnonymousResource]) ~> - dynRenderR((page, router) => renderPage(page, router)) + dynRenderR((page, router) => renderPage(page, router)) | dynamicRouteCT(user.caseClass[UserResource]) ~> - dynRenderR((page, router) => renderPage(page, router)) + dynRenderR((page, router) => renderPage(page, router)) | dynamicRouteCT(userUpdate.caseClass[UserResourceUpdated]) ~> - dynRenderR((page, router) => renderPage(page, router)) + dynRenderR((page, router) => renderPage(page, router)) | staticRoute(embedded, Embedded) ~> - renderR(renderScastieDefaultEmbedded) + renderR(renderScastieDefaultEmbedded) | dynamicRouteCT(embedded / anon.caseClass[EmbeddedAnonymousResource]) ~> - dynRenderR((page, router) => renderPage(page, router)) + dynRenderR((page, router) => renderPage(page, router)) | dynamicRouteCT(embedded / user.caseClass[EmbeddedUserResource]) ~> - dynRenderR((page, router) => renderPage(page, router)) + dynRenderR((page, router) => renderPage(page, router)) | dynamicRouteCT( embedded / userUpdate.caseClass[EmbeddedUserResourceUpdated] ) ~> - dynRenderR((page, router) => renderPage(page, router)) + dynRenderR((page, router) => renderPage(page, router)) ).notFound(redirectToPage(Home)(SetRouteVia.HistoryReplace)) .renderWith((page, router) => layout(page, router)) } @@ -144,12 +145,11 @@ class Routing(defaultServerUrl: String) { } private def renderScastieDefaultEmbedded( - router: RouterCtl[Page] - ): VdomElement = - Scastie - .default(router) - .copy(embedded = Some(EmbeddedOptions.empty(defaultServerUrl))) - .render + router: RouterCtl[Page] + ): VdomElement = Scastie + .default(router) + .copy(embedded = Some(EmbeddedOptions.empty(defaultServerUrl))) + .render private def renderPage(page: ResourcePage, router: RouterCtl[Page]): VdomElement = { val defaultEmbedded = Some(EmbeddedOptions.empty(defaultServerUrl)) @@ -184,7 +184,7 @@ class Routing(defaultServerUrl: String) { targetType = None, tryLibrary = None, code = None, - inputs = None, + inputs = None ).render } diff --git a/client/src/main/scala/com.olegych.scastie.client/RoutingADT.scala b/client/src/main/scala/com.olegych.scastie.client/RoutingADT.scala index 0442b1da3..8ccaaf3e5 100644 --- a/client/src/main/scala/com.olegych.scastie.client/RoutingADT.scala +++ b/client/src/main/scala/com.olegych.scastie.client/RoutingADT.scala @@ -3,57 +3,56 @@ package com.olegych.scastie.client import com.olegych.scastie.api._ object Page { + def fromSnippetId(snippetId: SnippetId): ResourcePage = { snippetId match { - case SnippetId(uuid, None) => - AnonymousResource(uuid) + case SnippetId(uuid, None) => AnonymousResource(uuid) - case SnippetId(uuid, Some(SnippetUserPart(login, 0))) => - UserResource(login, uuid) + case SnippetId(uuid, Some(SnippetUserPart(login, 0))) => UserResource(login, uuid) - case SnippetId(uuid, Some(SnippetUserPart(login, update))) => - UserResourceUpdated(login, uuid, update) + case SnippetId(uuid, Some(SnippetUserPart(login, update))) => UserResourceUpdated(login, uuid, update) } } + } sealed trait Page -case object Home extends Page +case object Home extends Page case object Embedded extends Page -case class TargetTypePage(targetType: ScalaTargetType, code: Option[String]) extends Page +case class TargetTypePage(targetType: ScalaTargetType, code: Option[String]) extends Page case class TryLibraryPage(dependency: ScalaDependency, project: Project, code: Option[String]) extends Page -case class OldSnippetIdPage(id: Int) extends Page -case class InputsPage(inputs: Inputs) extends Page +case class OldSnippetIdPage(id: Int) extends Page +case class InputsPage(inputs: Inputs) extends Page sealed trait ResourcePage extends Page case class AnonymousResource( - uuid: String + uuid: String ) extends ResourcePage case class UserResource( - login: String, - uuid: String + login: String, + uuid: String ) extends ResourcePage case class UserResourceUpdated( - login: String, - uuid: String, - update: Int + login: String, + uuid: String, + update: Int ) extends ResourcePage case class EmbeddedAnonymousResource( - uuid: String + uuid: String ) extends ResourcePage case class EmbeddedUserResource( - login: String, - uuid: String + login: String, + uuid: String ) extends ResourcePage case class EmbeddedUserResourceUpdated( - login: String, - uuid: String, - update: Int + login: String, + uuid: String, + update: Int ) extends ResourcePage diff --git a/client/src/main/scala/com.olegych.scastie.client/ScastieBackend.scala b/client/src/main/scala/com.olegych.scastie.client/ScastieBackend.scala index 3b503dded..8b701be72 100644 --- a/client/src/main/scala/com.olegych.scastie.client/ScastieBackend.scala +++ b/client/src/main/scala/com.olegych.scastie.client/ScastieBackend.scala @@ -1,6 +1,8 @@ package com.olegych.scastie.client import java.util.UUID +import scala.concurrent.Future +import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue import com.olegych.scastie.api._ import com.olegych.scastie.client.components.Scastie @@ -10,13 +12,9 @@ import japgolly.scalajs.react.extra._ import japgolly.scalajs.react.util.Effect.Id import org.scalajs.dom.{Position => _, _} -import scala.concurrent.Future -import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue - case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: BackendScope[Scastie, ScastieState]) { - private val restApiClient = - new RestApiClient(serverUrl) + private val restApiClient = new RestApiClient(serverUrl) // XXX: This should not be global Global.subscribe(scope, scastieId) @@ -25,131 +23,98 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac Callback(Global.subscribe(scope, scastieId)) } - val codeChange: String ~=> Callback = - Reusable.fn(code => scope.modState(_.setCode(code))) + val codeChange: String ~=> Callback = Reusable.fn(code => scope.modState(_.setCode(code))) val sbtConfigChange: String ~=> Callback = { Reusable.fn(newConfig => scope.modState(_.setSbtConfigExtra(newConfig))) } - val resetBuild: Reusable[Callback] = - Reusable.always { - val setData = scope.state.map( - state => { - state - .setInputs(Inputs.default.copy(code = state.inputs.code)) - .clearOutputs - .clearSnippetId - .setChangedInputs - } - ) + val resetBuild: Reusable[Callback] = Reusable.always { + val setData = scope.state.map(state => { + state + .setInputs(Inputs.default.copy(code = state.inputs.code)) + .clearOutputs + .clearSnippetId + .setChangedInputs + }) - setData >> setHome - } + setData >> setHome + } - val newSnippet: Reusable[Callback] = - Reusable.always { - val setData = scope.state.map(state => { - state - .copy(isDesktopForced = false) - .setInputs(Inputs.default.copy(code = "")) - .clearOutputs - .clearSnippetId - .setChangedInputs - }) - - setData >> setHome - } + val newSnippet: Reusable[Callback] = Reusable.always { + val setData = scope.state.map(state => { + state + .copy(isDesktopForced = false) + .setInputs(Inputs.default.copy(code = "")) + .clearOutputs + .clearSnippetId + .setChangedInputs + }) + + setData >> setHome + } val clear: Reusable[Callback] = Reusable.always(scope.modState(_.clearOutputsPreserveConsole) >> scope.modState(_.closeModals)) - private def clearOutputs: Callback = - scope.modState(_.clearOutputs) + private def clearOutputs: Callback = scope.modState(_.clearOutputs) - def clearCode: Callback = - scope.modState(_.setCode("")) + def clearCode: Callback = scope.modState(_.setCode("")) - val setViewReused: View ~=> Callback = - Reusable.fn(setView _) + val setViewReused: View ~=> Callback = Reusable.fn(setView _) - def setView(newView: View): Callback = - scope.modState(_.setView(newView)) + def setView(newView: View): Callback = scope.modState(_.setView(newView)) val viewSnapshot: StateSnapshot.withReuse.FromSetStateFn[View] = StateSnapshot.withReuse.prepare((opts, c) => opts.fold(c)(setView)) - val setTarget: ScalaTarget ~=> Callback = - Reusable.fn(target => scope.modState(_.setTarget(target))) + val setTarget: ScalaTarget ~=> Callback = Reusable.fn(target => scope.modState(_.setTarget(target))) - val addScalaDependency: (ScalaDependency, Project) ~=> Callback = - Reusable.fn { - case (scalaDependency, project) => - scope.modState(_.addScalaDependency(scalaDependency, project)) - } + val addScalaDependency: (ScalaDependency, Project) ~=> Callback = Reusable.fn { case (scalaDependency, project) => + scope.modState(_.addScalaDependency(scalaDependency, project)) + } val removeScalaDependency: ScalaDependency ~=> Callback = - Reusable.fn( - scalaDependency => scope.modState(_.removeScalaDependency(scalaDependency)) - ) + Reusable.fn(scalaDependency => scope.modState(_.removeScalaDependency(scalaDependency))) - val updateDependencyVersion: (ScalaDependency, String) ~=> Callback = - Reusable.fn { - case (scalaDependency, version) => - scope.modState(_.updateDependencyVersion(scalaDependency, version)) - } + val updateDependencyVersion: (ScalaDependency, String) ~=> Callback = Reusable.fn { case (scalaDependency, version) => + scope.modState(_.updateDependencyVersion(scalaDependency, version)) + } - val toggleTheme: Reusable[Callback] = - Reusable.always(scope.modState(_.toggleTheme)) + val toggleTheme: Reusable[Callback] = Reusable.always(scope.modState(_.toggleTheme)) - val setMetalsStatus: MetalsStatus ~=> Callback = - Reusable.fn(status => scope.modState(_.setMetalsStatus(status))) + val setMetalsStatus: MetalsStatus ~=> Callback = Reusable.fn(status => scope.modState(_.setMetalsStatus(status))) - val toggleMetalsStatus: Reusable[Callback] = - Reusable.always(scope.modState(_.toggleMetalsStatus)) + val toggleMetalsStatus: Reusable[Callback] = Reusable.always(scope.modState(_.toggleMetalsStatus)) - val toggleLineNumbers: Reusable[Callback] = - Reusable.always(scope.modState(_.toggleLineNumbers)) + val toggleLineNumbers: Reusable[Callback] = Reusable.always(scope.modState(_.toggleLineNumbers)) - val togglePresentationMode: Reusable[Callback] = - Reusable.always(scope.modState(_.togglePresentationMode)) + val togglePresentationMode: Reusable[Callback] = Reusable.always(scope.modState(_.togglePresentationMode)) - val openConsole: Reusable[Callback] = - Reusable.always(scope.modState(_.openConsole)) + val openConsole: Reusable[Callback] = Reusable.always(scope.modState(_.openConsole)) - val closeConsole: Reusable[Callback] = - Reusable.always(scope.modState(_.closeConsole)) + val closeConsole: Reusable[Callback] = Reusable.always(scope.modState(_.closeConsole)) - val toggleConsole: Reusable[Callback] = - Reusable.always(scope.modState(_.toggleConsole)) + val toggleConsole: Reusable[Callback] = Reusable.always(scope.modState(_.toggleConsole)) - val openResetModal: Reusable[Callback] = - Reusable.always(scope.modState(_.openResetModal)) + val openResetModal: Reusable[Callback] = Reusable.always(scope.modState(_.openResetModal)) - val closeResetModal: Reusable[Callback] = - Reusable.always(scope.modState(_.closeResetModal)) + val closeResetModal: Reusable[Callback] = Reusable.always(scope.modState(_.closeResetModal)) - val openNewSnippetModal: Reusable[Callback] = - Reusable.always(scope.modState(_.openNewSnippetModal)) + val openNewSnippetModal: Reusable[Callback] = Reusable.always(scope.modState(_.openNewSnippetModal)) // ok - private def closeNewSnippetModal0: Callback = - scope.modState(_.closeNewSnippetModal) + private def closeNewSnippetModal0: Callback = scope.modState(_.closeNewSnippetModal) - val closeNewSnippetModal: Reusable[Callback] = - Reusable.always(closeNewSnippetModal0) + val closeNewSnippetModal: Reusable[Callback] = Reusable.always(closeNewSnippetModal0) - val openHelpModal: Reusable[Callback] = - Reusable.always(scope.modState(_.openHelpModal)) + val openHelpModal: Reusable[Callback] = Reusable.always(scope.modState(_.openHelpModal)) - val openPrivacyPolicyModal: Reusable[Callback] = - Reusable.always(scope.modState(_.openPrivacyPolicyModal)) + val openPrivacyPolicyModal: Reusable[Callback] = Reusable.always(scope.modState(_.openPrivacyPolicyModal)) - val closeHelpModal: Reusable[Callback] = - Reusable.always(scope.modState(_.toggleHelpModal)) + val closeHelpModal: Reusable[Callback] = Reusable.always(scope.modState(_.toggleHelpModal)) - val closePrivacyPolicyModal: Reusable[Callback] = - Reusable.always(scope.modState(_.togglePrivacyPolicyModal)) + val closePrivacyPolicyModal: Reusable[Callback] = Reusable.always(scope.modState(_.togglePrivacyPolicyModal)) val closePrivacyPolicyPrompt: Reusable[Callback] = Reusable.always(scope.modState(_.setPrivacyPolicyPromptClosed(true))) @@ -157,17 +122,13 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac val openPrivacyPolicyPrompt: Reusable[Callback] = Reusable.always(scope.modState(_.setPrivacyPolicyPromptClosed(false))) - val openLoginModal: Reusable[Callback] = - Reusable.always(scope.modState(_.setLoginModalClosed(false))) + val openLoginModal: Reusable[Callback] = Reusable.always(scope.modState(_.setLoginModalClosed(false))) - val closeLoginModal: Reusable[Callback] = - Reusable.always(scope.modState(_.setLoginModalClosed(true))) + val closeLoginModal: Reusable[Callback] = Reusable.always(scope.modState(_.setLoginModalClosed(true))) - val toggleHelpModal: Reusable[Callback] = - Reusable.always(scope.modState(_.toggleHelpModal)) + val toggleHelpModal: Reusable[Callback] = Reusable.always(scope.modState(_.toggleHelpModal)) - val closeShareModal: Reusable[Callback] = - Reusable.always(scope.modState(_.closeShareModal)) + val closeShareModal: Reusable[Callback] = Reusable.always(scope.modState(_.closeShareModal)) val openShareModalOption: Option[SnippetId] ~=> Callback = Reusable.fn(snippetId => scope.modState(_.openShareModal(snippetId))) @@ -175,17 +136,13 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac val openShareModal: SnippetId ~=> Callback = Reusable.fn(snippetId => scope.modState(_.openShareModal(Some(snippetId)))) - val openEmbeddedModal: Reusable[Callback] = - Reusable.always(scope.modState(_.openEmbeddedModal)) + val openEmbeddedModal: Reusable[Callback] = Reusable.always(scope.modState(_.openEmbeddedModal)) - val closeEmbeddedModal: Reusable[Callback] = - Reusable.always(scope.modState(_.closeEmbeddedModal)) + val closeEmbeddedModal: Reusable[Callback] = Reusable.always(scope.modState(_.closeEmbeddedModal)) - val forceDesktop: Reusable[Callback] = - Reusable.always(scope.modState(_.forceDesktop)) + val forceDesktop: Reusable[Callback] = Reusable.always(scope.modState(_.forceDesktop)) - val toggleWorksheetMode: Reusable[Callback] = - Reusable.always(unlessEmbedded(_.toggleWorksheetMode)) + val toggleWorksheetMode: Reusable[Callback] = Reusable.always(unlessEmbedded(_.toggleWorksheetMode)) private def unlessEmbedded(f: ScastieState => ScastieState): Callback = { scope.props @@ -207,11 +164,9 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac progress.isDone } - def onOpen(): Unit = - direct.modState(_.logSystem("Connected. Waiting for sbt")) + def onOpen(): Unit = direct.modState(_.logSystem("Connected. Waiting for sbt")) - def onError(error: String): Unit = - direct.modState(_.logSystem(s"Error: $error")) + def onError(error: String): Unit = direct.modState(_.logSystem(s"Error: $error")) def onClose(reason: Option[String]): Unit = { val msg = reason.map(": " + _).getOrElse(".") @@ -223,14 +178,12 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac ) } - def onConnectionError(error: String): Callback = - scope.modState(_.logSystem(s"Error: $error")) + def onConnectionError(error: String): Callback = scope.modState(_.logSystem(s"Error: $error")) - def onConnected(stream: EventStream[SnippetProgress]): Callback = - scope.modState( - _.run(snippetId) - .copy(progressStream = Some(stream)) - ) + def onConnected(stream: EventStream[SnippetProgress]): Callback = scope.modState( + _.run(snippetId) + .copy(progressStream = Some(stream)) + ) } ) } @@ -283,44 +236,41 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac ) } - val run: Reusable[Callback] = - Reusable.always( - scope.state.flatMap( - state => - Callback.future( - restApiClient - .run(state.inputs) - .map(connectProgress) - ) + val run: Reusable[Callback] = Reusable.always( + scope.state.flatMap(state => + Callback.future( + restApiClient + .run(state.inputs) + .map(connectProgress) ) ) + ) - val acceptPolicy: Reusable[Callback] = - Reusable.always( - Callback.future { - restApiClient.acceptPrivacyPolicy().map { result => - scope.modState(_.setPrivacyPolicyPromptClosed(result)) - } + val acceptPolicy: Reusable[Callback] = Reusable.always( + Callback.future { + restApiClient.acceptPrivacyPolicy().map { result => + scope.modState(_.setPrivacyPolicyPromptClosed(result)) } - ) + } + ) - val removeUserFromPolicyStatus: Reusable[Callback] = - Reusable.always( - Callback.future { - restApiClient.removeUserFromPolicyStatus().map { result => - scope.modState(_.setPrivacyPolicyPromptClosed(result)).map(_ => { + val removeUserFromPolicyStatus: Reusable[Callback] = Reusable.always( + Callback.future { + restApiClient.removeUserFromPolicyStatus().map { result => + scope + .modState(_.setPrivacyPolicyPromptClosed(result)) + .map(_ => { if (result) document.location.reload() }) - } } - ) + } + ) - val removeAllUserSnippets: Reusable[Callback] = - Reusable.always( - Callback.future { - restApiClient.removeAllUserSnippets().map(Callback(_)) - } - ) + val removeAllUserSnippets: Reusable[Callback] = Reusable.always( + Callback.future { + restApiClient.removeAllUserSnippets().map(Callback(_)) + } + ) val refusePrivacyPolicy: Reusable[Callback] = Reusable.always( removeAllUserSnippets >> removeUserFromPolicyStatus @@ -328,7 +278,7 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac private def saveCallback(sId: SnippetId): Callback = { val setState = scope.modState(_.setCleanInputs.setSnippetId(sId).setLoadSnippet(false)) - val page = Page.fromSnippetId(sId) + val page = Page.fromSnippetId(sId) val setUrl = scope.props.map { _.router.foreach(r => org.scalajs.dom.window.history.pushState("", "", r.urlFor(page).value)) } @@ -341,56 +291,52 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac } } - val saveBlocking: Reusable[CallbackTo[Option[SnippetId]]] = - Reusable.always( - scope.state.map(state => restApiClient.saveBlocking(state.inputs)) - ) + val saveBlocking: Reusable[CallbackTo[Option[SnippetId]]] = Reusable.always( + scope.state.map(state => restApiClient.saveBlocking(state.inputs)) + ) - val saveOrUpdate: Reusable[Callback] = - Reusable.always( - scope.props.flatMap { props => - scope.state - .flatMap { state => - if (props.isEmbedded) { - run - } else { - state.snippetId match { - case Some(snippetId) => - if (snippetId.isOwnedBy(state.user)) { - update0(snippetId) - } else { - fork0(snippetId) - } - case None => save0 - } + val saveOrUpdate: Reusable[Callback] = Reusable.always( + scope.props.flatMap { props => + scope.state + .flatMap { state => + if (props.isEmbedded) { + run + } else { + state.snippetId match { + case Some(snippetId) => + if (snippetId.isOwnedBy(state.user)) { + update0(snippetId) + } else { + fork0(snippetId) + } + case None => save0 } } - } - ) - - private def fork0(snippetId: SnippetId): Callback = - scope.state.flatMap { state => - Callback.future( - restApiClient - .fork(EditInputs(snippetId, state.inputs)) - .map { - case Some(sId) => saveCallback(sId) - case None => Callback(window.alert("Failed to fork")) - } - ) + } } + ) - private def update0(snippetId: SnippetId): Callback = - scope.state.flatMap { state => - Callback.future( - restApiClient - .update(EditInputs(snippetId, state.inputs)) - .map { - case Some(sId) => saveCallback(sId) - case None => Callback(window.alert("Failed to update")) - } - ) - } + private def fork0(snippetId: SnippetId): Callback = scope.state.flatMap { state => + Callback.future( + restApiClient + .fork(EditInputs(snippetId, state.inputs)) + .map { + case Some(sId) => saveCallback(sId) + case None => Callback(window.alert("Failed to fork")) + } + ) + } + + private def update0(snippetId: SnippetId): Callback = scope.state.flatMap { state => + Callback.future( + restApiClient + .update(EditInputs(snippetId, state.inputs)) + .map { + case Some(sId) => saveCallback(sId) + case None => Callback(window.alert("Failed to update")) + } + ) + } def loadOldSnippet(id: Int): Callback = { loadSnippetBase( @@ -407,34 +353,31 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac } private def loadSnippetBase( - fetchSnippet: => Future[Option[FetchResult]], - afterLoading: ScastieState => ScastieState = identity, - snippetId: Option[SnippetId] = None + fetchSnippet: => Future[Option[FetchResult]], + afterLoading: ScastieState => ScastieState = identity, + snippetId: Option[SnippetId] = None ): Callback = { scope.state.flatMap { state => if (state.loadSnippet) { - val loadStateFromApi = - Callback.future( - fetchSnippet.map { - case Some(FetchResult(inputs, progresses)) => - val isDone = progresses.exists(_.isDone) - val connect = - snippetId match { - case Some(sid) if !isDone => connectProgress(sid) - case _ => Callback(()) - } - clearOutputs >> scope.modState { state => - afterLoading( - state - .setInputs(inputs) - .setProgresses(progresses) - .setCleanInputs - ) - } >> connect - case _ => - scope.modState(_.setCode(s"//snippet not found")) - } - ) + val loadStateFromApi = Callback.future( + fetchSnippet.map { + case Some(FetchResult(inputs, progresses)) => + val isDone = progresses.exists(_.isDone) + val connect = snippetId match { + case Some(sid) if !isDone => connectProgress(sid) + case _ => Callback(()) + } + clearOutputs >> scope.modState { state => + afterLoading( + state + .setInputs(inputs) + .setProgresses(progresses) + .setCleanInputs + ) + } >> connect + case _ => scope.modState(_.setCode(s"//snippet not found")) + } + ) loadStateFromApi >> setView(View.Editor) >> @@ -447,16 +390,15 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac } } - def loadUser: Callback = - Callback.future( - restApiClient - .fetchUser() - .map(result => scope.modState(_.setUser(result))) - ) >> Callback.future( - restApiClient - .getPrivacyPolicyStatus() - .map(result => scope.modState(_.setPrivacyPolicyPromptClosed(result))) - ) + def loadUser: Callback = Callback.future( + restApiClient + .fetchUser() + .map(result => scope.modState(_.setUser(result))) + ) >> Callback.future( + restApiClient + .getPrivacyPolicyStatus() + .map(result => scope.modState(_.setPrivacyPolicyPromptClosed(result))) + ) val formatCode: Reusable[Callback] = Reusable.always { scope.state.flatMap { state => @@ -464,31 +406,29 @@ case class ScastieBackend(scastieId: UUID, serverUrl: Option[String], scope: Bac restApiClient .format(FormatRequest(state.inputs.code, state.inputs.isWorksheetMode, state.inputs.target)) .map { - case FormatResponse(Right(formattedCode)) => - scope.modState { s => + case FormatResponse(Right(formattedCode)) => scope.modState { s => // avoid overriding user's code if he/she types while it's formatting - if (s.inputs.code == state.inputs.code) - s.clearOutputsPreserveConsole.setCode(formattedCode) + if (s.inputs.code == state.inputs.code) s.clearOutputsPreserveConsole.setCode(formattedCode) else s } - case FormatResponse(Left(error)) => - scope.modState { - _.setRuntimeError(Some(RuntimeError(message = "Formatting failed: " + error, line = None, fullStack = ""))) + case FormatResponse(Left(error)) => scope.modState { + _.setRuntimeError( + Some(RuntimeError(message = "Formatting failed: " + error, line = None, fullStack = "")) + ) } } } } } - val loadProfile: Reusable[Future[List[SnippetSummary]]] = - Reusable.always(restApiClient.fetchUserSnippets()) + val loadProfile: Reusable[Future[List[SnippetSummary]]] = Reusable.always(restApiClient.fetchUserSnippets()) - val deleteSnippet: SnippetId ~=> Future[Boolean] = - Reusable.always(snippetId => restApiClient.delete(snippetId)) + val deleteSnippet: SnippetId ~=> Future[Boolean] = Reusable.always(snippetId => restApiClient.delete(snippetId)) private def setHome = scope.props.flatMap( _.router .map(_.set(Home)) .getOrElse(Callback.empty) ) + } diff --git a/client/src/main/scala/com.olegych.scastie.client/ScastieState.scala b/client/src/main/scala/com.olegych.scastie.client/ScastieState.scala index b13872139..72fe4f8f5 100644 --- a/client/src/main/scala/com.olegych.scastie.client/ScastieState.scala +++ b/client/src/main/scala/com.olegych.scastie.client/ScastieState.scala @@ -1,8 +1,8 @@ package com.olegych.scastie.client import com.olegych.scastie.api._ -import org.scalajs.dom.HTMLElement import org.scalajs.dom.{Position => _} +import org.scalajs.dom.HTMLElement import play.api.libs.json._ sealed trait MetalsStatus { @@ -30,17 +30,17 @@ case class NetworkError(msg: String) extends MetalsStatus { } object SnippetState { - implicit val formatSnippetState: OFormat[SnippetState] = - Json.format[SnippetState] + implicit val formatSnippetState: OFormat[SnippetState] = Json.format[SnippetState] } case class SnippetState( - snippetId: Option[SnippetId], - loadSnippet: Boolean, - scalaJsContent: Option[String], + snippetId: Option[SnippetId], + loadSnippet: Boolean, + scalaJsContent: Option[String] ) object ScastieState { + def default(isEmbedded: Boolean): ScastieState = { ScastieState( view = View.Editor, @@ -59,7 +59,7 @@ object ScastieState { snippetState = SnippetState( snippetId = None, loadSnippet = true, - scalaJsContent = None, + scalaJsContent = None ), user = None, attachedDoms = Map(), @@ -73,8 +73,7 @@ object ScastieState { implicit val dontSerializeAttachedDoms: Format[Map[String, HTMLElement]] = dontSerialize[Map[String, HTMLElement]](Map()) - implicit val dontSerializeStatusState: Format[StatusState] = - dontSerialize[StatusState](StatusState.empty) + implicit val dontSerializeStatusState: Format[StatusState] = dontSerialize[StatusState](StatusState.empty) implicit val dontSerializeEventStream: Format[EventStream[StatusProgress]] = dontSerializeOption[EventStream[StatusProgress]] @@ -82,92 +81,89 @@ object ScastieState { implicit val dontSerializeProgressStream: Format[EventStream[SnippetProgress]] = dontSerializeOption[EventStream[SnippetProgress]] - implicit val dontSerializeMetalsStatus: Format[MetalsStatus] = - dontSerialize[MetalsStatus](MetalsLoading) + implicit val dontSerializeMetalsStatus: Format[MetalsStatus] = dontSerialize[MetalsStatus](MetalsLoading) - implicit val formatScastieState: OFormat[ScastieState] = - Json.format[ScastieState] + implicit val formatScastieState: OFormat[ScastieState] = Json.format[ScastieState] } case class ScastieState( - view: View, - isRunning: Boolean, - statusStream: Option[EventStream[StatusProgress]], - progressStream: Option[EventStream[SnippetProgress]], - modalState: ModalState, - isDarkTheme: Boolean, - isDesktopForced: Boolean, - isPresentationMode: Boolean, - showLineNumbers: Boolean, - consoleState: ConsoleState, - inputsHasChanged: Boolean, - snippetState: SnippetState, - user: Option[User], - attachedDoms: Map[String, HTMLElement], - inputs: Inputs, - outputs: Outputs, - status: StatusState, - metalsStatus: MetalsStatus = MetalsLoading, - isEmbedded: Boolean = false, - transient: Boolean = false, + view: View, + isRunning: Boolean, + statusStream: Option[EventStream[StatusProgress]], + progressStream: Option[EventStream[SnippetProgress]], + modalState: ModalState, + isDarkTheme: Boolean, + isDesktopForced: Boolean, + isPresentationMode: Boolean, + showLineNumbers: Boolean, + consoleState: ConsoleState, + inputsHasChanged: Boolean, + snippetState: SnippetState, + user: Option[User], + attachedDoms: Map[String, HTMLElement], + inputs: Inputs, + outputs: Outputs, + status: StatusState, + metalsStatus: MetalsStatus = MetalsLoading, + isEmbedded: Boolean = false, + transient: Boolean = false ) { def snippetId: Option[SnippetId] = snippetState.snippetId - def loadSnippet: Boolean = snippetState.loadSnippet + def loadSnippet: Boolean = snippetState.loadSnippet def copyAndSave( - attachedDoms: Map[String, HTMLElement] = attachedDoms, - view: View = view, - isRunning: Boolean = isRunning, - statusStream: Option[EventStream[StatusProgress]] = statusStream, - progressStream: Option[EventStream[SnippetProgress]] = progressStream, - modalState: ModalState = modalState, - isDarkTheme: Boolean = isDarkTheme, - isPresentationMode: Boolean = isPresentationMode, - isDesktopForced: Boolean = isDesktopForced, - showLineNumbers: Boolean = showLineNumbers, - consoleState: ConsoleState = consoleState, - inputsHasChanged: Boolean = inputsHasChanged, - snippetId: Option[SnippetId] = snippetId, - loadSnippet: Boolean = loadSnippet, - scalaJsContent: Option[String] = snippetState.scalaJsContent, - user: Option[User] = user, - inputs: Inputs = inputs, - outputs: Outputs = outputs, - status: StatusState = status, - metalsStatus: MetalsStatus = metalsStatus, - transient: Boolean = transient, + attachedDoms: Map[String, HTMLElement] = attachedDoms, + view: View = view, + isRunning: Boolean = isRunning, + statusStream: Option[EventStream[StatusProgress]] = statusStream, + progressStream: Option[EventStream[SnippetProgress]] = progressStream, + modalState: ModalState = modalState, + isDarkTheme: Boolean = isDarkTheme, + isPresentationMode: Boolean = isPresentationMode, + isDesktopForced: Boolean = isDesktopForced, + showLineNumbers: Boolean = showLineNumbers, + consoleState: ConsoleState = consoleState, + inputsHasChanged: Boolean = inputsHasChanged, + snippetId: Option[SnippetId] = snippetId, + loadSnippet: Boolean = loadSnippet, + scalaJsContent: Option[String] = snippetState.scalaJsContent, + user: Option[User] = user, + inputs: Inputs = inputs, + outputs: Outputs = outputs, + status: StatusState = status, + metalsStatus: MetalsStatus = metalsStatus, + transient: Boolean = transient ): ScastieState = { - val state0 = - copy( - view = view, - isRunning = isRunning, - statusStream = statusStream, - progressStream = progressStream, - modalState = modalState, - isDarkTheme = isDarkTheme, - isDesktopForced = isDesktopForced, - isPresentationMode = isPresentationMode, - showLineNumbers = showLineNumbers, - consoleState = consoleState, - inputsHasChanged = inputsHasChanged, - snippetState = SnippetState( - snippetId = snippetId, - loadSnippet = loadSnippet, - scalaJsContent = scalaJsContent, - ), - user = user, - attachedDoms = attachedDoms, - inputs = inputs.copy( - isShowingInUserProfile = false, - forked = None - ), - outputs = outputs, - status = status, - metalsStatus = metalsStatus, - isEmbedded = isEmbedded, - transient = transient, - ) + val state0 = copy( + view = view, + isRunning = isRunning, + statusStream = statusStream, + progressStream = progressStream, + modalState = modalState, + isDarkTheme = isDarkTheme, + isDesktopForced = isDesktopForced, + isPresentationMode = isPresentationMode, + showLineNumbers = showLineNumbers, + consoleState = consoleState, + inputsHasChanged = inputsHasChanged, + snippetState = SnippetState( + snippetId = snippetId, + loadSnippet = loadSnippet, + scalaJsContent = scalaJsContent + ), + user = user, + attachedDoms = attachedDoms, + inputs = inputs.copy( + isShowingInUserProfile = false, + forked = None + ), + outputs = outputs, + status = status, + metalsStatus = metalsStatus, + isEmbedded = isEmbedded, + transient = transient + ) if (!isEmbedded && !transient) { LocalStorage.save(state0) @@ -182,8 +178,7 @@ case class ScastieState( def isBuildDefault: Boolean = inputs.isDefault - def isClearable: Boolean = - outputs.isClearable + def isClearable: Boolean = outputs.isClearable def run(snippetId: SnippetId): ScastieState = { clearOutputs.resetScalajs @@ -197,82 +192,63 @@ case class ScastieState( copyAndSave(isRunning = isRunning).autoOpen(openConsole) } - def toggleTheme: ScastieState = - copyAndSave(isDarkTheme = !isDarkTheme) + def toggleTheme: ScastieState = copyAndSave(isDarkTheme = !isDarkTheme) - def setTheme(dark: Boolean): ScastieState = - copyAndSave(isDarkTheme = dark) + def setTheme(dark: Boolean): ScastieState = copyAndSave(isDarkTheme = dark) - def setMetalsStatus(status: MetalsStatus): ScastieState = - copyAndSave(metalsStatus = status) + def setMetalsStatus(status: MetalsStatus): ScastieState = copyAndSave(metalsStatus = status) def toggleMetalsStatus: ScastieState = copyAndSave(metalsStatus = if (metalsStatus != MetalsDisabled) MetalsDisabled else MetalsLoading) - def toggleLineNumbers: ScastieState = - copyAndSave(showLineNumbers = !showLineNumbers) + def toggleLineNumbers: ScastieState = copyAndSave(showLineNumbers = !showLineNumbers) - def togglePresentationMode: ScastieState = - copyAndSave(isPresentationMode = !isPresentationMode) + def togglePresentationMode: ScastieState = copyAndSave(isPresentationMode = !isPresentationMode) - def toggleWorksheetMode: ScastieState = - copyAndSave( - inputs = inputs.copy(_isWorksheetMode = !inputs.isWorksheetMode), - inputsHasChanged = true - ) + def toggleWorksheetMode: ScastieState = copyAndSave( + inputs = inputs.copy(_isWorksheetMode = !inputs.isWorksheetMode), + inputsHasChanged = true + ) - def toggleHelpModal: ScastieState = - copyAndSave( - modalState = modalState.copy(isHelpModalClosed = !modalState.isHelpModalClosed) - ) + def toggleHelpModal: ScastieState = copyAndSave( + modalState = modalState.copy(isHelpModalClosed = !modalState.isHelpModalClosed) + ) - def togglePrivacyPolicyModal: ScastieState = - copyAndSave( - modalState = modalState.copy(isPrivacyPolicyModalClosed = !modalState.isPrivacyPolicyModalClosed) - ) + def togglePrivacyPolicyModal: ScastieState = copyAndSave( + modalState = modalState.copy(isPrivacyPolicyModalClosed = !modalState.isPrivacyPolicyModalClosed) + ) - def setPrivacyPolicyPromptClosed(status: Boolean): ScastieState = - copyAndSave( - modalState = modalState.copy(isPrivacyPolicyPromptClosed = status) - ) + def setPrivacyPolicyPromptClosed(status: Boolean): ScastieState = copyAndSave( + modalState = modalState.copy(isPrivacyPolicyPromptClosed = status) + ) - def setLoginModalClosed(status: Boolean): ScastieState = - copyAndSave( - modalState = modalState.copy(isLoginModalClosed = status) - ) + def setLoginModalClosed(status: Boolean): ScastieState = copyAndSave( + modalState = modalState.copy(isLoginModalClosed = status) + ) - def openHelpModal: ScastieState = - copyAndSave(modalState = modalState.copy(isHelpModalClosed = false)) + def openHelpModal: ScastieState = copyAndSave(modalState = modalState.copy(isHelpModalClosed = false)) def openPrivacyPolicyModal: ScastieState = copyAndSave(modalState = modalState.copy(isPrivacyPolicyModalClosed = false)) - def closeHelpModal: ScastieState = - copyAndSave(modalState = modalState.copy(isHelpModalClosed = true)) + def closeHelpModal: ScastieState = copyAndSave(modalState = modalState.copy(isHelpModalClosed = true)) - def openResetModal: ScastieState = - copyAndSave(modalState = modalState.copy(isResetModalClosed = false)) + def openResetModal: ScastieState = copyAndSave(modalState = modalState.copy(isResetModalClosed = false)) - def closeResetModal: ScastieState = - copyAndSave(modalState = modalState.copy(isResetModalClosed = true)) + def closeResetModal: ScastieState = copyAndSave(modalState = modalState.copy(isResetModalClosed = true)) - def openNewSnippetModal: ScastieState = - copyAndSave(modalState = modalState.copy(isNewSnippetModalClosed = false)) + def openNewSnippetModal: ScastieState = copyAndSave(modalState = modalState.copy(isNewSnippetModalClosed = false)) - def closeNewSnippetModal: ScastieState = - copyAndSave(modalState = modalState.copy(isNewSnippetModalClosed = true)) + def closeNewSnippetModal: ScastieState = copyAndSave(modalState = modalState.copy(isNewSnippetModalClosed = true)) def openShareModal(snippetId: Option[SnippetId]): ScastieState = copyAndSave(modalState = modalState.copy(shareModalSnippetId = snippetId)) - def closeShareModal: ScastieState = - copyAndSave(modalState = modalState.copy(shareModalSnippetId = None)) + def closeShareModal: ScastieState = copyAndSave(modalState = modalState.copy(shareModalSnippetId = None)) - def openEmbeddedModal: ScastieState = - copyAndSave(modalState = modalState.copy(isEmbeddedClosed = false)) + def openEmbeddedModal: ScastieState = copyAndSave(modalState = modalState.copy(isEmbeddedClosed = false)) - def closeEmbeddedModal: ScastieState = - copyAndSave(modalState = modalState.copy(isEmbeddedClosed = true)) + def closeEmbeddedModal: ScastieState = copyAndSave(modalState = modalState.copy(isEmbeddedClosed = true)) def forceDesktop: ScastieState = copyAndSave(isDesktopForced = true) @@ -305,16 +281,14 @@ case class ScastieState( def toggleConsole: ScastieState = { copyAndSave( consoleState = - if (consoleState.consoleIsOpen) - consoleState.copy( - consoleIsOpen = false, - userOpenedConsole = false - ) - else - consoleState.copy( - consoleIsOpen = true, - userOpenedConsole = true - ) + if (consoleState.consoleIsOpen) consoleState.copy( + consoleIsOpen = false, + userOpenedConsole = false + ) + else consoleState.copy( + consoleIsOpen = true, + userOpenedConsole = true + ) ) } @@ -322,11 +296,9 @@ case class ScastieState( copyAndSave(consoleState = consoleState.copy(consoleHasUserOutput = true)) } - def setLoadSnippet(value: Boolean): ScastieState = - copy(snippetState = snippetState.copy(loadSnippet = value)) + def setLoadSnippet(value: Boolean): ScastieState = copy(snippetState = snippetState.copy(loadSnippet = value)) - def setUser(user: Option[User]): ScastieState = - copyAndSave(user = user) + def setUser(user: Option[User]): ScastieState = copyAndSave(user = user) def setCode(code: String): ScastieState = { if (inputs.code != code) { @@ -339,51 +311,43 @@ case class ScastieState( } } - def setInputs(inputs: Inputs): ScastieState = - copyAndSave( - inputs = inputs - ) + def setInputs(inputs: Inputs): ScastieState = copyAndSave( + inputs = inputs + ) - def setSbtConfigExtra(config: String): ScastieState = - copyAndSave( - inputs = inputs.copy(sbtConfigExtra = config), - inputsHasChanged = true - ) + def setSbtConfigExtra(config: String): ScastieState = copyAndSave( + inputs = inputs.copy(sbtConfigExtra = config), + inputsHasChanged = true + ) - def setChangedInputs: ScastieState = - copyAndSave(inputsHasChanged = true) + def setChangedInputs: ScastieState = copyAndSave(inputsHasChanged = true) - def setCleanInputs: ScastieState = - copyAndSave(inputsHasChanged = false) + def setCleanInputs: ScastieState = copyAndSave(inputsHasChanged = false) - def setView(newView: View): ScastieState = - copyAndSave(view = newView) + def setView(newView: View): ScastieState = copyAndSave(view = newView) - def setTarget(target: ScalaTarget): ScastieState = - copyAndSave( - inputs = inputs.modifyConfig(_.copy(target = target)), - inputsHasChanged = true - ) + def setTarget(target: ScalaTarget): ScastieState = copyAndSave( + inputs = inputs.modifyConfig(_.copy(target = target)), + inputsHasChanged = true + ) - def clearDependencies: ScastieState = - copyAndSave( - inputs = inputs.clearDependencies, - inputsHasChanged = true - ) + def clearDependencies: ScastieState = copyAndSave( + inputs = inputs.clearDependencies, + inputsHasChanged = true + ) def addScalaDependency(scalaDependency: ScalaDependency, project: Project): ScastieState = { val newInputs = inputs.addScalaDependency(scalaDependency, project) copyAndSave( inputs = newInputs, - inputsHasChanged = newInputs != inputs, + inputsHasChanged = newInputs != inputs ) } - def removeScalaDependency(scalaDependency: ScalaDependency): ScastieState = - copyAndSave( - inputs = inputs.removeScalaDependency(scalaDependency), - inputsHasChanged = true - ) + def removeScalaDependency(scalaDependency: ScalaDependency): ScastieState = copyAndSave( + inputs = inputs.removeScalaDependency(scalaDependency), + inputsHasChanged = true + ) def updateDependencyVersion(scalaDependency: ScalaDependency, version: String): ScastieState = { copyAndSave( @@ -407,24 +371,21 @@ case class ScastieState( def clearOutputsPreserveConsole: ScastieState = { copyAndSave( - outputs = Outputs.default.copy(consoleOutputs = outputs.consoleOutputs), + outputs = Outputs.default.copy(consoleOutputs = outputs.consoleOutputs) ) } - def closeModals: ScastieState = - copyAndSave(modalState = ModalState.allClosed) + def closeModals: ScastieState = copyAndSave(modalState = ModalState.allClosed) def setRuntimeError(runtimeError: Option[RuntimeError]): ScastieState = if (runtimeError.isEmpty) this else copyAndSave(outputs = outputs.copy(runtimeError = runtimeError)) - def setSbtError(err: Boolean): ScastieState = - copyAndSave(outputs = outputs.copy(sbtError = err)) + def setSbtError(err: Boolean): ScastieState = copyAndSave(outputs = outputs.copy(sbtError = err)) def logOutput(line: Option[ProcessOutput], wrap: ProcessOutput => ConsoleOutput): ScastieState = { line match { - case Some(l) => - copyAndSave( + case Some(l) => copyAndSave( outputs = outputs.copy( consoleOutputs = outputs.consoleOutputs ++ Vector(wrap(l)) ) @@ -460,10 +421,8 @@ case class ScastieState( def addStatus(statusUpdate: StatusProgress): ScastieState = { statusUpdate match { - case StatusProgress.KeepAlive => - this - case StatusProgress.Sbt(sbtRunners) => - copyAndSave(status = status.copy(sbtRunners = Some(sbtRunners))) + case StatusProgress.KeepAlive => this + case StatusProgress.Sbt(sbtRunners) => copyAndSave(status = status.copy(sbtRunners = Some(sbtRunners))) } } @@ -472,8 +431,8 @@ case class ScastieState( } def setProgresses(progresses: List[SnippetProgress]): ScastieState = coalesceUpdates { self => - progresses.foldLeft(self) { - case (state, progress) => state.addProgress(progress) + progresses.foldLeft(self) { case (state, progress) => + state.addProgress(progress) } } @@ -506,20 +465,18 @@ case class ScastieState( val useWorksheetModeTip = if (compilationInfos.exists(ci => topDef(ci))) - if (inputs.target.hasWorksheetMode) - Set( - info( - """|It seems you're writing code without an enclosing class/object. - |Switch to Worksheet mode if you want to use scastie more like a REPL.""".stripMargin - ) + if (inputs.target.hasWorksheetMode) Set( + info( + """|It seems you're writing code without an enclosing class/object. + |Switch to Worksheet mode if you want to use scastie more like a REPL.""".stripMargin ) - else - Set( - info( - """|It seems you're writing code without an enclosing class/object. - |This configuration does not support Worksheet mode.""".stripMargin - ) + ) + else Set( + info( + """|It seems you're writing code without an enclosing class/object. + |This configuration does not support Worksheet mode.""".stripMargin ) + ) else Set() copyAndSave( diff --git a/client/src/main/scala/com.olegych.scastie.client/StatusState.scala b/client/src/main/scala/com.olegych.scastie.client/StatusState.scala index dda0e8e17..282a15be4 100644 --- a/client/src/main/scala/com.olegych.scastie.client/StatusState.scala +++ b/client/src/main/scala/com.olegych.scastie.client/StatusState.scala @@ -8,5 +8,5 @@ object StatusState { final case class StatusState(sbtRunners: Option[Vector[SbtRunnerState]]) { def sbtRunnerCount: Option[Int] = sbtRunners.map(_.size) - def isSbtOk: Boolean = sbtRunners.exists(_.nonEmpty) + def isSbtOk: Boolean = sbtRunners.exists(_.nonEmpty) } diff --git a/client/src/main/scala/com.olegych.scastie.client/Views.scala b/client/src/main/scala/com.olegych.scastie.client/Views.scala index 9127fbd1e..af909448f 100644 --- a/client/src/main/scala/com.olegych.scastie.client/Views.scala +++ b/client/src/main/scala/com.olegych.scastie.client/Views.scala @@ -3,24 +3,25 @@ package com.olegych.scastie.client import play.api.libs.json._ sealed trait View + object View { - case object Editor extends View + case object Editor extends View case object BuildSettings extends View - case object CodeSnippets extends View - case object Status extends View + case object CodeSnippets extends View + case object Status extends View implicit object ViewFormat extends Format[View] { + def writes(view: View): JsValue = { JsString(view.toString) } - private val values: Map[String, View] = - List[View]( - Editor, - BuildSettings, - CodeSnippets, - Status - ).map(v => (v.toString, v)).toMap + private val values: Map[String, View] = List[View]( + Editor, + BuildSettings, + CodeSnippets, + Status + ).map(v => (v.toString, v)).toMap def reads(json: JsValue): JsResult[View] = { json match { @@ -33,5 +34,7 @@ object View { case _ => JsError(Seq()) } } + } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/BuildSettings.scala b/client/src/main/scala/com.olegych.scastie.client/components/BuildSettings.scala index f43c6572a..17b104eba 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/BuildSettings.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/BuildSettings.scala @@ -3,27 +3,26 @@ package com.olegych.scastie.client.components import com.olegych.scastie.api._ import com.olegych.scastie.client.components.editor.SimpleEditor import japgolly.scalajs.react._ - import vdom.all._ final case class BuildSettings( - visible: Boolean, - librariesFrom: Map[ScalaDependency, Project], - isDarkTheme: Boolean, - isBuildDefault: Boolean, - isResetModalClosed: Boolean, - scalaTarget: ScalaTarget, - sbtConfigExtra: String, - sbtConfig: String, - sbtPluginsConfig: String, - setTarget: ScalaTarget ~=> Callback, - closeResetModal: Reusable[Callback], - resetBuild: Reusable[Callback], - openResetModal: Reusable[Callback], - sbtConfigChange: String ~=> Callback, - removeScalaDependency: ScalaDependency ~=> Callback, - updateDependencyVersion: (ScalaDependency, String) ~=> Callback, - addScalaDependency: (ScalaDependency, Project) ~=> Callback + visible: Boolean, + librariesFrom: Map[ScalaDependency, Project], + isDarkTheme: Boolean, + isBuildDefault: Boolean, + isResetModalClosed: Boolean, + scalaTarget: ScalaTarget, + sbtConfigExtra: String, + sbtConfig: String, + sbtPluginsConfig: String, + setTarget: ScalaTarget ~=> Callback, + closeResetModal: Reusable[Callback], + resetBuild: Reusable[Callback], + openResetModal: Reusable[Callback], + sbtConfigChange: String ~=> Callback, + removeScalaDependency: ScalaDependency ~=> Callback, + updateDependencyVersion: (ScalaDependency, String) ~=> Callback, + addScalaDependency: (ScalaDependency, Project) ~=> Callback ) { @inline def render: VdomElement = BuildSettings.component(this) @@ -31,8 +30,7 @@ final case class BuildSettings( object BuildSettings { - implicit val reusability: Reusability[BuildSettings] = - Reusability.derive[BuildSettings] + implicit val reusability: Reusability[BuildSettings] = Reusability.derive[BuildSettings] private def render(props: BuildSettings): VdomElement = { @@ -51,7 +49,7 @@ object BuildSettings { title := "Reset your configuration", onClick --> props.openResetModal, role := "button", - cls := "btn", + cls := "btn", if (props.isBuildDefault) visibility.collapse else visibility.visible )( "Reset" @@ -69,7 +67,7 @@ object BuildSettings { div(cls := "build-settings-container")( resetButton, h2( - span("Target"), + span("Target") ), TargetSelector(props.scalaTarget, props.setTarget).render, h2( @@ -116,10 +114,10 @@ object BuildSettings { ) } - private val component = - ScalaComponent - .builder[BuildSettings]("BuildSettings") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[BuildSettings]("BuildSettings") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/ClearButton.scala b/client/src/main/scala/com.olegych.scastie.client/components/ClearButton.scala index 094ab50c4..84854550c 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/ClearButton.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/ClearButton.scala @@ -3,7 +3,6 @@ package components import com.olegych.scastie.client.components.editor.EditorKeymaps import japgolly.scalajs.react._ - import vdom.all._ final case class ClearButton(clear: Reusable[Callback]) { @@ -12,14 +11,13 @@ final case class ClearButton(clear: Reusable[Callback]) { object ClearButton { - implicit val reusability: Reusability[ClearButton] = - Reusability.derive[ClearButton] + implicit val reusability: Reusability[ClearButton] = Reusability.derive[ClearButton] private def render(props: ClearButton): VdomElement = { li( title := s"Clear Messages (${EditorKeymaps.clear.getName})", - role := "button", - cls := "btn", + role := "button", + cls := "btn", onClick --> props.clear )( div( @@ -29,10 +27,10 @@ object ClearButton { ) } - private val component = - ScalaComponent - .builder[ClearButton]("ClearButton") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[ClearButton]("ClearButton") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/CodeSnippets.scala b/client/src/main/scala/com.olegych.scastie.client/components/CodeSnippets.scala index 02e5c918d..630e7d29c 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/CodeSnippets.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/CodeSnippets.scala @@ -1,74 +1,68 @@ package com.olegych.scastie.client.components +import scala.concurrent.Future + import com.olegych.scastie.api._ import com.olegych.scastie.client.Page import com.olegych.scastie.client.View +import extra.router._ import japgolly.scalajs.react._ import japgolly.scalajs.react.component.builder.Lifecycle.RenderScope - -import scala.concurrent.Future - -import vdom.all._ -import extra.router._ import scalajs.concurrent.JSExecutionContext.Implicits.queue +import vdom.all._ final case class CodeSnippets( - view: View, - user: User, - router: RouterCtl[Page], - isDarkTheme: Boolean, - isShareModalClosed: SnippetId ~=> Boolean, - closeShareModal: Reusable[Callback], - openShareModal: SnippetId ~=> Callback, - loadProfile: Reusable[Future[List[SnippetSummary]]], - deleteSnippet: SnippetId ~=> Future[Boolean] + view: View, + user: User, + router: RouterCtl[Page], + isDarkTheme: Boolean, + isShareModalClosed: SnippetId ~=> Boolean, + closeShareModal: Reusable[Callback], + openShareModal: SnippetId ~=> Callback, + loadProfile: Reusable[Future[List[SnippetSummary]]], + deleteSnippet: SnippetId ~=> Future[Boolean] ) { @inline def render: VdomElement = CodeSnippets.component(this) } object CodeSnippets { - implicit val reusability: Reusability[CodeSnippets] = - Reusability.derive[CodeSnippets] + implicit val reusability: Reusability[CodeSnippets] = Reusability.derive[CodeSnippets] private[CodeSnippets] class CodeSnippetsBackend( - scope: BackendScope[CodeSnippets, List[SnippetSummary]] + scope: BackendScope[CodeSnippets, List[SnippetSummary]] ) { def loadProfile0(): Callback = { - scope.props.flatMap( - props => - Callback.future( - props.loadProfile.map( - _.map(summaries => scope.modState(_ => summaries)) - ) + scope.props.flatMap(props => + Callback.future( + props.loadProfile.map( + _.map(summaries => scope.modState(_ => summaries)) + ) ) ) } def deleteSnippet0(summary: SnippetSummary): Callback = { - scope.props.flatMap( - props => - Callback.future( - props - .deleteSnippet(summary.snippetId) - .map( - deleted => scope.modState(_.filterNot(_ == summary)).when_(deleted) - ) + scope.props.flatMap(props => + Callback.future( + props + .deleteSnippet(summary.snippetId) + .map(deleted => scope.modState(_.filterNot(_ == summary)).when_(deleted)) ) ) } + } private def renderSnippet(backend: CodeSnippetsBackend, props: CodeSnippets)( - summary: SnippetSummary + summary: SnippetSummary ): VdomElement = { - val page = Page.fromSnippetId(summary.snippetId) + val page = Page.fromSnippetId(summary.snippetId) val update = summary.snippetId.user.map(_.update.toString).getOrElse("") - val snippetUrl = - props.router.urlFor(Page.fromSnippetId(summary.snippetId)).value + val snippetUrl = props.router.urlFor(Page.fromSnippetId(summary.snippetId)).value div(cls := "snippet")( CopyModal( @@ -82,15 +76,15 @@ object CodeSnippets { ).render, div(cls := "header", "/" + summary.snippetId.base64UUID)( span(" - "), - div(cls := "clear-mobile"), + div(cls := "clear-mobile"), span(cls := "update", "Update: " + update), div(cls := "actions")( li(onClick --> props.openShareModal(summary.snippetId), cls := "btn", title := "Share", role := "button")( i(cls := "fa fa-share-alt") ), li( - cls := "btn", - role := "button", + cls := "btn", + role := "button", title := "Delete", onClick --> backend.deleteSnippet0(summary) )( @@ -108,21 +102,20 @@ object CodeSnippets { } private def render( - scope: RenderScope[ - CodeSnippets, - List[SnippetSummary], - CodeSnippetsBackend - ], - props: CodeSnippets, - summaries: List[SnippetSummary] + scope: RenderScope[ + CodeSnippets, + List[SnippetSummary], + CodeSnippetsBackend + ], + props: CodeSnippets, + summaries: List[SnippetSummary] ): VdomElement = { - val userAvatar = - div(cls := "avatar")( - img(src := props.user.avatar_url + "&s=70", alt := "Your Github Avatar", cls := "image-button avatar") - ) + val userAvatar = div(cls := "avatar")( + img(src := props.user.avatar_url + "&s=70", alt := "Your Github Avatar", cls := "image-button avatar") + ) - val userName = props.user.name.getOrElse("") + val userName = props.user.name.getOrElse("") val userLogin = props.user.login val noSummaries = @@ -133,11 +126,10 @@ object CodeSnippets { xs.groupBy(_.snippetId.base64UUID) .toList - .flatMap { - case (_, snippets) => - List( - snippets.sortBy(_.snippetId.user.map(_.update).getOrElse(0)).last - ) + .flatMap { case (_, snippets) => + List( + snippets.sortBy(_.snippetId.user.map(_.update).getOrElse(0)).last + ) } .sortBy(_.time) .reverse @@ -154,10 +146,9 @@ object CodeSnippets { div(cls := "snippets")( noSummaries, sortSnippets(summaries) - .map( - summary => - div(cls := "group", key := summary.snippetId.base64UUID)( - renderSnippet(scope.backend, props)(summary) + .map(summary => + div(cls := "group", key := summary.snippetId.base64UUID)( + renderSnippet(scope.backend, props)(summary) ) ) .toTagMod @@ -165,23 +156,21 @@ object CodeSnippets { ) } - private val component = - ScalaComponent - .builder[CodeSnippets]("CodeSnippets") - .initialState(List.empty[SnippetSummary]) - .backend(new CodeSnippetsBackend(_)) - .renderPS(render) - .componentWillReceiveProps { delta => - val viewChangedToCodeSnippet = - delta.currentProps.view != View.CodeSnippets && - delta.nextProps.view == View.CodeSnippets - - val loadProfile: Callback = - delta.backend.loadProfile0() - - loadProfile.when_(viewChangedToCodeSnippet) - } - .componentWillMount(_.backend.loadProfile0()) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[CodeSnippets]("CodeSnippets") + .initialState(List.empty[SnippetSummary]) + .backend(new CodeSnippetsBackend(_)) + .renderPS(render) + .componentWillReceiveProps { delta => + val viewChangedToCodeSnippet = delta.currentProps.view != View.CodeSnippets && + delta.nextProps.view == View.CodeSnippets + + val loadProfile: Callback = delta.backend.loadProfile0() + + loadProfile.when_(viewChangedToCodeSnippet) + } + .componentWillMount(_.backend.loadProfile0()) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/Console.scala b/client/src/main/scala/com.olegych.scastie.client/components/Console.scala index d49c4a374..490d648c6 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/Console.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/Console.scala @@ -6,24 +6,24 @@ import com.olegych.scastie.client.HTMLFormatter import com.olegych.scastie.client.View import japgolly.scalajs.react._ import org.scalajs.dom.raw.HTMLDivElement - import vdom.all._ -final case class Console(isOpen: Boolean, - isRunning: Boolean, - isEmbedded: Boolean, - consoleOutputs: Vector[ConsoleOutput], - run: Reusable[Callback], - setView: View ~=> Callback, - close: Reusable[Callback], - open: Reusable[Callback]) { +final case class Console( + isOpen: Boolean, + isRunning: Boolean, + isEmbedded: Boolean, + consoleOutputs: Vector[ConsoleOutput], + run: Reusable[Callback], + setView: View ~=> Callback, + close: Reusable[Callback], + open: Reusable[Callback] +) { @inline def render: VdomElement = Console.component(this) } object Console { - implicit val reusability: Reusability[Console] = - Reusability.derive[Console] + implicit val reusability: Reusability[Console] = Reusability.derive[Console] private val consoleElement = Ref[HTMLDivElement] @@ -33,8 +33,7 @@ object Console { else (display.none, display.flex) val consoleCss = - if (props.isOpen) - TagMod(cls := "console-open") + if (props.isOpen) TagMod(cls := "console-open") else EmptyVdom val (users, systems) = props.consoleOutputs.partition { @@ -59,7 +58,7 @@ object Console { isStatusOk = true, save = props.run, setView = props.setView, - embedded = true, + embedded = true ).render.when(props.isEmbedded), div(cls := "console-label")( i(cls := "fa fa-terminal"), @@ -68,7 +67,7 @@ object Console { ) ), div.withRef(consoleElement)( - cls := "output-console", + cls := "output-console", dangerouslySetInnerHtml := renderConsoleOutputs ) ), @@ -78,7 +77,7 @@ object Console { isStatusOk = true, save = props.run, setView = props.setView, - embedded = true, + embedded = true ).render.when(props.isEmbedded), displaySwitcher, div(cls := "console-label")( @@ -90,17 +89,16 @@ object Console { ) } - private val component = - ScalaComponent - .builder[Console]("Console") - .initialState(ConsoleState.default) - .render_P(render) - .componentDidUpdate( - scope => - Callback { - consoleElement.unsafeGet().scrollTop = consoleElement.unsafeGet().scrollHeight.toDouble - }.when_(scope.prevProps.isRunning) - ) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[Console]("Console") + .initialState(ConsoleState.default) + .render_P(render) + .componentDidUpdate(scope => + Callback { + consoleElement.unsafeGet().scrollTop = consoleElement.unsafeGet().scrollHeight.toDouble + }.when_(scope.prevProps.isRunning) + ) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/CopyModal.scala b/client/src/main/scala/com.olegych.scastie.client/components/CopyModal.scala index c30244677..8a3702e4c 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/CopyModal.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/CopyModal.scala @@ -55,7 +55,7 @@ object CopyModal { props.content ), div(onClick --> copyLink, title := "Copy to Clipboard", cls := "snippet-clip clipboard-copy")( - i(cls := "fa fa-clipboard") + i(cls := "fa fa-clipboard") ) ) ) diff --git a/client/src/main/scala/com.olegych.scastie.client/components/DesktopButton.scala b/client/src/main/scala/com.olegych.scastie.client/components/DesktopButton.scala index 56d91a99a..b3e0117ac 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/DesktopButton.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/DesktopButton.scala @@ -3,7 +3,6 @@ package client package components import japgolly.scalajs.react._ - import vdom.all._ final case class DesktopButton(forceDesktop: Reusable[Callback]) { @@ -11,8 +10,7 @@ final case class DesktopButton(forceDesktop: Reusable[Callback]) { } object DesktopButton { - implicit val reusability: Reusability[DesktopButton] = - Reusability.derive[DesktopButton] + implicit val reusability: Reusability[DesktopButton] = Reusability.derive[DesktopButton] private def render(props: DesktopButton): VdomElement = { li(title := "Go to desktop", cls := "btn", onClick --> props.forceDesktop)( @@ -21,10 +19,10 @@ object DesktopButton { ) } - private val component = - ScalaComponent - .builder[DesktopButton]("DesktopButton") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[DesktopButton]("DesktopButton") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/DownloadButton.scala b/client/src/main/scala/com.olegych.scastie.client/components/DownloadButton.scala index 36b57ebf6..405f59df9 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/DownloadButton.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/DownloadButton.scala @@ -3,7 +3,6 @@ package components import com.olegych.scastie.api.SnippetId import japgolly.scalajs.react._ - import vdom.all._ final case class DownloadButton(snippetId: SnippetId) { @@ -11,25 +10,30 @@ final case class DownloadButton(snippetId: SnippetId) { } object DownloadButton { - implicit val reusability: Reusability[DownloadButton] = - Reusability.derive[DownloadButton] + implicit val reusability: Reusability[DownloadButton] = Reusability.derive[DownloadButton] def render(props: DownloadButton): VdomElement = { - val url = props.snippetId.url + val url = props.snippetId.url val fullUrl = s"/api/download/$url" li( - a(href := fullUrl, download := url.replaceAll("/", "-") + ".zip", title := s"Download", role := "button", cls := "btn")( + a( + href := fullUrl, + download := url.replaceAll("/", "-") + ".zip", + title := s"Download", + role := "button", + cls := "btn" + )( i(cls := "fa fa-download"), span("Download") ) ) } - private val component = - ScalaComponent - .builder[DownloadButton]("DownloadButton") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[DownloadButton]("DownloadButton") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/EditorTopBar.scala b/client/src/main/scala/com.olegych.scastie.client/components/EditorTopBar.scala index 7fea28cba..6da746ad7 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/EditorTopBar.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/EditorTopBar.scala @@ -2,40 +2,43 @@ package com.olegych.scastie package client package components -import api.{SnippetId, User, ScalaTarget} - -import japgolly.scalajs.react._, vdom.all._, extra.router._, extra._ - -final case class EditorTopBar(clear: Reusable[Callback], - closeNewSnippetModal: Reusable[Callback], - closeEmbeddedModal: Reusable[Callback], - openEmbeddedModal: Reusable[Callback], - formatCode: Reusable[Callback], - newSnippet: Reusable[Callback], - openNewSnippetModal: Reusable[Callback], - save: Reusable[Callback], - toggleWorksheetMode: Reusable[Callback], - router: Option[RouterCtl[Page]], - inputsHasChanged: Boolean, - isDarkTheme: Boolean, - isNewSnippetModalClosed: Boolean, - isEmbeddedModalClosed: Boolean, - isRunning: Boolean, - isStatusOk: Boolean, - snippetId: Option[SnippetId], - user: Option[User], - view: StateSnapshot[View], - isWorksheetMode: Boolean, - metalsStatus: MetalsStatus, - toggleMetalsStatus: Reusable[Callback], - scalaTarget: ScalaTarget) { +import api.{ScalaTarget, SnippetId, User} +import extra._ +import extra.router._ +import japgolly.scalajs.react._ +import vdom.all._ + +final case class EditorTopBar( + clear: Reusable[Callback], + closeNewSnippetModal: Reusable[Callback], + closeEmbeddedModal: Reusable[Callback], + openEmbeddedModal: Reusable[Callback], + formatCode: Reusable[Callback], + newSnippet: Reusable[Callback], + openNewSnippetModal: Reusable[Callback], + save: Reusable[Callback], + toggleWorksheetMode: Reusable[Callback], + router: Option[RouterCtl[Page]], + inputsHasChanged: Boolean, + isDarkTheme: Boolean, + isNewSnippetModalClosed: Boolean, + isEmbeddedModalClosed: Boolean, + isRunning: Boolean, + isStatusOk: Boolean, + snippetId: Option[SnippetId], + user: Option[User], + view: StateSnapshot[View], + isWorksheetMode: Boolean, + metalsStatus: MetalsStatus, + toggleMetalsStatus: Reusable[Callback], + scalaTarget: ScalaTarget +) { @inline def render: VdomElement = EditorTopBar.component(this) } object EditorTopBar { - implicit val reusability: Reusability[EditorTopBar] = - Reusability.derive[EditorTopBar] + implicit val reusability: Reusability[EditorTopBar] = Reusability.derive[EditorTopBar] private def render(props: EditorTopBar): VdomElement = { def isDisabled = (cls := "disabled").when(props.view.value != View.Editor) @@ -45,7 +48,7 @@ object EditorTopBar { isStatusOk = props.isStatusOk, save = props.save, setView = Reusable.fn(view => props.view.setState(view)), - embedded = false, + embedded = false ).render val newButton = NewButton( @@ -76,43 +79,37 @@ object EditorTopBar { val metalsButton = MetalsStatusIndicator( props.metalsStatus, props.toggleMetalsStatus, - props.view.value, + props.view.value ).render - val downloadButton = - props.snippetId match { - case Some(sid) => - DownloadButton(snippetId = sid).render - case _ => - EmptyVdom - } - - val embeddedModalButton = - (props.snippetId, props.router) match { - case (Some(sid), Some(router)) => - val url = router.urlFor(Page.fromSnippetId(sid)).value - - val content = - s"""""".stripMargin - - val embeddedModal = - CopyModal( - isDarkTheme = props.isDarkTheme, - title = "Share your Code Snippet", - subtitle = "Copy and embed your code snippet", - modalId = "embed-modal", - content = content, - isClosed = props.isEmbeddedModalClosed, - close = props.closeEmbeddedModal - ).render - - li(title := s"Embed", role := "button", cls := "btn", onClick --> props.openEmbeddedModal)( - i(cls := "fa fa-code"), - span("Embed"), - embeddedModal - ) - case _ => EmptyVdom - } + val downloadButton = props.snippetId match { + case Some(sid) => DownloadButton(snippetId = sid).render + case _ => EmptyVdom + } + + val embeddedModalButton = (props.snippetId, props.router) match { + case (Some(sid), Some(router)) => + val url = router.urlFor(Page.fromSnippetId(sid)).value + + val content = s"""""".stripMargin + + val embeddedModal = CopyModal( + isDarkTheme = props.isDarkTheme, + title = "Share your Code Snippet", + subtitle = "Copy and embed your code snippet", + modalId = "embed-modal", + content = content, + isClosed = props.isEmbeddedModalClosed, + close = props.closeEmbeddedModal + ).render + + li(title := s"Embed", role := "button", cls := "btn", onClick --> props.openEmbeddedModal)( + i(cls := "fa fa-code"), + span("Embed"), + embeddedModal + ) + case _ => EmptyVdom + } nav(cls := "editor-topbar", isDisabled)( ul(cls := "editor-buttons")( @@ -123,15 +120,15 @@ object EditorTopBar { worksheetButton, downloadButton, embeddedModalButton, - metalsButton, + metalsButton ) ) } - private val component = - ScalaComponent - .builder[EditorTopBar]("EditorTopBar") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[EditorTopBar]("EditorTopBar") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/EmbeddedOverlay.scala b/client/src/main/scala/com.olegych.scastie.client/components/EmbeddedOverlay.scala index f54da0746..239255b28 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/EmbeddedOverlay.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/EmbeddedOverlay.scala @@ -6,10 +6,11 @@ import org.scalajs.dom import vdom.all._ final case class EmbeddedOverlay( - inputsHasChanged: Boolean, - embeddedSnippetId: Option[SnippetId], - serverUrl: Option[String], - save: Reusable[CallbackTo[Option[SnippetId]]]) { + inputsHasChanged: Boolean, + embeddedSnippetId: Option[SnippetId], + serverUrl: Option[String], + save: Reusable[CallbackTo[Option[SnippetId]]] +) { @inline def render: VdomElement = EmbeddedOverlay.component(this) } @@ -27,7 +28,7 @@ object EmbeddedOverlay { props.embeddedSnippetId match { case Some(snippetId) if !props.inputsHasChanged => open(snippetId) - case _ => props.save.asCBO.flatMap(open) + case _ => props.save.asCBO.flatMap(open) } } @@ -40,7 +41,7 @@ object EmbeddedOverlay { } private val component = ScalaFnComponent - .withHooks[EmbeddedOverlay] - .renderWithReuse(render) -} + .withHooks[EmbeddedOverlay] + .renderWithReuse(render) +} diff --git a/client/src/main/scala/com.olegych.scastie.client/components/FormatButton.scala b/client/src/main/scala/com.olegych.scastie.client/components/FormatButton.scala index 28a4a6357..5c75d2eab 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/FormatButton.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/FormatButton.scala @@ -14,19 +14,19 @@ object FormatButton { private def render(props: FormatButton): VdomElement = { li( title := s"Format Code (${EditorKeymaps.format.getName})", - role := "button", - cls := "btn", - onClick --> props.formatCode, + role := "button", + cls := "btn", + onClick --> props.formatCode )( i(cls := "fa fa-align-left"), span("Format") ) } - private val component = - ScalaComponent - .builder[FormatButton]("FormatButton") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[FormatButton]("FormatButton") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/HelpModal.scala b/client/src/main/scala/com.olegych.scastie.client/components/HelpModal.scala index d28bccaa1..e9c9d9866 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/HelpModal.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/HelpModal.scala @@ -1,37 +1,34 @@ package com.olegych.scastie.client.components +import com.olegych.scastie.client.components.editor.EditorKeymaps import japgolly.scalajs.react._ import vdom.all._ -import com.olegych.scastie.client.components.editor.EditorKeymaps final case class HelpModal(isDarkTheme: Boolean, isClosed: Boolean, close: Reusable[Callback]) { @inline def render: VdomElement = HelpModal.component(this) } object HelpModal { - implicit val reusability: Reusability[HelpModal] = - Reusability.derive[HelpModal] + implicit val reusability: Reusability[HelpModal] = Reusability.derive[HelpModal] private def render(props: HelpModal): VdomElement = { - def generateATag(url: String, text: String) = - a(href := url, target := "_blank", rel := "nofollow", text) + def generateATag(url: String, text: String) = a(href := url, target := "_blank", rel := "nofollow", text) - val scastieGithub = - generateATag("https://github.com/scalacenter/scastie", "scalacenter/scastie") + val scastieGithub = generateATag("https://github.com/scalacenter/scastie", "scalacenter/scastie") val sublime = generateATag( "https://sublime-text-unofficial-documentation.readthedocs.org/en/latest/reference/keyboard_shortcuts_osx.html", "keyboard shortcuts." ) - val scalafmtConfiguration = - generateATag("https://scalameta.org/scalafmt/docs/configuration.html#disabling-or-customizing-formatting", "configuration section") + val scalafmtConfiguration = generateATag( + "https://scalameta.org/scalafmt/docs/configuration.html#disabling-or-customizing-formatting", + "configuration section" + ) - val originalScastie = - generateATag("https://github.com/OlegYch/scastie_old", "GitHub") + val originalScastie = generateATag("https://github.com/OlegYch/scastie_old", "GitHub") - val gitter = - generateATag("https://gitter.im/scalacenter/scastie", "Gitter") + val gitter = generateATag("https://gitter.im/scalacenter/scastie", "Gitter") Modal( title = "Help about Scastie", @@ -41,8 +38,8 @@ object HelpModal { modalCss = TagMod(), modalId = "long-help", content = div(cls := "markdown-body")( - p( "Scastie is an interactive playground for Scala with support for sbt configuration."), - p( "Scastie editor supports Sublime Text ", sublime), + p("Scastie is an interactive playground for Scala with support for sbt configuration."), + p("Scastie editor supports Sublime Text ", sublime), h2(s"Save (${EditorKeymaps.saveOrUpdate.getName})"), p( "Run and save your code." @@ -58,7 +55,8 @@ object HelpModal { h2(s"Format (${EditorKeymaps.format.getName})"), p( "The code formatting is done by scalafmt. You can configure the formatting with comments in your code. Read the ", - scalafmtConfiguration), + scalafmtConfiguration + ), h2(s"Worksheet"), p( "Enabled by default, the Worksheet Mode gives the value and the type of each line of your program. You can also add HTML blocks such as: ", @@ -91,15 +89,14 @@ object HelpModal { "Your saved code fragments will appear here and you'll be able to delete or share them." ), h2("Feedback"), - p( "You can join our ", gitter, " channel and send issues."), + p("You can join our ", gitter, " channel and send issues."), h2("BuildInfo"), - p( "It's available on Github at ")( + p("It's available on Github at ")( scastieGithub, br, - " License: Apache 2", + " License: Apache 2" ), p( - "Scastie is an original idea from Aleh Aleshka (OlegYch) " )( originalScastie @@ -108,8 +105,8 @@ object HelpModal { ).render } - private val component = - ScalaFnComponent - .withHooks[HelpModal] - .renderWithReuse(render) + private val component = ScalaFnComponent + .withHooks[HelpModal] + .renderWithReuse(render) + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/LoginModal.scala b/client/src/main/scala/com.olegych.scastie.client/components/LoginModal.scala index e42935d1a..42b9185a7 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/LoginModal.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/LoginModal.scala @@ -3,10 +3,8 @@ package components import japgolly.scalajs.react._ import org.scalajs.dom - import vdom.all._ - final case class LoginModal( isDarkTheme: Boolean, isClosed: Boolean, @@ -20,9 +18,7 @@ object LoginModal { implicit val reusability: Reusability[LoginModal] = Reusability.derive[LoginModal] - - def login: Callback = - Callback(dom.window.location.pathname = "/login") + def login: Callback = Callback(dom.window.location.pathname = "/login") def render(props: LoginModal): VdomElement = { val theme = if (props.isDarkTheme) "dark" else "light" @@ -37,21 +33,21 @@ object LoginModal { content = TagMod( button(onClick --> (login >> props.close), cls := "github-login")( i(cls := "fa fa-github"), - "Continue with GitHub", + "Continue with GitHub" ), p( "By signing in, you agree to our ", - a(href := "#", onClick ==> (e => e.preventDefaultCB >> e.stopPropagationCB >> props.openPrivacyPolicyModal))("privacy policy"), + a(href := "#", onClick ==> (e => e.preventDefaultCB >> e.stopPropagationCB >> props.openPrivacyPolicyModal))( + "privacy policy" + ), "." ) ) ).render } - private val component = - ScalaFnComponent - .withHooks[LoginModal] - .renderWithReuse(render) - + private val component = ScalaFnComponent + .withHooks[LoginModal] + .renderWithReuse(render) } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/MainPanel.scala b/client/src/main/scala/com.olegych.scastie.client/components/MainPanel.scala index 8bccde4c6..dbfd2661e 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/MainPanel.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/MainPanel.scala @@ -1,9 +1,9 @@ package com.olegych.scastie.client.components +import com.olegych.scastie.client.components.editor.CodeEditor import com.olegych.scastie.client.ScastieBackend import com.olegych.scastie.client.ScastieState import com.olegych.scastie.client.View -import com.olegych.scastie.client.components.editor.CodeEditor import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.all._ @@ -13,8 +13,7 @@ final case class MainPanel(state: ScastieState, backend: ScastieBackend, props: } object MainPanel { - implicit val reusability: Reusability[MainPanel] = - Reusability.derive[MainPanel] + implicit val reusability: Reusability[MainPanel] = Reusability.derive[MainPanel] def render(in: MainPanel): VdomElement = { import in._ @@ -26,160 +25,148 @@ object MainPanel { val isStatusOk = state.status.isSbtOk - val embeddedMenu = - EmbeddedOverlay( - inputsHasChanged = state.inputsHasChanged, - embeddedSnippetId = props.embeddedSnippetId, - serverUrl = props.serverUrl, - save = backend.saveBlocking, - ).render.when(props.isEmbedded) - - val consoleCssForEditor = - (cls := "console-open").when(state.consoleState.consoleIsOpen) - - val codeSnippets = - (props.router, state.user) match { - case (Some(router), Some(user)) if state.view == View.CodeSnippets => - div(cls := "snippets-container inner-container")( - CodeSnippets( - isDarkTheme = state.isDarkTheme, - view = state.view, - user = user, - router = router, - isShareModalClosed = state.modalState.isShareModalClosed, - closeShareModal = backend.closeShareModal, - openShareModal = backend.openShareModal, - loadProfile = backend.loadProfile, - deleteSnippet = backend.deleteSnippet - ).render - ) - case _ => EmptyVdom - } - - val editor = - CodeEditor( - visible = visible(View.Editor), - isDarkTheme = state.isDarkTheme, - isPresentationMode = state.isPresentationMode, - isWorksheetMode = state.inputs.isWorksheetMode, - isEmbedded = props.isEmbedded, - showLineNumbers = state.showLineNumbers, - value = state.inputs.code, - attachedDoms = state.attachedDoms, - instrumentations = state.outputs.instrumentations, - compilationInfos = state.outputs.compilationInfos, - runtimeError = state.outputs.runtimeError, - saveOrUpdate = backend.saveOrUpdate, - clear = backend.clear, - openNewSnippetModal = backend.openNewSnippetModal, - toggleHelp = backend.toggleHelpModal, - toggleConsole = backend.toggleConsole, - toggleLineNumbers = backend.toggleLineNumbers, - togglePresentationMode = backend.togglePresentationMode, - formatCode = backend.formatCode, - codeChange = backend.codeChange, - target = state.inputs.target, - metalsStatus = state.metalsStatus, - setMetalsStatus = backend.setMetalsStatus, - dependencies = state.inputs.libraries - ).render - - val console = - Console( - isOpen = state.consoleState.consoleIsOpen, - isRunning = state.isRunning, - isEmbedded = props.isEmbedded, - consoleOutputs = state.outputs.consoleOutputs, - run = backend.run, - setView = backend.setViewReused, - close = backend.closeConsole, - open = backend.openConsole - ).render - - val buildSettings = - BuildSettings( - visible = visible(View.BuildSettings), - librariesFrom = state.inputs.librariesFrom, - isDarkTheme = state.isDarkTheme, - isBuildDefault = state.isBuildDefault, - isResetModalClosed = state.modalState.isResetModalClosed, - scalaTarget = state.inputs.target, - sbtConfigExtra = state.inputs.sbtConfigExtra, - sbtConfig = state.inputs.sbtConfigGenerated, - sbtPluginsConfig = state.inputs.sbtPluginsConfigGenerated, - setTarget = backend.setTarget, - closeResetModal = backend.closeResetModal, - resetBuild = backend.resetBuild, - openResetModal = backend.openResetModal, - sbtConfigChange = backend.sbtConfigChange, - removeScalaDependency = backend.removeScalaDependency, - updateDependencyVersion = backend.updateDependencyVersion, - addScalaDependency = backend.addScalaDependency - ).render - - val mobileBar = - MobileBar( - isRunning = state.isRunning, - isStatusOk = isStatusOk, - isDarkTheme = state.isDarkTheme, - save = backend.saveOrUpdate, - setView = backend.setViewReused, - clear = backend.clear, - isNewSnippetModalClosed = state.modalState.isNewSnippetModalClosed, - openNewSnippetModal = backend.openNewSnippetModal, - closeNewSnippetModal = backend.closeNewSnippetModal, - newSnippet = backend.newSnippet, - forceDesktop = backend.forceDesktop - ).render - - val topBar = - TopBar( - backend.viewSnapshot(state.view), - state.user, - backend.openLoginModal - ).render.unless(props.isEmbedded || state.isPresentationMode) - - val editorTopBar = - EditorTopBar( - clear = backend.clear, - closeNewSnippetModal = backend.closeNewSnippetModal, - closeEmbeddedModal = backend.closeEmbeddedModal, - openEmbeddedModal = backend.openEmbeddedModal, - formatCode = backend.formatCode, - newSnippet = backend.newSnippet, - openNewSnippetModal = backend.openNewSnippetModal, - save = backend.saveOrUpdate, - toggleWorksheetMode = backend.toggleWorksheetMode, - router = props.router, - inputsHasChanged = state.inputsHasChanged, - isDarkTheme = state.isDarkTheme, - isNewSnippetModalClosed = state.modalState.isNewSnippetModalClosed, - isEmbeddedModalClosed = state.modalState.isEmbeddedClosed, - isRunning = state.isRunning, - isStatusOk = isStatusOk, - snippetId = state.snippetId, - user = state.user, - view = backend.viewSnapshot(state.view), - isWorksheetMode = state.inputs.isWorksheetMode, - metalsStatus = state.metalsStatus, - toggleMetalsStatus = backend.toggleMetalsStatus, - scalaTarget = state.inputs.target - ).render.unless(props.isEmbedded || state.isPresentationMode) - - val statusView = - props.router match { - case Some(router) => - Status( - state = state.status, + val embeddedMenu = EmbeddedOverlay( + inputsHasChanged = state.inputsHasChanged, + embeddedSnippetId = props.embeddedSnippetId, + serverUrl = props.serverUrl, + save = backend.saveBlocking + ).render.when(props.isEmbedded) + + val consoleCssForEditor = (cls := "console-open").when(state.consoleState.consoleIsOpen) + + val codeSnippets = (props.router, state.user) match { + case (Some(router), Some(user)) if state.view == View.CodeSnippets => + div(cls := "snippets-container inner-container")( + CodeSnippets( + isDarkTheme = state.isDarkTheme, + view = state.view, + user = user, router = router, - isAdmin = state.user.exists(_.isAdmin), - inputs = state.inputs + isShareModalClosed = state.modalState.isShareModalClosed, + closeShareModal = backend.closeShareModal, + openShareModal = backend.openShareModal, + loadProfile = backend.loadProfile, + deleteSnippet = backend.deleteSnippet ).render - case _ => EmptyVdom - } - - val presentationModeClass = - (cls := "presentation-mode").when(state.isPresentationMode) + ) + case _ => EmptyVdom + } + + val editor = CodeEditor( + visible = visible(View.Editor), + isDarkTheme = state.isDarkTheme, + isPresentationMode = state.isPresentationMode, + isWorksheetMode = state.inputs.isWorksheetMode, + isEmbedded = props.isEmbedded, + showLineNumbers = state.showLineNumbers, + value = state.inputs.code, + attachedDoms = state.attachedDoms, + instrumentations = state.outputs.instrumentations, + compilationInfos = state.outputs.compilationInfos, + runtimeError = state.outputs.runtimeError, + saveOrUpdate = backend.saveOrUpdate, + clear = backend.clear, + openNewSnippetModal = backend.openNewSnippetModal, + toggleHelp = backend.toggleHelpModal, + toggleConsole = backend.toggleConsole, + toggleLineNumbers = backend.toggleLineNumbers, + togglePresentationMode = backend.togglePresentationMode, + formatCode = backend.formatCode, + codeChange = backend.codeChange, + target = state.inputs.target, + metalsStatus = state.metalsStatus, + setMetalsStatus = backend.setMetalsStatus, + dependencies = state.inputs.libraries + ).render + + val console = Console( + isOpen = state.consoleState.consoleIsOpen, + isRunning = state.isRunning, + isEmbedded = props.isEmbedded, + consoleOutputs = state.outputs.consoleOutputs, + run = backend.run, + setView = backend.setViewReused, + close = backend.closeConsole, + open = backend.openConsole + ).render + + val buildSettings = BuildSettings( + visible = visible(View.BuildSettings), + librariesFrom = state.inputs.librariesFrom, + isDarkTheme = state.isDarkTheme, + isBuildDefault = state.isBuildDefault, + isResetModalClosed = state.modalState.isResetModalClosed, + scalaTarget = state.inputs.target, + sbtConfigExtra = state.inputs.sbtConfigExtra, + sbtConfig = state.inputs.sbtConfigGenerated, + sbtPluginsConfig = state.inputs.sbtPluginsConfigGenerated, + setTarget = backend.setTarget, + closeResetModal = backend.closeResetModal, + resetBuild = backend.resetBuild, + openResetModal = backend.openResetModal, + sbtConfigChange = backend.sbtConfigChange, + removeScalaDependency = backend.removeScalaDependency, + updateDependencyVersion = backend.updateDependencyVersion, + addScalaDependency = backend.addScalaDependency + ).render + + val mobileBar = MobileBar( + isRunning = state.isRunning, + isStatusOk = isStatusOk, + isDarkTheme = state.isDarkTheme, + save = backend.saveOrUpdate, + setView = backend.setViewReused, + clear = backend.clear, + isNewSnippetModalClosed = state.modalState.isNewSnippetModalClosed, + openNewSnippetModal = backend.openNewSnippetModal, + closeNewSnippetModal = backend.closeNewSnippetModal, + newSnippet = backend.newSnippet, + forceDesktop = backend.forceDesktop + ).render + + val topBar = TopBar( + backend.viewSnapshot(state.view), + state.user, + backend.openLoginModal + ).render.unless(props.isEmbedded || state.isPresentationMode) + + val editorTopBar = EditorTopBar( + clear = backend.clear, + closeNewSnippetModal = backend.closeNewSnippetModal, + closeEmbeddedModal = backend.closeEmbeddedModal, + openEmbeddedModal = backend.openEmbeddedModal, + formatCode = backend.formatCode, + newSnippet = backend.newSnippet, + openNewSnippetModal = backend.openNewSnippetModal, + save = backend.saveOrUpdate, + toggleWorksheetMode = backend.toggleWorksheetMode, + router = props.router, + inputsHasChanged = state.inputsHasChanged, + isDarkTheme = state.isDarkTheme, + isNewSnippetModalClosed = state.modalState.isNewSnippetModalClosed, + isEmbeddedModalClosed = state.modalState.isEmbeddedClosed, + isRunning = state.isRunning, + isStatusOk = isStatusOk, + snippetId = state.snippetId, + user = state.user, + view = backend.viewSnapshot(state.view), + isWorksheetMode = state.inputs.isWorksheetMode, + metalsStatus = state.metalsStatus, + toggleMetalsStatus = backend.toggleMetalsStatus, + scalaTarget = state.inputs.target + ).render.unless(props.isEmbedded || state.isPresentationMode) + + val statusView = props.router match { + case Some(router) => Status( + state = state.status, + router = router, + isAdmin = state.user.exists(_.isAdmin), + inputs = state.inputs + ).render + case _ => EmptyVdom + } + + val presentationModeClass = (cls := "presentation-mode").when(state.isPresentationMode) div( cls := "main-panel", @@ -208,10 +195,10 @@ object MainPanel { } - private val component = - ScalaComponent - .builder[MainPanel]("MainPanel") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[MainPanel]("MainPanel") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/MetalsStatusIndicator.scala b/client/src/main/scala/com.olegych.scastie.client/components/MetalsStatusIndicator.scala index df738b681..bea6f199b 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/MetalsStatusIndicator.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/MetalsStatusIndicator.scala @@ -2,17 +2,16 @@ package com.olegych.scastie package client package components -import japgolly.scalajs.react._ - import scala.scalajs.js.annotation.JSImport -import vdom.all._ +import japgolly.scalajs.react._ import scalajs.js +import vdom.all._ final case class MetalsStatusIndicator( - metalsStatus: MetalsStatus, - toggleMetalsStatus: Reusable[Callback], - view: View, + metalsStatus: MetalsStatus, + toggleMetalsStatus: Reusable[Callback], + view: View ) { @inline def render: VdomElement = MetalsStatusIndicator.component(this) } @@ -21,16 +20,15 @@ final case class MetalsStatusIndicator( @js.native object MetalsLogo extends js.Any - object MetalsStatusIndicator { def metalsLogo: String = MetalsLogo.asInstanceOf[String] def getIndicatorIconClasses(status: MetalsStatus): String = { status match { - case MetalsLoading => "metals-loading fa-spinner fa-spin" - case MetalsDisabled => "metals-disabled fa-circle metals-disabled" - case MetalsReady => "metals-ready fa-circle metals-ready" - case _: NetworkError => "fa-exclamation-circle" + case MetalsLoading => "metals-loading fa-spinner fa-spin" + case MetalsDisabled => "metals-disabled fa-circle metals-disabled" + case MetalsReady => "metals-ready fa-circle metals-ready" + case _: NetworkError => "fa-exclamation-circle" case _: MetalsConfigurationError => "fa-exclamation-triangle" } } @@ -38,9 +36,9 @@ object MetalsStatusIndicator { private def render(props: MetalsStatusIndicator): VdomElement = { li( title := props.metalsStatus.info, - role := "button", - cls := "btn editor metals-status-indicator", - onClick --> props.toggleMetalsStatus, + role := "button", + cls := "btn editor metals-status-indicator", + onClick --> props.toggleMetalsStatus )( img(src := metalsLogo), span("Metals Status"), @@ -48,8 +46,8 @@ object MetalsStatusIndicator { ) } - private val component = - ScalaFnComponent - .withHooks[MetalsStatusIndicator] - .render(props => MetalsStatusIndicator.render(props)) + private val component = ScalaFnComponent + .withHooks[MetalsStatusIndicator] + .render(props => MetalsStatusIndicator.render(props)) + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/MobileBar.scala b/client/src/main/scala/com.olegych.scastie.client/components/MobileBar.scala index 1b8bb2c96..a6687a6ff 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/MobileBar.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/MobileBar.scala @@ -4,23 +4,24 @@ import com.olegych.scastie.client.View import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.all._ -final case class MobileBar(isRunning: Boolean, - isStatusOk: Boolean, - isDarkTheme: Boolean, - save: Reusable[Callback], - setView: View ~=> Callback, - isNewSnippetModalClosed: Boolean, - clear: Reusable[Callback], - openNewSnippetModal: Reusable[Callback], - closeNewSnippetModal: Reusable[Callback], - newSnippet: Reusable[Callback], - forceDesktop: Reusable[Callback]) { +final case class MobileBar( + isRunning: Boolean, + isStatusOk: Boolean, + isDarkTheme: Boolean, + save: Reusable[Callback], + setView: View ~=> Callback, + isNewSnippetModalClosed: Boolean, + clear: Reusable[Callback], + openNewSnippetModal: Reusable[Callback], + closeNewSnippetModal: Reusable[Callback], + newSnippet: Reusable[Callback], + forceDesktop: Reusable[Callback] +) { @inline def render: VdomElement = MobileBar.component(this) } object MobileBar { - implicit val reusability: Reusability[MobileBar] = - Reusability.derive[MobileBar] + implicit val reusability: Reusability[MobileBar] = Reusability.derive[MobileBar] private def render(props: MobileBar): VdomElement = { nav(cls := "editor-mobile")( @@ -30,7 +31,7 @@ object MobileBar { isStatusOk = props.isStatusOk, save = props.save, setView = props.setView, - embedded = false, + embedded = false ).render, NewButton( isDarkTheme = props.isDarkTheme, @@ -40,9 +41,9 @@ object MobileBar { newSnippet = props.newSnippet ).render, ClearButton( - clear = props.clear, - ).render, - //this doesn't work too well, better use browsers 'request desktop site' + clear = props.clear + ).render + // this doesn't work too well, better use browsers 'request desktop site' // DesktopButton( // forceDesktop = props.forceDesktop // ).render @@ -50,10 +51,10 @@ object MobileBar { ) } - private val component = - ScalaComponent - .builder[MobileBar]("MobileBar") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[MobileBar]("MobileBar") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/NewButton.scala b/client/src/main/scala/com.olegych.scastie.client/components/NewButton.scala index d8d312024..81850d052 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/NewButton.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/NewButton.scala @@ -3,27 +3,26 @@ package components import com.olegych.scastie.client.components.editor.EditorKeymaps import japgolly.scalajs.react._ - import vdom.all._ final case class NewButton( - isDarkTheme: Boolean, - isNewSnippetModalClosed: Boolean, - openNewSnippetModal: Reusable[Callback], - closeNewSnippetModal: Reusable[Callback], - newSnippet: Reusable[Callback]) { + isDarkTheme: Boolean, + isNewSnippetModalClosed: Boolean, + openNewSnippetModal: Reusable[Callback], + closeNewSnippetModal: Reusable[Callback], + newSnippet: Reusable[Callback] +) { @inline def render: VdomElement = NewButton.component(this) } object NewButton { - implicit val reusability: Reusability[NewButton] = - Reusability.derive[NewButton] + implicit val reusability: Reusability[NewButton] = Reusability.derive[NewButton] def render(props: NewButton): VdomElement = { li( title := s"New code snippet (${EditorKeymaps.openNewSnippetModal.getName})", - role := "button", + role := "button", onClick --> props.openNewSnippetModal, cls := "btn" )( @@ -42,10 +41,10 @@ object NewButton { ) } - private val component = - ScalaComponent - .builder[NewButton]("NewButton") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[NewButton]("NewButton") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/PrivacyPolicyModal.scala b/client/src/main/scala/com.olegych.scastie.client/components/PrivacyPolicyModal.scala index e249f38a6..1fb9ad493 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/PrivacyPolicyModal.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/PrivacyPolicyModal.scala @@ -1,17 +1,17 @@ package com.olegych.scastie.client.components +import scala.scalajs.js.annotation.JSImport + import japgolly.scalajs.react._ import scalajs.js import vdom.all._ -import scala.scalajs.js.annotation.JSImport final case class PrivacyPolicyModal(isDarkTheme: Boolean, isClosed: Boolean, close: Reusable[Callback]) { @inline def render: VdomElement = PrivacyPolicyModal.component(this) } object PrivacyPolicyModal { - implicit val reusability: Reusability[PrivacyPolicyModal] = - Reusability.derive[PrivacyPolicyModal] + implicit val reusability: Reusability[PrivacyPolicyModal] = Reusability.derive[PrivacyPolicyModal] @js.native @JSImport("@scastieRoot/privacy-policy.md", "html") @@ -31,8 +31,8 @@ object PrivacyPolicyModal { ).render } - private val component = - ScalaFnComponent - .withHooks[PrivacyPolicyModal] - .renderWithReuse(render) + private val component = ScalaFnComponent + .withHooks[PrivacyPolicyModal] + .renderWithReuse(render) + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/PrivacyPolicyPrompt.scala b/client/src/main/scala/com.olegych.scastie.client/components/PrivacyPolicyPrompt.scala index a49392056..6a0e830d9 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/PrivacyPolicyPrompt.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/PrivacyPolicyPrompt.scala @@ -1,30 +1,25 @@ package com.olegych.scastie.client package components - import japgolly.scalajs.react._ import org.scalajs.dom - import vdom.all._ - // scheduled for removal 2023-04-30 @deprecated("Scheduled for removal", "2023-04-30") final case class PrivacyPolicyPrompt( - isDarkTheme: Boolean, - isClosed: Boolean, - acceptPrivacyPolicy: Reusable[Callback], - refusePrivacyPolicy: Reusable[Callback], - openPrivacyPolicyModal: Reusable[Callback] - ) { + isDarkTheme: Boolean, + isClosed: Boolean, + acceptPrivacyPolicy: Reusable[Callback], + refusePrivacyPolicy: Reusable[Callback], + openPrivacyPolicyModal: Reusable[Callback] +) { @inline def render: VdomElement = PrivacyPolicyPrompt.component(this) } - @deprecated("Scheduled for removal", "2023-04-30") object PrivacyPolicyPrompt { - implicit val reusability: Reusability[PrivacyPolicyPrompt] = - Reusability.derive[PrivacyPolicyPrompt] + implicit val reusability: Reusability[PrivacyPolicyPrompt] = Reusability.derive[PrivacyPolicyPrompt] def reloadWindow = Reusable.always(Callback { dom.window.location.reload() }) @@ -40,11 +35,15 @@ object PrivacyPolicyPrompt { modalId = "privacy-policy-prompt", content = TagMod( div(cls := "modal-intro")( - p("""With the introduction of privacy policy to Scastie, you have to decide + p( + """With the introduction of privacy policy to Scastie, you have to decide | whether you want to keep your existing code snippets, or remove them all from our database. | By keeping the snippets, you acknowledge that you have read and agreed | to the privacy policy terms available """.stripMargin.stripLineEnd, - a(href := "#", onClick ==> (e => e.preventDefaultCB >> e.stopPropagationCB >> props.openPrivacyPolicyModal))( + a( + href := "#", + onClick ==> (e => e.preventDefaultCB >> e.stopPropagationCB >> props.openPrivacyPolicyModal) + )( "here" ), "." @@ -62,15 +61,13 @@ object PrivacyPolicyPrompt { ), p( """If you do not explicitly ask us to keep your snippets before April 30th 2023, we will delete them all.""" - ), + ) ), ul( li(onClick ==> (e => e.stopPropagationCB >> props.acceptPrivacyPolicy), cls := "btn")( "Keep my existing snippets" ), - li(onClick ==> (e => - e.stopPropagationCB >> props.refusePrivacyPolicy - ), cls := "btn")( + li(onClick ==> (e => e.stopPropagationCB >> props.refusePrivacyPolicy), cls := "btn")( "Delete my existing snippets" ) ) @@ -78,8 +75,8 @@ object PrivacyPolicyPrompt { ).render } - private val component = - ScalaFnComponent - .withHooks[PrivacyPolicyPrompt] - .renderWithReuse(render) + private val component = ScalaFnComponent + .withHooks[PrivacyPolicyPrompt] + .renderWithReuse(render) + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/PromptModal.scala b/client/src/main/scala/com.olegych.scastie.client/components/PromptModal.scala index b83cfbc76..94aff262a 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/PromptModal.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/PromptModal.scala @@ -3,26 +3,25 @@ package client package components import japgolly.scalajs.react._ - import vdom.all._ final case class PromptModal( - isDarkTheme: Boolean, - modalText: String, - modalId: String, - isClosed: Boolean, - close: Reusable[Callback], - actionText: String, - actionLabel: String, - action: Reusable[Callback]) { + isDarkTheme: Boolean, + modalText: String, + modalId: String, + isClosed: Boolean, + close: Reusable[Callback], + actionText: String, + actionLabel: String, + action: Reusable[Callback] +) { @inline def render: VdomElement = PromptModal.component(this) } object PromptModal { - implicit val reusability: Reusability[PromptModal] = - Reusability.derive[PromptModal] + implicit val reusability: Reusability[PromptModal] = Reusability.derive[PromptModal] private def render(props: PromptModal): VdomElement = { Modal( @@ -38,10 +37,7 @@ object PromptModal { props.actionText ), ul( - li(onClick ==> ( - e => e.stopPropagationCB >> props.action >> props.close - ), - cls := "btn")( + li(onClick ==> (e => e.stopPropagationCB >> props.action >> props.close), cls := "btn")( props.actionLabel ), li(onClick ==> (e => e.stopPropagationCB >> props.close), cls := "btn")( @@ -52,10 +48,10 @@ object PromptModal { ).render } - private val component = - ScalaComponent - .builder[PromptModal]("PrompModal") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[PromptModal]("PrompModal") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/RunButton.scala b/client/src/main/scala/com.olegych.scastie.client/components/RunButton.scala index d43d912e0..ecfaf1ecf 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/RunButton.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/RunButton.scala @@ -3,27 +3,34 @@ package components import com.olegych.scastie.client.components.editor.EditorKeymaps import japgolly.scalajs.react._ - import vdom.all._ -final case class RunButton(isRunning: Boolean, isStatusOk: Boolean, save: Reusable[Callback], setView: View ~=> Callback, embedded: Boolean) { +final case class RunButton( + isRunning: Boolean, + isStatusOk: Boolean, + save: Reusable[Callback], + setView: View ~=> Callback, + embedded: Boolean +) { @inline def render: VdomElement = RunButton.component(this) } object RunButton { - implicit val reusability: Reusability[RunButton] = - Reusability.derive[RunButton] + implicit val reusability: Reusability[RunButton] = Reusability.derive[RunButton] def render(props: RunButton): VdomElement = { if (!props.isRunning) { val runTitle = - if (props.isStatusOk) - s"Run (${EditorKeymaps.saveOrUpdate.getName})" - else - s"Run (${EditorKeymaps.saveOrUpdate.getName}) - warning: unknown status" - - li(onClick ==> { e => e.stopPropagationCB >> props.save }, role := "button", title := runTitle, cls := "btn run-button")( + if (props.isStatusOk) s"Run (${EditorKeymaps.saveOrUpdate.getName})" + else s"Run (${EditorKeymaps.saveOrUpdate.getName}) - warning: unknown status" + + li( + onClick ==> { e => e.stopPropagationCB >> props.save }, + role := "button", + title := runTitle, + cls := "btn run-button" + )( i(cls := "fa fa-play"), span("Run") ) @@ -35,10 +42,10 @@ object RunButton { } } - private val component = - ScalaComponent - .builder[RunButton]("RunButton") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[RunButton]("RunButton") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/ScaladexSearch.scala b/client/src/main/scala/com.olegych.scastie.client/components/ScaladexSearch.scala index 315fbe9a9..4b658f08f 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/ScaladexSearch.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/ScaladexSearch.scala @@ -1,79 +1,76 @@ package com.olegych.scastie.client.components +import scala.concurrent.Future + +import com.olegych.scastie.api._ import com.olegych.scastie.api.ScalaTarget.Jvm import com.olegych.scastie.api.ScalaTarget.Scala3 -import com.olegych.scastie.api._ import com.olegych.scastie.buildinfo.BuildInfo +import dom.{HTMLElement, HTMLInputElement} +import dom.ext.KeyCode import japgolly.scalajs.react._ import japgolly.scalajs.react.component.builder.Lifecycle.RenderScope import org.scalajs.dom import play.api.libs.json.Json - -import scala.concurrent.Future - -import vdom.all._ -import dom.ext.KeyCode -import dom.{HTMLInputElement, HTMLElement} -import scalajs.js.Thenable.Implicits._ import scalajs.concurrent.JSExecutionContext.Implicits.queue +import scalajs.js.Thenable.Implicits._ +import vdom.all._ final case class ScaladexSearch( - removeScalaDependency: ScalaDependency ~=> Callback, - updateDependencyVersion: (ScalaDependency, String) ~=> Callback, - addScalaDependency: (ScalaDependency, Project) ~=> Callback, - librariesFrom: Map[ScalaDependency, Project], - scalaTarget: ScalaTarget + removeScalaDependency: ScalaDependency ~=> Callback, + updateDependencyVersion: (ScalaDependency, String) ~=> Callback, + addScalaDependency: (ScalaDependency, Project) ~=> Callback, + librariesFrom: Map[ScalaDependency, Project], + scalaTarget: ScalaTarget ) { @inline def render: VdomElement = ScaladexSearch.component(this) } object ScaladexSearch { - implicit val propsReusability: Reusability[ScaladexSearch] = - Reusability.derive[ScaladexSearch] + implicit val propsReusability: Reusability[ScaladexSearch] = Reusability.derive[ScaladexSearch] - implicit val selectedReusability: Reusability[Selected] = - Reusability.derive[Selected] + implicit val selectedReusability: Reusability[Selected] = Reusability.derive[Selected] - implicit val stateReusability: Reusability[SearchState] = - Reusability.derive[SearchState] + implicit val stateReusability: Reusability[SearchState] = Reusability.derive[SearchState] private[ScaladexSearch] object SearchState { + def default: SearchState = SearchState( query = "", selectedIndex = 0, projects = List.empty, selecteds = List.empty ) + } private[ScaladexSearch] case class Selected( - project: Project, - release: ScalaDependency, - options: ReleaseOptions + project: Project, + release: ScalaDependency, + options: ReleaseOptions ) { def matches(p: Project, artifact: String) = p == project && release.artifact == artifact } private[ScaladexSearch] case class SearchState( - query: String, - selectedIndex: Int, - projects: List[(Project, ScalaTarget)], - selecteds: List[Selected] + query: String, + selectedIndex: Int, + projects: List[(Project, ScalaTarget)], + selecteds: List[Selected] ) { private val selectedProjectsArtifacts = selecteds .map(selected => (selected.project, selected.release.artifact, None, selected.release.target)) .toSet - val search: List[(Project, String, Option[String], ScalaTarget)] = - projects - .flatMap { - case (project, target) => project.artifacts.map(artifact => (project, artifact, None, target)) - } - .filter { projectAndArtifact => - !selectedProjectsArtifacts.contains(projectAndArtifact) - } + val search: List[(Project, String, Option[String], ScalaTarget)] = projects + .flatMap { case (project, target) => + project.artifacts.map(artifact => (project, artifact, None, target)) + } + .filter { projectAndArtifact => + !selectedProjectsArtifacts.contains(projectAndArtifact) + } def removeSelected(selected: Selected): SearchState = { copy(selecteds = selecteds.filterNot(_.release.matches(selected.release))) @@ -86,7 +83,10 @@ object ScaladexSearch { } def updateVersion(selected: Selected, version: String): SearchState = { - val updated = selected.copy(release = selected.release.copy(version = version), options = selected.options.copy(version = version)) + val updated = selected.copy( + release = selected.release.copy(version = version), + options = selected.options.copy(version = version) + ) copy( selecteds = selecteds.filterNot(_.release.matches(updated.release)) :+ updated ) @@ -99,31 +99,31 @@ object ScaladexSearch { def clearProjects: SearchState = { copy(projects = List()) } + } // private val scaladexBaseUrl = "http://localhost:8080" private val scaladexBaseUrl = "https://index.scala-lang.org" - private val scaladexApiUrl = scaladexBaseUrl + "/api" + private val scaladexApiUrl = scaladexBaseUrl + "/api" - private implicit val projectOrdering: Ordering[Project] = - Ordering.by { project: Project => - (project.organization, project.repository) - } + private implicit val projectOrdering: Ordering[Project] = Ordering.by { project: Project => + (project.organization, project.repository) + } private implicit val scalaDependenciesOrdering: Ordering[ScalaDependency] = Ordering.by { scalaDependency: ScalaDependency => scalaDependency.artifact } - private implicit val selectedOrdering: Ordering[Selected] = - Ordering.by { selected: Selected => - (selected.project, selected.release) - } + private implicit val selectedOrdering: Ordering[Selected] = Ordering.by { selected: Selected => + (selected.project, selected.release) + } private val projectListRef = Ref[HTMLElement] private val searchInputRef = Ref[HTMLInputElement] private[ScaladexSearch] class ScaladexSearchBackend(scope: BackendScope[ScaladexSearch, SearchState]) { + def keyDown(e: ReactKeyboardEventFromInput): Callback = { if (e.keyCode == KeyCode.Down || e.keyCode == KeyCode.Up) { @@ -146,18 +146,13 @@ object ScaladexSearch { ) } - def selectProject = - scope.modState( - s => - s.copy( - selectedIndex = clamp(s.search.size, s.selectedIndex + diff) - ) + def selectProject = scope.modState(s => + s.copy( + selectedIndex = clamp(s.search.size, s.selectedIndex + diff) ) + ) - def scrollToSelectedProject = - scope.state.map( - s => scrollToSelected(s.selectedIndex, s.search.size) - ) + def scrollToSelectedProject = scope.state.map(s => scrollToSelected(s.selectedIndex, s.search.size)) selectProject >> e.preventDefaultCB >> @@ -165,15 +160,15 @@ object ScaladexSearch { } else if (e.keyCode == KeyCode.Enter) { - def addArtifactIfInRange = - for { - state <- scope.state - props <- scope.props - _ <- if (0 <= state.selectedIndex && state.selectedIndex < state.search.size) { + def addArtifactIfInRange = for { + state <- scope.state + props <- scope.props + _ <- + if (0 <= state.selectedIndex && state.selectedIndex < state.search.size) { val (p, a, v, t) = state.search(state.selectedIndex) addArtifact((p, a, v), t, state) } else Callback.empty - } yield () + } yield () addArtifactIfInRange >> Callback(searchInputRef.unsafeGet().focus()) } else { @@ -181,57 +176,52 @@ object ScaladexSearch { } } - def addArtifact(projectAndArtifact: (Project, String, Option[String]), - target: ScalaTarget, - state: SearchState, - localOnly: Boolean = false): Callback = { + def addArtifact( + projectAndArtifact: (Project, String, Option[String]), + target: ScalaTarget, + state: SearchState, + localOnly: Boolean = false + ): Callback = { val (project, artifact, version) = projectAndArtifact if (state.selecteds.exists(_.matches(project, artifact))) Callback(()) - else - Callback.future { - fetchSelected(project, artifact, target, version).map { - case Some(selected) if !state.selecteds.exists(_.release.matches(selected.release)) => - def addScalaDependencyLocal = - scope.modState(_.addSelected(selected)) - - def addScalaDependencyBackend = - if (localOnly) Callback(()) else scope.props.flatMap(_.addScalaDependency((selected.release, selected.project))) - - addScalaDependencyBackend >> addScalaDependencyLocal - case _ => Callback(()) - } + else Callback.future { + fetchSelected(project, artifact, target, version).map { + case Some(selected) if !state.selecteds.exists(_.release.matches(selected.release)) => + def addScalaDependencyLocal = scope.modState(_.addSelected(selected)) + + def addScalaDependencyBackend = + if (localOnly) Callback(()) + else scope.props.flatMap(_.addScalaDependency((selected.release, selected.project))) + + addScalaDependencyBackend >> addScalaDependencyLocal + case _ => Callback(()) } + } } def removeSelected(selected: Selected): Callback = { - def removeDependencyLocal = - scope.modState(_.removeSelected(selected)) + def removeDependencyLocal = scope.modState(_.removeSelected(selected)) - def removeDependecyBackend = - scope.props.flatMap(_.removeScalaDependency(selected.release)) + def removeDependecyBackend = scope.props.flatMap(_.removeScalaDependency(selected.release)) removeDependecyBackend >> removeDependencyLocal } def updateVersion(selected: Selected)(e: ReactEventFromInput): Callback = { - val version = e.target.value - def updateDependencyVersionLocal = - scope.modState(_.updateVersion(selected, version)) + val version = e.target.value + def updateDependencyVersionLocal = scope.modState(_.updateVersion(selected, version)) - def updateDependencyVersionBackend = - scope.props.flatMap( - _.updateDependencyVersion((selected.release, version)) - ) + def updateDependencyVersionBackend = scope.props.flatMap( + _.updateDependencyVersion((selected.release, version)) + ) updateDependencyVersionBackend >> updateDependencyVersionLocal } - def selectIndex(index: Int): Callback = - scope.modState(s => s.copy(selectedIndex = index)) + def selectIndex(index: Int): Callback = scope.modState(s => s.copy(selectedIndex = index)) - def resetQuery: Callback = - scope.modState(s => s.copy(query = "", projects = Nil)) + def resetQuery: Callback = scope.modState(s => s.copy(query = "", projects = Nil)) def setQuery(e: ReactEventFromInput): Callback = { e.extract(_.target.value) { value => @@ -247,7 +237,7 @@ object ScaladexSearch { val q = toQuery(t.scaladexRequest + ("q" -> searchState.query)) for { response <- dom.fetch(scaladexApiUrl + "/search" + q) - text <- response.text() + text <- response.text() } yield { Json.fromJson[List[Project]](Json.parse(text)).asOpt.getOrElse(Nil).map(_ -> t) } @@ -256,8 +246,7 @@ object ScaladexSearch { val projsForThisTarget = queryAndParse(target) val projects: Future[List[(Project, ScalaTarget)]] = target match { // If scala3 but no scala 3 versions available, offer 2.13 artifacts - case Scala3(_) => - projsForThisTarget.flatMap { ls => + case Scala3(_) => projsForThisTarget.flatMap { ls => queryAndParse(Jvm(BuildInfo.latest213)) .map(arts213 => ls ::: arts213) } @@ -271,26 +260,25 @@ object ScaladexSearch { } for { - props <- scope.props + props <- scope.props searchState <- scope.state - _ <- fetch(props.scalaTarget, searchState) + _ <- fetch(props.scalaTarget, searchState) } yield () } - private def toQuery(in: Map[String, String]): String = - in.map { case (k, v) => s"$k=$v" }.mkString("?", "&", "") + private def toQuery(in: Map[String, String]): String = in.map { case (k, v) => s"$k=$v" }.mkString("?", "&", "") private def fetchSelected(project: Project, artifact: String, target: ScalaTarget, version: Option[String]) = { val query = toQuery( Map( "organization" -> project.organization, - "repository" -> project.repository + "repository" -> project.repository ) ++ target.scaladexRequest ) for { response <- dom.fetch(scaladexApiUrl + "/project" + query) - text <- response.text() + text <- response.text() } yield { Json.fromJson[ReleaseOptions](Json.parse(text)).asOpt.map { options => { @@ -300,46 +288,45 @@ object ScaladexSearch { groupId = options.groupId, artifact = artifact, target = target, - version = version.getOrElse(options.version), + version = version.getOrElse(options.version) ), - options = options, + options = options ) } } } } + } private def render( - scope: RenderScope[ScaladexSearch, SearchState, ScaladexSearchBackend], - props: ScaladexSearch, - searchState: SearchState + scope: RenderScope[ScaladexSearch, SearchState, ScaladexSearchBackend], + props: ScaladexSearch, + searchState: SearchState ): VdomElement = { - def selectedIndex(index: Int, selected: Int) = - (cls := "selected").when(index == selected) - - def renderProject(project: Project, - artifact: String, - scalaTarget: ScalaTarget, - selected: TagMod, - handlers: TagMod = EmptyVdom, - remove: TagMod = EmptyVdom, - options: TagMod = EmptyVdom) = { + def selectedIndex(index: Int, selected: Int) = (cls := "selected").when(index == selected) + + def renderProject( + project: Project, + artifact: String, + scalaTarget: ScalaTarget, + selected: TagMod, + handlers: TagMod = EmptyVdom, + remove: TagMod = EmptyVdom, + options: TagMod = EmptyVdom + ) = { import project._ val common = TagMod(title := organization, cls := "logo") - val artifact2 = - artifact - .replace(project.repository + "-", "") - .replace(project.repository, "") + val artifact2 = artifact + .replace(project.repository + "-", "") + .replace(project.repository, "") val label = - if (project.repository != artifact) - s"${project.repository} / $artifact2" + if (project.repository != artifact) s"${project.repository} / $artifact2" else artifact - val scaladexLink = - s"https://scaladex.scala-lang.org/$organization/$repository/$artifact" + val scaladexLink = s"https://scaladex.scala-lang.org/$organization/$repository/$artifact" div(cls := "result", selected, handlers)( a(cls := "scaladexresult", href := scaladexLink, target := "_blank")( @@ -357,7 +344,7 @@ object ScaladexSearch { if (scalaTarget.binaryScalaVersion != props.scalaTarget.binaryScalaVersion) span(cls := "artifact")(s"(Scala ${scalaTarget.binaryScalaVersion} artifacts)") else "" - ), + ) ) } @@ -366,7 +353,7 @@ object ScaladexSearch { select( selected.options.versions.reverse.map(v => option(value := v)(v)).toTagMod, value := selected.release.version, - onChange ==> scope.backend.updateVersion(selected), + onChange ==> scope.backend.updateVersion(selected) ) ) } @@ -404,9 +391,9 @@ object ScaladexSearch { added, div(cls := "search-input")( input.search.withRef(searchInputRef)( - cls := "search-query", + cls := "search-query", placeholder := "Search for 'cats'", - value := searchState.query, + value := searchState.query, onChange ==> scope.backend.setQuery, onKeyDown ==> scope.backend.keyDown ), @@ -417,34 +404,33 @@ object ScaladexSearch { ) ), div.withRef(projectListRef)(cls := "results", displayResults)( - searchState.search.zipWithIndex.map { - case ((project, artifact, version, target), index) => - renderProject( - project, - artifact, - target, - selected = selectedIndex(index, searchState.selectedIndex), - handlers = TagMod( - onClick --> scope.backend.addArtifact((project, artifact, version), target, scope.state), - onMouseOver --> scope.backend.selectIndex(index) - ) + searchState.search.zipWithIndex.map { case ((project, artifact, version, target), index) => + renderProject( + project, + artifact, + target, + selected = selectedIndex(index, searchState.selectedIndex), + handlers = TagMod( + onClick --> scope.backend.addArtifact((project, artifact, version), target, scope.state), + onMouseOver --> scope.backend.selectIndex(index) ) + ) }.toTagMod ) ) } - private val component = - ScalaComponent - .builder[ScaladexSearch]("Scaladex Search") - .initialState(SearchState.default) - .backend(new ScaladexSearchBackend(_)) - .renderPS(render) - .componentWillReceiveProps { x => - Callback.traverse(x.nextProps.librariesFrom.toList.sortBy(_._1.artifact)) { lib => - x.backend.addArtifact((lib._2, lib._1.artifact, Some(lib._1.version)), lib._1.target, x.state, localOnly = true) - } + private val component = ScalaComponent + .builder[ScaladexSearch]("Scaladex Search") + .initialState(SearchState.default) + .backend(new ScaladexSearchBackend(_)) + .renderPS(render) + .componentWillReceiveProps { x => + Callback.traverse(x.nextProps.librariesFrom.toList.sortBy(_._1.artifact)) { lib => + x.backend.addArtifact((lib._2, lib._1.artifact, Some(lib._1.version)), lib._1.target, x.state, localOnly = true) } - .configure(Reusability.shouldComponentUpdate) - .build + } + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/Scastie.scala b/client/src/main/scala/com.olegych.scastie.client/components/Scastie.scala index 2982bc82c..ee09ffced 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/Scastie.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/Scastie.scala @@ -1,5 +1,7 @@ package com.olegych.scastie.client.components +import java.util.UUID + import com.olegych.scastie.api._ import com.olegych.scastie.client._ import japgolly.scalajs.react._ @@ -9,44 +11,40 @@ import japgolly.scalajs.react.vdom.all._ import org.scalajs.dom import org.scalajs.dom.HTMLScriptElement -import java.util.UUID - final case class Scastie( - router: Option[RouterCtl[Page]], - private val scastieId: UUID, - private val snippetId: Option[SnippetId], - private val oldSnippetId: Option[Int], - private val embedded: Option[EmbeddedOptions], - private val targetType: Option[ScalaTargetType], - private val tryLibrary: Option[(ScalaDependency, Project)], - private val code: Option[String], - private val inputs: Option[Inputs], + router: Option[RouterCtl[Page]], + private val scastieId: UUID, + private val snippetId: Option[SnippetId], + private val oldSnippetId: Option[Int], + private val embedded: Option[EmbeddedOptions], + private val targetType: Option[ScalaTargetType], + private val tryLibrary: Option[(ScalaDependency, Project)], + private val code: Option[String], + private val inputs: Option[Inputs] ) { @inline def render = Scastie.component(serverUrl, scastieId)(this) def serverUrl: Option[String] = embedded.map(_.serverUrl) - def isEmbedded: Boolean = embedded.isDefined - //todo not sure how is it different from regular snippet id + def isEmbedded: Boolean = embedded.isDefined + // todo not sure how is it different from regular snippet id def embeddedSnippetId: Option[SnippetId] = embedded.flatMap(_.snippetId) } object Scastie { - implicit val scastieReuse: Reusability[Scastie] = - Reusability.derive[Scastie] - - def default(router: RouterCtl[Page]): Scastie = - Scastie( - scastieId = UUID.randomUUID(), - router = Some(router), - snippetId = None, - oldSnippetId = None, - embedded = None, - targetType = None, - tryLibrary = None, - code = None, - inputs = None, - ) + implicit val scastieReuse: Reusability[Scastie] = Reusability.derive[Scastie] + + def default(router: RouterCtl[Page]): Scastie = Scastie( + scastieId = UUID.randomUUID(), + router = Some(router), + snippetId = None, + oldSnippetId = None, + embedded = None, + targetType = None, + tryLibrary = None, + code = None, + inputs = None + ) private def setTitle(state: ScastieState, props: Scastie) = { def scastieCode = if (state.inputs.code.isEmpty) "Scastie" else state.inputs.code + " - Scastie" @@ -62,16 +60,15 @@ object Scastie { } private def render( - scope: RenderScope[Scastie, ScastieState, ScastieBackend], - props: Scastie, - state: ScastieState + scope: RenderScope[Scastie, ScastieState, ScastieBackend], + props: Scastie, + state: ScastieState ): VdomElement = { val theme = if (state.isDarkTheme) "dark" else "light" - val forceDesktopClass = - (cls := "force-desktop").when(state.isDesktopForced) + val forceDesktopClass = (cls := "force-desktop").when(state.isDesktopForced) div(cls := s"app $theme", forceDesktopClass)( SideBar( @@ -97,70 +94,61 @@ object Scastie { isDarkTheme = state.isDarkTheme, isClosed = state.modalState.isLoginModalClosed, close = scope.backend.closeLoginModal, - openPrivacyPolicyModal = scope.backend.openPrivacyPolicyModal, + openPrivacyPolicyModal = scope.backend.openPrivacyPolicyModal ).render, PrivacyPolicyPrompt( isDarkTheme = state.isDarkTheme, isClosed = state.modalState.isPrivacyPolicyPromptClosed, acceptPrivacyPolicy = scope.backend.acceptPolicy, refusePrivacyPolicy = scope.backend.refusePrivacyPolicy, - openPrivacyPolicyModal = scope.backend.openPrivacyPolicyModal, + openPrivacyPolicyModal = scope.backend.openPrivacyPolicyModal ).render, PrivacyPolicyModal( isDarkTheme = state.isDarkTheme, isClosed = state.modalState.isPrivacyPolicyModalClosed, close = scope.backend.closePrivacyPolicyModal - ).render, + ).render ) } private def start(props: Scastie, backend: ScastieBackend): Callback = { - val initialState = - props.embedded match { - case None => { - props.snippetId match { - case Some(snippetId) => - backend.loadSnippet(snippetId) - - case None => - props.oldSnippetId match { - case Some(id) => - backend.loadOldSnippet(id) - - case None => - Callback.traverseOption(LocalStorage.load) { state => - backend.scope.modState { _ => - state - .setRunning(false) - .setCleanInputs - .resetScalajs - } + val initialState = props.embedded match { + case None => { + props.snippetId match { + case Some(snippetId) => backend.loadSnippet(snippetId) + + case None => props.oldSnippetId match { + case Some(id) => backend.loadOldSnippet(id) + + case None => Callback.traverseOption(LocalStorage.load) { state => + backend.scope.modState { _ => + state + .setRunning(false) + .setCleanInputs + .resetScalajs } - } - } + } + } } - case Some(embededOptions) => { - val setInputs = - (embededOptions.snippetId, embededOptions.inputs) match { - case (Some(snippetId), _) => - backend.loadSnippet(snippetId) - - case (_, Some(inputs)) => - backend.scope.modState(_.setInputs(inputs)) + } + case Some(embededOptions) => { + val setInputs = (embededOptions.snippetId, embededOptions.inputs) match { + case (Some(snippetId), _) => backend.loadSnippet(snippetId) - case _ => Callback.empty - } + case (_, Some(inputs)) => backend.scope.modState(_.setInputs(inputs)) - val setTheme = - embededOptions.theme match { - case Some("dark") => backend.scope.modState(_.setTheme(dark = true)) - case Some("light") => backend.scope.modState(_.setTheme(dark = false)) - case _ => Callback(()) - } + case _ => Callback.empty + } - setInputs >> setTheme + val setTheme = embededOptions.theme match { + case Some("dark") => backend.scope.modState(_.setTheme(dark = true)) + case Some("light") => backend.scope.modState(_.setTheme(dark = false)) + case _ => Callback(()) } + + setInputs >> setTheme } + } initialState >> backend.loadUser } @@ -190,20 +178,19 @@ object Scastie { val scalaJsRunScriptElement = createScript(scalaJsRunId) println("== Running Scala.js ==") - val scalaJsScript = - s"""|try { - | var main = new ScastiePlaygroundMain(); - | scastie.ClientMain.signal( - | main.result, - | main.attachedElements, - | "$scastieId" - | ); - |} catch (e) { - | scastie.ClientMain.error( - | e, - | "$scastieId" - | ); - |}""".stripMargin + val scalaJsScript = s"""|try { + | var main = new ScastiePlaygroundMain(); + | scastie.ClientMain.signal( + | main.result, + | main.attachedElements, + | "$scastieId" + | ); + |} catch (e) { + | scastie.ClientMain.error( + | e, + | "$scastieId" + | ); + |}""".stripMargin scalaJsRunScriptElement.innerHTML = scalaJsScript } @@ -215,8 +202,8 @@ object Scastie { state.snippetState.scalaJsContent.foreach { content => println("== Loading Scala.js! ==") val scalaJsScriptElement = createScript(scalaJsId) - val fixedContent = playgroundMainRegex.replaceAllIn(content, "var ScastiePlaygroundMain") - val scriptTextNode = dom.document.createTextNode(fixedContent) + val fixedContent = playgroundMainRegex.replaceAllIn(content, "var ScastiePlaygroundMain") + val scriptTextNode = dom.document.createTextNode(fixedContent) scalaJsScriptElement.appendChild(scriptTextNode) runScalaJs() } @@ -224,87 +211,83 @@ object Scastie { } } - private def component(serverUrl: Option[String], scastieId: UUID) = - ScalaComponent - .builder[Scastie]("Scastie") - .initialStateFromProps { props => - val state = { - val scheme = LocalStorage.load.map(_.isDarkTheme) - val loadedState = ScastieState.default(props.isEmbedded) - val loadedStateWithScheme = scheme.map(theme => loadedState.copy(isDarkTheme = theme)).getOrElse(loadedState) - if (!props.isEmbedded) { - loadedStateWithScheme - } else { - loadedStateWithScheme.setCleanInputs.clearOutputs - } - } - - val state1 = - props.targetType match { - case Some(targetType) => { - val state0 = - state.setTarget(targetType.defaultScalaTarget) - - if (targetType == ScalaTargetType.Scala3) { - state0.setCode(ScalaTarget.Scala3.defaultCode) - } else { - state0 - } - } - case _ => state - } - - val state2 = props.tryLibrary match { - case Some(dependency) => - state1 - .setTarget(dependency._1.target) - .addScalaDependency(dependency._1, dependency._2) - case _ => state1 + private def component(serverUrl: Option[String], scastieId: UUID) = ScalaComponent + .builder[Scastie]("Scastie") + .initialStateFromProps { props => + val state = { + val scheme = LocalStorage.load.map(_.isDarkTheme) + val loadedState = ScastieState.default(props.isEmbedded) + val loadedStateWithScheme = scheme.map(theme => loadedState.copy(isDarkTheme = theme)).getOrElse(loadedState) + if (!props.isEmbedded) { + loadedStateWithScheme + } else { + loadedStateWithScheme.setCleanInputs.clearOutputs } + } - val state3 = props.code match { - case Some(code) => state2.setCode(code) - case _ => state2 - } + val state1 = props.targetType match { + case Some(targetType) => { + val state0 = state.setTarget(targetType.defaultScalaTarget) - props.inputs match { - case Some(inputs) => state3.setInputs(inputs) - case _ => state3 + if (targetType == ScalaTargetType.Scala3) { + state0.setCode(ScalaTarget.Scala3.defaultCode) + } else { + state0 + } } + case _ => state } - .backend(ScastieBackend(scastieId, serverUrl, _)) - .renderPS(render) - .componentWillMount { current => - start(current.props, current.backend) >> - setTitle(current.state, current.props) >> - current.backend.closeNewSnippetModal >> - current.backend.closeResetModal >> - current.backend.connectStatus.when_(!current.props.isEmbedded) - } - .componentWillUnmount { current => - current.backend.disconnectStatus.when_(!current.props.isEmbedded) >> - current.backend.unsubscribeGlobal + + val state2 = props.tryLibrary match { + case Some(dependency) => state1 + .setTarget(dependency._1.target) + .addScalaDependency(dependency._1, dependency._2) + case _ => state1 } - .componentDidUpdate { scope => - setTitle(scope.prevState, scope.currentProps) >> - scope.modState(_.scalaJsScriptLoaded) >> - executeScalaJs(scastieId, scope.currentState) + + val state3 = props.code match { + case Some(code) => state2.setCode(code) + case _ => state2 } - .componentWillReceiveProps { scope => - val next = scope.nextProps.snippetId - val current = scope.currentProps.snippetId - val state = scope.state - val backend = scope.backend - - val loadSnippet: CallbackOption[Unit] = - for { - snippetId <- CallbackOption.option(next) - _ <- CallbackOption.require(next != current) - _ <- backend.loadSnippet(snippetId).toCBO >> backend.setView(View.Editor) - } yield () - - setTitle(state, scope.nextProps) >> loadSnippet.toCallback + + props.inputs match { + case Some(inputs) => state3.setInputs(inputs) + case _ => state3 } - .configure(Reusability.shouldComponentUpdate) - .build + } + .backend(ScastieBackend(scastieId, serverUrl, _)) + .renderPS(render) + .componentWillMount { current => + start(current.props, current.backend) >> + setTitle(current.state, current.props) >> + current.backend.closeNewSnippetModal >> + current.backend.closeResetModal >> + current.backend.connectStatus.when_(!current.props.isEmbedded) + } + .componentWillUnmount { current => + current.backend.disconnectStatus.when_(!current.props.isEmbedded) >> + current.backend.unsubscribeGlobal + } + .componentDidUpdate { scope => + setTitle(scope.prevState, scope.currentProps) >> + scope.modState(_.scalaJsScriptLoaded) >> + executeScalaJs(scastieId, scope.currentState) + } + .componentWillReceiveProps { scope => + val next = scope.nextProps.snippetId + val current = scope.currentProps.snippetId + val state = scope.state + val backend = scope.backend + + val loadSnippet: CallbackOption[Unit] = for { + snippetId <- CallbackOption.option(next) + _ <- CallbackOption.require(next != current) + _ <- backend.loadSnippet(snippetId).toCBO >> backend.setView(View.Editor) + } yield () + + setTitle(state, scope.nextProps) >> loadSnippet.toCallback + } + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/SideBar.scala b/client/src/main/scala/com.olegych.scastie.client/components/SideBar.scala index 5c7a93fc3..48741fe47 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/SideBar.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/SideBar.scala @@ -2,14 +2,13 @@ package com.olegych.scastie package client package components -import com.olegych.scastie.api._ +import scala.scalajs.js -import japgolly.scalajs.react._ -import vdom.all._ +import com.olegych.scastie.api._ import extra._ - -import scala.scalajs.js +import japgolly.scalajs.react._ import js.annotation._ +import vdom.all._ @JSImport("@resources/images/icon-scastie.png", JSImport.Default) @js.native @@ -20,24 +19,25 @@ object ScastieLogo extends js.Any object Placeholder extends js.Any object Assets { - def logo: String = ScastieLogo.asInstanceOf[String] + def logo: String = ScastieLogo.asInstanceOf[String] def placeholder: String = Placeholder.asInstanceOf[String] } -final case class SideBar(isDarkTheme: Boolean, - status: StatusState, - inputs: Inputs, - toggleTheme: Reusable[Callback], - view: StateSnapshot[View], - openHelpModal: Reusable[Callback], - openPrivacyPolicyModal: Reusable[Callback]) { +final case class SideBar( + isDarkTheme: Boolean, + status: StatusState, + inputs: Inputs, + toggleTheme: Reusable[Callback], + view: StateSnapshot[View], + openHelpModal: Reusable[Callback], + openPrivacyPolicyModal: Reusable[Callback] +) { @inline def render: VdomElement = SideBar.component(this) } object SideBar { - implicit val reusability: Reusability[SideBar] = - Reusability.derive[SideBar] + implicit val reusability: Reusability[SideBar] = Reusability.derive[SideBar] private def render(props: SideBar): VdomElement = { val toggleThemeLabel = @@ -48,11 +48,15 @@ object SideBar { if (props.isDarkTheme) "fa fa-sun-o" else "fa fa-moon-o" - val themeButton = - li(onClick --> props.toggleTheme, role := "button", title := s"Select $toggleThemeLabel Theme (F2)", cls := "btn")( - i(cls := s"fa $selectedIcon"), - span(toggleThemeLabel) - ) + val themeButton = li( + onClick --> props.toggleTheme, + role := "button", + title := s"Select $toggleThemeLabel Theme (F2)", + cls := "btn" + )( + i(cls := s"fa $selectedIcon"), + span(toggleThemeLabel) + ) val privacyPolicyButton = li(onClick --> props.openPrivacyPolicyModal, role := "button", title := "Show privacy policy", cls := "btn")( @@ -60,26 +64,26 @@ object SideBar { span("Privacy Policy") ) - val helpButton = - li(onClick --> props.openHelpModal, role := "button", title := "Show help Menu", cls := "btn")( - i(cls := "fa fa-question-circle"), - span("Help") - ) + val helpButton = li(onClick --> props.openHelpModal, role := "button", title := "Show help Menu", cls := "btn")( + i(cls := "fa fa-question-circle"), + span("Help") + ) val runnersStatusButton = { - val (statusIcon, statusClass, statusLabel) = - props.status.sbtRunnerCount match { - case None => - ("fa-times-circle", "status-unknown", "Unknown") + val (statusIcon, statusClass, statusLabel) = props.status.sbtRunnerCount match { + case None => ("fa-times-circle", "status-unknown", "Unknown") - case Some(0) => - ("fa-times-circle", "status-down", "Down") + case Some(0) => ("fa-times-circle", "status-down", "Down") - case Some(_) => - ("fa-check-circle", "status-up", "Up") - } + case Some(_) => ("fa-check-circle", "status-up", "Up") + } - li(onClick --> props.view.setState(View.Status), role := "button", title := "Show runners status", cls := s"btn $statusClass")( + li( + onClick --> props.view.setState(View.Status), + role := "button", + title := "Show runners status", + cls := s"btn $statusClass" + )( i(cls := s"fa $statusIcon"), span(statusLabel) ) @@ -121,10 +125,10 @@ object SideBar { ) } - private val component = - ScalaComponent - .builder[SideBar]("SideBar") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[SideBar]("SideBar") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/Status.scala b/client/src/main/scala/com.olegych.scastie.client/components/Status.scala index ffcb2b0d0..95dc9323b 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/Status.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/Status.scala @@ -4,10 +4,9 @@ import com.olegych.scastie.api.Inputs import com.olegych.scastie.api.TaskId import com.olegych.scastie.client.Page import com.olegych.scastie.client.StatusState +import extra.router._ import japgolly.scalajs.react._ - import vdom.all._ -import extra.router._ final case class Status(state: StatusState, router: RouterCtl[Page], isAdmin: Boolean, inputs: Inputs) { @inline def render: VdomElement = Status.component(this) @@ -15,8 +14,7 @@ final case class Status(state: StatusState, router: RouterCtl[Page], isAdmin: Bo object Status { - implicit val reusability: Reusability[Status] = - Reusability.derive[Status] + implicit val reusability: Reusability[Status] = Reusability.derive[Status] def render(props: Status): VdomElement = { def renderSbtTask(tasks: Vector[TaskId]): VdomElement = { @@ -25,13 +23,12 @@ object Status { div("No Task Running") } else { ul( - tasks.zipWithIndex.map { - case (TaskId(snippetId), j) => - li(key := snippetId.toString)( - props.router.link(Page.fromSnippetId(snippetId))( - s"Task $j" - ) + tasks.zipWithIndex.map { case (TaskId(snippetId), j) => + li(key := snippetId.toString)( + props.router.link(Page.fromSnippetId(snippetId))( + s"Task $j" ) + ) }.toTagMod ) } @@ -51,31 +48,28 @@ object Status { span(cls := "runner " + cssConfig)(label) } - val sbtRunnersStatus = - props.state.sbtRunners match { - case Some(sbtRunners) => - div( - h1("Sbt Runners"), - ul( - sbtRunners.zipWithIndex.map { - case (sbtRunner, i) => - li(key := i)( - renderConfiguration(sbtRunner.config), - renderSbtTask(sbtRunner.tasks) - ) - }.toTagMod - ) + val sbtRunnersStatus = props.state.sbtRunners match { + case Some(sbtRunners) => div( + h1("Sbt Runners"), + ul( + sbtRunners.zipWithIndex.map { case (sbtRunner, i) => + li(key := i)( + renderConfiguration(sbtRunner.config), + renderSbtTask(sbtRunner.tasks) + ) + }.toTagMod ) - case _ => div() - } + ) + case _ => div() + } div(sbtRunnersStatus) } - private val component = - ScalaComponent - .builder[Status]("Status") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[Status]("Status") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/TargetSelector.scala b/client/src/main/scala/com.olegych.scastie.client/components/TargetSelector.scala index bca1c389a..62dec25f5 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/TargetSelector.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/TargetSelector.scala @@ -2,7 +2,6 @@ package com.olegych.scastie.client.components import com.olegych.scastie.api._ import japgolly.scalajs.react._ - import vdom.all._ case class TargetSelector(scalaTarget: ScalaTarget, onChange: ScalaTarget ~=> Callback) { @@ -28,27 +27,27 @@ object TargetSelector { } } - val targetSelector = - ScalaFnComponent - .withHooks[TargetSelector] - .render(props => { - div( - ul(cls := "target")( - targetTypes.map { targetType => - val targetLabel = labelFor(targetType) - li( - input( - `type` := "radio", - id := targetLabel, - value := targetLabel, - name := "target", - onChange --> props.onChange(targetType.defaultScalaTarget), - checked := targetType == props.scalaTarget.targetType - ), - label(`for` := targetLabel, role := "button", cls := "radio", targetLabel) - ) - }.toTagMod - ) + val targetSelector = ScalaFnComponent + .withHooks[TargetSelector] + .render(props => { + div( + ul(cls := "target")( + targetTypes.map { targetType => + val targetLabel = labelFor(targetType) + li( + input( + `type` := "radio", + id := targetLabel, + value := targetLabel, + name := "target", + onChange --> props.onChange(targetType.defaultScalaTarget), + checked := targetType == props.scalaTarget.targetType + ), + label(`for` := targetLabel, role := "button", cls := "radio", targetLabel) + ) + }.toTagMod ) - }) + ) + }) + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/TopBar.scala b/client/src/main/scala/com.olegych.scastie.client/components/TopBar.scala index 4b208c884..ab72316c9 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/TopBar.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/TopBar.scala @@ -3,10 +3,10 @@ package client package components import api.User - -import japgolly.scalajs.react._, vdom.all._, extra._ - +import extra._ +import japgolly.scalajs.react._ import org.scalajs.dom +import vdom.all._ final case class TopBar(view: StateSnapshot[View], user: Option[User], openLoginModal: Reusable[Callback]) { @inline def render: VdomElement = TopBar.component(this) @@ -14,54 +14,47 @@ final case class TopBar(view: StateSnapshot[View], user: Option[User], openLogin object TopBar { - implicit val reusability: Reusability[TopBar] = - Reusability.derive[TopBar] + implicit val reusability: Reusability[TopBar] = Reusability.derive[TopBar] private def render(props: TopBar): VdomElement = { - def openInNewTab(link: String): Callback = - Callback { - dom.window.open(link, "_blank").focus() - } + def openInNewTab(link: String): Callback = Callback { + dom.window.open(link, "_blank").focus() + } - def feedback: Callback = - openInNewTab("https://gitter.im/scalacenter/scastie") + def feedback: Callback = openInNewTab("https://gitter.im/scalacenter/scastie") - def issue: Callback = - openInNewTab("https://github.com/scalacenter/scastie/issues/new/choose") + def issue: Callback = openInNewTab("https://github.com/scalacenter/scastie/issues/new/choose") val logoutUrl = "/logout" - def logout: Callback = - props.view.setState(View.Editor) >> - Callback(dom.window.location.pathname = logoutUrl) + def logout: Callback = props.view.setState(View.Editor) >> + Callback(dom.window.location.pathname = logoutUrl) - val profileButton = - props.user match { - case Some(user) => - li( - cls := "btn dropdown", - img(src := user.avatar_url + "&s=30", alt := "Your Github Avatar", cls := "avatar"), - span(user.login), - i(cls := "fa fa-caret-down"), - ul( - cls := "subactions", - li( - onClick --> props.view.setState(View.CodeSnippets), - role := "link", - title := "Go to your code snippets", - cls := "btn", - (cls := "selected").when(View.CodeSnippets == props.view.value) - )( - i(cls := "fa fa-code"), - "Snippets" - ), - li(role := "link", onClick --> logout, cls := "btn", i(cls := "fa fa-sign-out"), "Logout") - ) + val profileButton = props.user match { + case Some(user) => li( + cls := "btn dropdown", + img(src := user.avatar_url + "&s=30", alt := "Your Github Avatar", cls := "avatar"), + span(user.login), + i(cls := "fa fa-caret-down"), + ul( + cls := "subactions", + li( + onClick --> props.view.setState(View.CodeSnippets), + role := "link", + title := "Go to your code snippets", + cls := "btn", + (cls := "selected").when(View.CodeSnippets == props.view.value) + )( + i(cls := "fa fa-code"), + "Snippets" + ), + li(role := "link", onClick --> logout, cls := "btn", i(cls := "fa fa-sign-out"), "Logout") ) + ) - case None => - li(role := "link", onClick --> props.openLoginModal, cls := "btn", i(cls := "fa fa-sign-in"), "Login") - } + case None => + li(role := "link", onClick --> props.openLoginModal, cls := "btn", i(cls := "fa fa-sign-in"), "Login") + } nav( cls := "topbar", @@ -74,18 +67,22 @@ object TopBar { i(cls := "fa fa-caret-down"), ul( cls := "subactions", - li(onClick --> feedback, - role := "link", - title := "Open Gitter.im Chat to give us feedback", - cls := "btn", - i(cls := "fa fa-gitter"), - span("Scastie's gitter")), - li(onClick --> issue, - role := "link", - title := "Create new issue on GitHub", - cls := "btn", - i(cls := "fa fa-github"), - span("Github issues")) + li( + onClick --> feedback, + role := "link", + title := "Open Gitter.im Chat to give us feedback", + cls := "btn", + i(cls := "fa fa-gitter"), + span("Scastie's gitter") + ), + li( + onClick --> issue, + role := "link", + title := "Create new issue on GitHub", + cls := "btn", + i(cls := "fa fa-github"), + span("Github issues") + ) ) ), profileButton @@ -98,4 +95,5 @@ object TopBar { .render_P(render) .configure(Reusability.shouldComponentUpdate) .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/VersionSelector.scala b/client/src/main/scala/com.olegych.scastie.client/components/VersionSelector.scala index cf931eb24..81587b31c 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/VersionSelector.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/VersionSelector.scala @@ -1,10 +1,9 @@ package com.olegych.scastie.client.components import com.olegych.scastie.api._ +import com.olegych.scastie.buildinfo.BuildInfo import japgolly.scalajs.react._ - import vdom.all._ -import com.olegych.scastie.buildinfo.BuildInfo case class VersionSelector(scalaTarget: ScalaTarget, onChange: ScalaTarget ~=> Callback) { @inline def render: VdomElement = VersionSelector.versionSelectorHook(this) @@ -12,66 +11,70 @@ case class VersionSelector(scalaTarget: ScalaTarget, onChange: ScalaTarget ~=> C object VersionSelector { - val versionSelectorHook = - ScalaFnComponent - .withHooks[VersionSelector] - .render(props => { - def versionSelectors(scalaVersion: String) = - props.scalaTarget match { - case d: ScalaTarget.Jvm => ScalaTarget.Jvm.apply(scalaVersion) - case d: ScalaTarget.Typelevel => ScalaTarget.Typelevel.apply(scalaVersion) - case d: ScalaTarget.Scala3 => ScalaTarget.Scala3.apply(scalaVersion) - case js: ScalaTarget.Js => ScalaTarget.Js(scalaVersion, js.scalaJsVersion) - case n: ScalaTarget.Native => ScalaTarget.Native(n.scalaNativeVersion, n.scalaVersion) - } + val versionSelectorHook = ScalaFnComponent + .withHooks[VersionSelector] + .render(props => { + def versionSelectors(scalaVersion: String) = props.scalaTarget match { + case d: ScalaTarget.Jvm => ScalaTarget.Jvm.apply(scalaVersion) + case d: ScalaTarget.Typelevel => ScalaTarget.Typelevel.apply(scalaVersion) + case d: ScalaTarget.Scala3 => ScalaTarget.Scala3.apply(scalaVersion) + case js: ScalaTarget.Js => ScalaTarget.Js(scalaVersion, js.scalaJsVersion) + case n: ScalaTarget.Native => ScalaTarget.Native(n.scalaNativeVersion, n.scalaVersion) + } - def renderRecommended3Versions(scalaVersion: String) = { - if (scalaVersion == BuildInfo.stableLTS) s"$scalaVersion LTS" - else if (scalaVersion == BuildInfo.stableNext) s"$scalaVersion Next" - else scalaVersion - } + def renderRecommended3Versions(scalaVersion: String) = { + if (scalaVersion == BuildInfo.stableLTS) s"$scalaVersion LTS" + else if (scalaVersion == BuildInfo.stableNext) s"$scalaVersion Next" + else scalaVersion + } - ul(cls := "suggestedVersions")( - ScalaVersions - .suggestedScalaVersions(props.scalaTarget.targetType) - .map { suggestedVersion => - li( - input( - `type` := "radio", - id := s"scala-$suggestedVersion", - value := suggestedVersion, - name := "scalaV", - onChange --> props.onChange(versionSelectors(suggestedVersion)), - checked := props.scalaTarget.scalaVersion == suggestedVersion - ), - label(`for` := s"scala-$suggestedVersion", className := "radio", role := "button", renderRecommended3Versions(suggestedVersion)) + ul(cls := "suggestedVersions")( + ScalaVersions + .suggestedScalaVersions(props.scalaTarget.targetType) + .map { suggestedVersion => + li( + input( + `type` := "radio", + id := s"scala-$suggestedVersion", + value := suggestedVersion, + name := "scalaV", + onChange --> props.onChange(versionSelectors(suggestedVersion)), + checked := props.scalaTarget.scalaVersion == suggestedVersion + ), + label( + `for` := s"scala-$suggestedVersion", + className := "radio", + role := "button", + renderRecommended3Versions(suggestedVersion) ) - } - .toTagMod, - li( - label( - div(cls := "select-wrapper"){ - val isRecommended = ScalaVersions - .suggestedScalaVersions(props.scalaTarget.targetType) - .contains(props.scalaTarget.scalaVersion) - - select( - name := "scalaVersion", - onChange ==> { (e: ReactEventFromInput) => - props.onChange(versionSelectors(e.target.value)) - }, - value := {if (isRecommended) "Other" else props.scalaTarget.scalaVersion}, - TagMod.when(!isRecommended)(className := "selected-option") - )( - ScalaVersions - .allVersions(props.scalaTarget.targetType) - .map(version => option(version)) - .prepended(option("Other")(hidden := true, disabled := true)) - .toTagMod - ) - } ) + } + .toTagMod, + li( + label( + div(cls := "select-wrapper") { + val isRecommended = ScalaVersions + .suggestedScalaVersions(props.scalaTarget.targetType) + .contains(props.scalaTarget.scalaVersion) + + select( + name := "scalaVersion", + onChange ==> { (e: ReactEventFromInput) => + props.onChange(versionSelectors(e.target.value)) + }, + value := { if (isRecommended) "Other" else props.scalaTarget.scalaVersion }, + TagMod.when(!isRecommended)(className := "selected-option") + )( + ScalaVersions + .allVersions(props.scalaTarget.targetType) + .map(version => option(version)) + .prepended(option("Other")(hidden := true, disabled := true)) + .toTagMod + ) + } ) ) - }) + ) + }) + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/ViewToggleButton.scala b/client/src/main/scala/com.olegych.scastie.client/components/ViewToggleButton.scala index 79791a78d..c00ede8a3 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/ViewToggleButton.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/ViewToggleButton.scala @@ -2,38 +2,41 @@ package com.olegych.scastie package client package components -import japgolly.scalajs.react._, vdom.all._, extra._ +import extra._ +import japgolly.scalajs.react._ +import vdom.all._ -final case class ViewToggleButton(currentView: StateSnapshot[View], - forView: View, - buttonTitle: String, - faIcon: String, - onClick: Reusable[Callback]) { +final case class ViewToggleButton( + currentView: StateSnapshot[View], + forView: View, + buttonTitle: String, + faIcon: String, + onClick: Reusable[Callback] +) { @inline def render: VdomElement = ViewToggleButton.component(this) } object ViewToggleButton { - implicit val reusability: Reusability[ViewToggleButton] = - Reusability.derive[ViewToggleButton] + implicit val reusability: Reusability[ViewToggleButton] = Reusability.derive[ViewToggleButton] private def render(props: ViewToggleButton): VdomElement = { li( onClick --> (props.currentView.setState(props.forView) >> props.onClick), - role := "button", + role := "button", title := props.buttonTitle, - (cls := "selected").when(props.currentView.value == props.forView), - cls := "btn" + (cls := "selected").when(props.currentView.value == props.forView), + cls := "btn" )( i(cls := props.faIcon, cls := "fa"), span(props.buttonTitle) ) } - private val component = - ScalaComponent - .builder[ViewToggleButton]("ViewToggleButton") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[ViewToggleButton]("ViewToggleButton") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/WorksheetButton.scala b/client/src/main/scala/com.olegych.scastie.client/components/WorksheetButton.scala index e8518539a..d41194455 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/WorksheetButton.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/WorksheetButton.scala @@ -3,32 +3,27 @@ package client package components import japgolly.scalajs.react._ - import vdom.all._ final case class WorksheetButton( - hasWorksheetMode: Boolean, - isWorksheetMode: Boolean, - toggleWorksheetMode: Reusable[Callback], - view: View + hasWorksheetMode: Boolean, + isWorksheetMode: Boolean, + toggleWorksheetMode: Reusable[Callback], + view: View ) { @inline def render: VdomElement = WorksheetButton.component(this) } object WorksheetButton { - implicit val reusability: Reusability[WorksheetButton] = - Reusability.derive[WorksheetButton] + implicit val reusability: Reusability[WorksheetButton] = Reusability.derive[WorksheetButton] private def render(props: WorksheetButton): VdomElement = { val isWorksheetModeSelected = if (props.isWorksheetMode) - if (props.view != View.Editor) - TagMod(cls := "enabled alpha") - else - TagMod(cls := "enabled") - else - EmptyVdom + if (props.view != View.Editor) TagMod(cls := "enabled alpha") + else TagMod(cls := "enabled") + else EmptyVdom val isWorksheetModeToggleLabel = if (props.isWorksheetMode) "OFF" @@ -40,7 +35,7 @@ object WorksheetButton { else "This configuration does not support Worksheet mode"), isWorksheetModeSelected, role := "button", - cls := "btn editor", + cls := "btn editor", onClick --> props.toggleWorksheetMode )( i(cls := "fa fa-calendar"), @@ -49,10 +44,10 @@ object WorksheetButton { ) } - private val component = - ScalaComponent - .builder[WorksheetButton]("WorksheetButton") - .render_P(render) - .configure(Reusability.shouldComponentUpdate) - .build + private val component = ScalaComponent + .builder[WorksheetButton]("WorksheetButton") + .render_P(render) + .configure(Reusability.shouldComponentUpdate) + .build + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/CodeEditor.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/CodeEditor.scala index 939b8dbad..8854a1cdf 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/CodeEditor.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/CodeEditor.scala @@ -1,12 +1,15 @@ package com.olegych.scastie.client.components.editor import com.olegych.scastie.api -import com.olegych.scastie.client.HTMLFormatter import com.olegych.scastie.client._ +import com.olegych.scastie.client.HTMLFormatter +import hooks.Hooks.UseStateF import japgolly.scalajs.react._ +import js.JSConverters._ import org.scalajs.dom import org.scalajs.dom.Element import org.scalajs.dom.HTMLElement +import scalajs.js import typings.codemirrorAutocomplete.mod._ import typings.codemirrorCommands.mod._ import typings.codemirrorLanguage.mod._ @@ -15,93 +18,96 @@ import typings.codemirrorLint.mod._ import typings.codemirrorSearch.mod._ import typings.codemirrorState.mod._ import typings.codemirrorView.mod._ - -import scalajs.js import vdom.all._ import JsUtils._ -import hooks.Hooks.UseStateF -import js.JSConverters._ -final case class CodeEditor(visible: Boolean, - isDarkTheme: Boolean, - isPresentationMode: Boolean, - isWorksheetMode: Boolean, - isEmbedded: Boolean, - showLineNumbers: Boolean, - value: String, - attachedDoms: Map[String, HTMLElement], - instrumentations: Set[api.Instrumentation], - compilationInfos: Set[api.Problem], - runtimeError: Option[api.RuntimeError], - saveOrUpdate: Reusable[Callback], - clear: Reusable[Callback], - openNewSnippetModal: Reusable[Callback], - toggleHelp: Reusable[Callback], - toggleConsole: Reusable[Callback], - toggleLineNumbers: Reusable[Callback], - togglePresentationMode: Reusable[Callback], - formatCode: Reusable[Callback], - codeChange: String ~=> Callback, - target: api.ScalaTarget, - metalsStatus: MetalsStatus, - setMetalsStatus: MetalsStatus ~=> Callback, - dependencies: Set[api.ScalaDependency]) - extends Editor { +final case class CodeEditor( + visible: Boolean, + isDarkTheme: Boolean, + isPresentationMode: Boolean, + isWorksheetMode: Boolean, + isEmbedded: Boolean, + showLineNumbers: Boolean, + value: String, + attachedDoms: Map[String, HTMLElement], + instrumentations: Set[api.Instrumentation], + compilationInfos: Set[api.Problem], + runtimeError: Option[api.RuntimeError], + saveOrUpdate: Reusable[Callback], + clear: Reusable[Callback], + openNewSnippetModal: Reusable[Callback], + toggleHelp: Reusable[Callback], + toggleConsole: Reusable[Callback], + toggleLineNumbers: Reusable[Callback], + togglePresentationMode: Reusable[Callback], + formatCode: Reusable[Callback], + codeChange: String ~=> Callback, + target: api.ScalaTarget, + metalsStatus: MetalsStatus, + setMetalsStatus: MetalsStatus ~=> Callback, + dependencies: Set[api.ScalaDependency] +) extends Editor { @inline def render: VdomElement = CodeEditor.hooksComponent(this) } object CodeEditor { - private def init(props: CodeEditor, ref: Ref.Simple[Element], editorView: UseStateF[CallbackTo, EditorView]): Callback = - ref.foreachCB(divRef => { - - val syntaxHighlighting = new SyntaxHighlightingPlugin(editorView) - val extensions = js.Array[Any]( - Editor.editorTheme.of(props.codemirrorTheme), - lineNumbers(), - highlightSpecialChars(), - history(), - drawSelection(), - dropCursor(), - EditorState.allowMultipleSelections.of(true), - indentOnInput(), - bracketMatching(), - closeBrackets(), - rectangularSelection(), - crosshairCursor(), - highlightSelectionMatches(), - Editor.indentationMarkersExtension, - keymap.of(closeBracketsKeymap ++ defaultKeymap ++ historyKeymap ++ foldKeymap ++ completionKeymap ++ lintKeymap ++ searchKeymap), - StateField - .define(StateFieldSpec[Set[api.Instrumentation]](_ => props.instrumentations, (value, _) => value)) - .extension, - DecorationProvider(props), - EditorState.tabSize.of(2), - Prec.highest(EditorKeymaps.keymapping(props)), - InteractiveProvider.interactive.of(InteractiveProvider(props).extension), - SyntaxHighlightingTheme.highlightingTheme, - lintGutter(), - OnChangeHandler(props.codeChange), - syntaxHighlighting.syntaxHighlightingExtension.of(syntaxHighlighting.fallbackExtension), - ) + private def init( + props: CodeEditor, + ref: Ref.Simple[Element], + editorView: UseStateF[CallbackTo, EditorView] + ): Callback = ref.foreachCB(divRef => { + + val syntaxHighlighting = new SyntaxHighlightingPlugin(editorView) + val extensions = js.Array[Any]( + Editor.editorTheme.of(props.codemirrorTheme), + lineNumbers(), + highlightSpecialChars(), + history(), + drawSelection(), + dropCursor(), + EditorState.allowMultipleSelections.of(true), + indentOnInput(), + bracketMatching(), + closeBrackets(), + rectangularSelection(), + crosshairCursor(), + highlightSelectionMatches(), + Editor.indentationMarkersExtension, + keymap.of( + closeBracketsKeymap ++ defaultKeymap ++ historyKeymap ++ foldKeymap ++ completionKeymap ++ lintKeymap ++ searchKeymap + ), + StateField + .define(StateFieldSpec[Set[api.Instrumentation]](_ => props.instrumentations, (value, _) => value)) + .extension, + DecorationProvider(props), + EditorState.tabSize.of(2), + Prec.highest(EditorKeymaps.keymapping(props)), + InteractiveProvider.interactive.of(InteractiveProvider(props).extension), + SyntaxHighlightingTheme.highlightingTheme, + lintGutter(), + OnChangeHandler(props.codeChange), + syntaxHighlighting.syntaxHighlightingExtension.of(syntaxHighlighting.fallbackExtension) + ) - val editorStateConfig = EditorStateConfig() - .setExtensions(extensions) - .setDoc(props.value) + val editorStateConfig = EditorStateConfig() + .setExtensions(extensions) + .setDoc(props.value) - val editor = new EditorView(EditorViewConfig() + val editor = new EditorView( + EditorViewConfig() .setState(EditorState.create(editorStateConfig)) .setParent(divRef) - ) + ) - editorView.setState(editor) - }) + editorView.setState(editor) + }) private def getDecorations(props: CodeEditor, doc: Text): js.Array[Diagnostic] = { val errors = props.compilationInfos .filter(prob => prob.line.isDefined && prob.line.get <= doc.lines) .map(problem => { - val line = problem.line.get max 1 + val line = problem.line.get max 1 val lineInfo = doc.line(line) Diagnostic(lineInfo.from, HTMLFormatter.format(problem.message), parseSeverity(problem.severity), lineInfo.to) @@ -114,32 +120,38 @@ object CodeEditor { }) val runtimeErrors = props.runtimeError.map(runtimeError => { - val line = runtimeError.line.getOrElse(1).min(doc.lines.toInt) + val line = runtimeError.line.getOrElse(1).min(doc.lines.toInt) val lineInfo = doc.line(line) - val msg = if (runtimeError.fullStack.nonEmpty) runtimeError.fullStack else runtimeError.message + val msg = if (runtimeError.fullStack.nonEmpty) runtimeError.fullStack else runtimeError.message Diagnostic(lineInfo.from, HTMLFormatter.format(msg), codemirrorLintStrings.error, lineInfo.to) }) (errors ++ runtimeErrors).toJSArray } - private def updateDiagnostics(editorView: UseStateF[CallbackTo, EditorView], prevProps: Option[CodeEditor], props: CodeEditor): Callback = { + private def updateDiagnostics( + editorView: UseStateF[CallbackTo, EditorView], + prevProps: Option[CodeEditor], + props: CodeEditor + ): Callback = { Callback { - editorView.value.dispatch(setDiagnostics(editorView.value.state, getDecorations(props, editorView.value.state.doc))) + editorView.value.dispatch( + setDiagnostics(editorView.value.state, getDecorations(props, editorView.value.state.doc)) + ) }.when_( prevProps.isDefined && props.value == editorView.value.state.doc.toString() && ( - prevProps.get.compilationInfos != props.compilationInfos || - prevProps.get.runtimeError != props.runtimeError - ) + prevProps.get.compilationInfos != props.compilationInfos || + prevProps.get.runtimeError != props.runtimeError + ) ) } private def updateComponent( - props: CodeEditor, - ref: Ref.Simple[Element], - prevProps: Option[CodeEditor], - editorView: UseStateF[CallbackTo, EditorView] + props: CodeEditor, + ref: Ref.Simple[Element], + prevProps: Option[CodeEditor], + editorView: UseStateF[CallbackTo, EditorView] ): Callback = { Editor.updateCode(editorView, props) >> Editor.updateTheme(ref, prevProps, props, editorView) >> @@ -148,16 +160,15 @@ object CodeEditor { InteractiveProvider.reloadMetalsConfiguration(editorView, prevProps, props) } - val hooksComponent = - ScalaFnComponent - .withHooks[CodeEditor] - .useRef(Ref[Element]) - .useRef[Option[CodeEditor]](None) - .useState(new EditorView()) - .useEffectOnMountBy((props, ref, prevProps, editorView) => init(props, ref.value, editorView)) - .useEffectBy( - (props, ref, prevProps, editorView) => updateComponent(props, ref.value, prevProps.value, editorView) >> prevProps.set(Some(props)) - ) - .render((_, ref, _, _) => Editor.render(ref.value)) + val hooksComponent = ScalaFnComponent + .withHooks[CodeEditor] + .useRef(Ref[Element]) + .useRef[Option[CodeEditor]](None) + .useState(new EditorView()) + .useEffectOnMountBy((props, ref, prevProps, editorView) => init(props, ref.value, editorView)) + .useEffectBy((props, ref, prevProps, editorView) => + updateComponent(props, ref.value, prevProps.value, editorView) >> prevProps.set(Some(props)) + ) + .render((_, ref, _, _) => Editor.render(ref.value)) } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/DebouncingCapabilities.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/DebouncingCapabilities.scala index 7a73c807a..a0079a1fd 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/DebouncingCapabilities.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/DebouncingCapabilities.scala @@ -1,15 +1,13 @@ package com.olegych.scastie.client.components.editor -import typings.codemirrorState.mod._ -import typings.codemirrorView.mod._ - import scala.concurrent.duration._ import scala.scalajs.js.timers._ import scalajs.js +import typings.codemirrorState.mod._ +import typings.codemirrorView.mod._ import EditorTextOps._ - trait DebouncingCapabilities { type OnChange = (String, EditorView) => Unit @@ -17,13 +15,11 @@ trait DebouncingCapabilities { FacetConfig[OnChange, OnChange]().setCombine(input => over(input.toSeq)) } - private def debounce(fn: OnChange): OnChange = { + private def debounce(fn: OnChange): OnChange = { var timeout: js.UndefOr[js.timers.SetTimeoutHandle] = js.undefined (code: String, view: EditorView) => { - val tokenLength = view - .lineBeforeCursor - .reverseIterator + val tokenLength = view.lineBeforeCursor.reverseIterator .takeWhile(c => !c.isWhitespace || c == '.') .length @@ -38,8 +34,8 @@ trait DebouncingCapabilities { } } - private def over(functions: Seq[OnChange]): OnChange = { - (code: String, view: EditorView) => functions.foreach(f => f(code, view)) + private def over(functions: Seq[OnChange]): OnChange = { (code: String, view: EditorView) => + functions.foreach(f => f(code, view)) } protected def onChangeCallback(onChange: OnChange): Extension = { @@ -49,11 +45,12 @@ trait DebouncingCapabilities { onChangeFacet.of(debouncedOnChange), EditorView.updateListener.of(viewUpdate => { if (viewUpdate.docChanged) { - val content = viewUpdate.state.sliceDoc() + val content = viewUpdate.state.sliceDoc() val onChange = viewUpdate.state.facet[OnChange](onChangeFacet.asInstanceOf[Facet[Any, OnChange]]) onChange(content, viewUpdate.view) } }) ) } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/DecorationProvider.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/DecorationProvider.scala index 19e4dee0e..00b0f95de 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/DecorationProvider.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/DecorationProvider.scala @@ -1,24 +1,24 @@ package com.olegych.scastie.client.components.editor +import scala.collection.mutable.ListBuffer + import com.olegych.scastie.api import com.olegych.scastie.api.AttachedDom import com.olegych.scastie.api.Html import com.olegych.scastie.api.Value +import hooks.Hooks.UseStateF import japgolly.scalajs.react._ +import js.JSConverters._ import org.scalajs.dom import org.scalajs.dom.HTMLElement +import scalajs.js import typings.codemirrorState.mod._ import typings.codemirrorView.mod._ -import scala.collection.mutable.ListBuffer - -import scalajs.js -import hooks.Hooks.UseStateF -import js.JSConverters._ - object DecorationProvider { final class AttachedDomDecoration(uuid: String, attachedDoms: Map[String, HTMLElement]) extends WidgetType { + override def toDOM(view: EditorView): HTMLElement = { val wrap = dom.document.createElement("div") wrap.setAttribute("aria-hidden", "true") @@ -26,9 +26,11 @@ object DecorationProvider { attachedDoms.get(uuid).map(wrap.append(_)) wrap.domAsHtml } + } final class TypeDecoration(value: String, typeName: String) extends WidgetType { + override def toDOM(view: EditorView): HTMLElement = { val wrap = dom.document.createElement("span") wrap.setAttribute("aria-hidden", "true") @@ -45,9 +47,11 @@ object DecorationProvider { wrap.append(textBody) wrap.domAsHtml } + } final class HTMLDecoration(html: String) extends WidgetType { + override def toDOM(view: EditorView): HTMLElement = { val wrap = dom.document.createElement("pre") wrap.setAttribute("aria-hidden", "true") @@ -55,9 +59,14 @@ object DecorationProvider { wrap.innerHTML = html wrap.domAsHtml } + } - private def createDecorations(instrumentations: Set[api.Instrumentation], attachedDoms: Map[String, HTMLElement], maxPosititon: Int): DecorationSet = { + private def createDecorations( + instrumentations: Set[api.Instrumentation], + attachedDoms: Map[String, HTMLElement], + maxPosititon: Int + ): DecorationSet = { val deco = instrumentations .filter(_.position.end < maxPosititon) .map { instrumentation => @@ -77,11 +86,11 @@ object DecorationProvider { Decoration.set(x, true) } - private val addTypeDecorations = StateEffect.define[DecorationSet]() + private val addTypeDecorations = StateEffect.define[DecorationSet]() private val filterTypeDecorations = StateEffect.define[DecorationSet]() private def updateDecorationPositions(previousValue: DecorationSet, transaction: Transaction): DecorationSet = { - val newNewlines: ListBuffer[Int] = ListBuffer.empty + val newNewlines: ListBuffer[Int] = ListBuffer.empty val decorationsToReAdd: ListBuffer[Range[Decoration]] = ListBuffer.empty transaction.changes.iterChanges((_, _, fromB, toB, _) => { transaction.newDoc.sliceString(fromB, toB).lastOption.foreach { @@ -103,12 +112,10 @@ object DecorationProvider { }.asInstanceOf[RangeSetUpdate[DecorationSet]]) .map(transaction.changes) - if (decorationsToReAdd.isEmpty) - newValues - else - newValues.update(new js.Object { - var add = decorationsToReAdd.toJSArray - }.asInstanceOf[RangeSetUpdate[DecorationSet]]) + if (decorationsToReAdd.isEmpty) newValues + else newValues.update(new js.Object { + var add = decorationsToReAdd.toJSArray + }.asInstanceOf[RangeSetUpdate[DecorationSet]]) } private def updateState(previousValue: DecorationSet, transaction: Transaction): DecorationSet = { @@ -121,8 +128,7 @@ object DecorationProvider { val decorationSet = stateEffect.value.asInstanceOf[DecorationSet] if (decorationSet.size > 0) decorationSet else Decoration.none } - case _ => - updateDecorationPositions(previousValue, transaction) + case _ => updateDecorationPositions(previousValue, transaction) } } @@ -130,37 +136,36 @@ object DecorationProvider { !ignoredRanges.contains(from) } - private def stateFieldSpec(props: CodeEditor) = - StateFieldSpec[DecorationSet]( - create = _ => createDecorations(props.instrumentations, props.attachedDoms, props.value.length), - update = updateState, - ).setProvide(v => EditorView.decorations.from(v)) + private def stateFieldSpec(props: CodeEditor) = StateFieldSpec[DecorationSet]( + create = _ => createDecorations(props.instrumentations, props.attachedDoms, props.value.length), + update = updateState + ).setProvide(v => EditorView.decorations.from(v)) def updateDecorations( - editorView: UseStateF[CallbackTo, EditorView], - prevProps: Option[CodeEditor], - props: CodeEditor - ): Callback = - Callback { - val decorations = createDecorations(props.instrumentations, props.attachedDoms, editorView.value.state.doc.length.toInt + 1) - val addTypesEffect = addTypeDecorations.of(decorations) - val changes = new js.Object { - var desc = new js.Object { - var length = prevProps.map(_.value.length).getOrElse(0) - var newLength = props.value.length - var empty = newLength == length - }.asInstanceOf[ChangeDesc] - }.asInstanceOf[ChangeSpec] - - editorView.value.dispatch( - TransactionSpec() - .setChanges(changes) - .setEffects(addTypesEffect.asInstanceOf[StateEffect[Any]]) - ) - }.when_( - prevProps.isDefined && - (props.instrumentations != prevProps.get.instrumentations) + editorView: UseStateF[CallbackTo, EditorView], + prevProps: Option[CodeEditor], + props: CodeEditor + ): Callback = Callback { + val decorations = + createDecorations(props.instrumentations, props.attachedDoms, editorView.value.state.doc.length.toInt + 1) + val addTypesEffect = addTypeDecorations.of(decorations) + val changes = new js.Object { + var desc = new js.Object { + var length = prevProps.map(_.value.length).getOrElse(0) + var newLength = props.value.length + var empty = newLength == length + }.asInstanceOf[ChangeDesc] + }.asInstanceOf[ChangeSpec] + + editorView.value.dispatch( + TransactionSpec() + .setChanges(changes) + .setEffects(addTypesEffect.asInstanceOf[StateEffect[Any]]) ) + }.when_( + prevProps.isDefined && + (props.instrumentations != prevProps.get.instrumentations) + ) def apply(props: CodeEditor): Extension = StateField.define(stateFieldSpec(props)).extension } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/Editor.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/Editor.scala index b4fb7b472..3b6eed12a 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/Editor.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/Editor.scala @@ -3,13 +3,12 @@ package com.olegych.scastie.client.components.editor import japgolly.scalajs.react._ import org.scalablytyped.runtime.StringDictionary import org.scalajs.dom.Element +import scalajs.js import typings.codemirrorState.mod._ import typings.codemirrorView.anon import typings.codemirrorView.mod._ import typings.replitCodemirrorIndentationMarkers.anon.ActiveDark import typings.replitCodemirrorIndentationMarkers.mod._ - -import scalajs.js import vdom.all._ trait Editor { @@ -35,20 +34,24 @@ object Editor { def render(ref: Ref.Simple[Element]): VdomElement = div(cls := "editor-wrapper cm-s-solarized cm-s-light").withRef(ref) - def updateTheme(ref: Ref.Simple[Element], prevProps: Option[Editor], props: Editor, editorView: hooks.Hooks.UseStateF[CallbackTo, EditorView]): Callback = - ref - .foreach(ref => { - val cssTheme = if (props.isDarkTheme) "dark" else "light" - editorView.value.dispatch(TransactionSpec().setEffects(editorTheme.reconfigure(props.codemirrorTheme))) - ref.setAttribute("class", s"editor-wrapper cm-s-solarized cm-s-$cssTheme") - }) - .when_(prevProps.map(_.isDarkTheme != props.isDarkTheme).getOrElse(true)) + def updateTheme( + ref: Ref.Simple[Element], + prevProps: Option[Editor], + props: Editor, + editorView: hooks.Hooks.UseStateF[CallbackTo, EditorView] + ): Callback = ref + .foreach(ref => { + val cssTheme = if (props.isDarkTheme) "dark" else "light" + editorView.value.dispatch(TransactionSpec().setEffects(editorTheme.reconfigure(props.codemirrorTheme))) + ref.setAttribute("class", s"editor-wrapper cm-s-solarized cm-s-$cssTheme") + }) + .when_(prevProps.map(_.isDarkTheme != props.isDarkTheme).getOrElse(true)) def updateCode(editorView: Hooks.UseStateF[CallbackTo, EditorView], newState: Editor): Callback = { Callback { editorView.value.dispatch(TransactionSpec().setChanges(new js.Object { - var from = 0 - var to = editorView.value.state.doc.length + var from = 0 + var to = editorView.value.state.doc.length var insert = newState.value }.asInstanceOf[ChangeSpec])) }.when_(editorView.value.state.doc.toString() != newState.value) diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/EditorKeymaps.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/EditorKeymaps.scala index 2c0e005b4..fa4904112 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/EditorKeymaps.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/EditorKeymaps.scala @@ -1,16 +1,15 @@ package com.olegych.scastie.client.components.editor +import com.olegych.scastie.client import org.scalajs.dom -import typings.codemirrorState.anon -import typings.codemirrorView.mod.EditorView -import typings.codemirrorView.mod.{KeyBinding => JSKeyBinding} -import typings.codemirrorCommands.mod._ +import scalajs.js import typings.codemirrorAutocomplete.mod.acceptCompletion +import typings.codemirrorCommands.mod._ +import typings.codemirrorState.anon import typings.codemirrorState.mod._ -import com.olegych.scastie.client - -import scalajs.js import typings.codemirrorState.mod.TransactionSpec +import typings.codemirrorView.mod.{KeyBinding => JSKeyBinding} +import typings.codemirrorView.mod.EditorView object EditorKeymaps { @@ -23,31 +22,31 @@ object EditorKeymaps { } } - val saveOrUpdate = new Key("Ctrl-Enter", "Meta-Enter") - val saveOrUpdateAlt = new Key("Ctrl-s", "Meta-s") + val saveOrUpdate = new Key("Ctrl-Enter", "Meta-Enter") + val saveOrUpdateAlt = new Key("Ctrl-s", "Meta-s") val openNewSnippetModal = new Key("Ctrl-m", "Meta-m") - val clear = new Key("Escape") - val clearAlt = new Key("F1") - val console = new Key("F3") - val help = new Key("F5") - val format = new Key("F6") - val presentation = new Key("F8") + val clear = new Key("Escape") + val clearAlt = new Key("F1") + val console = new Key("F3") + val help = new Key("F5") + val format = new Key("F6") + val presentation = new Key("F8") - def keymapping(e: CodeEditor) = - typings.codemirrorView.mod.keymap.of( - js.Array( - KeyBinding.tabKeybind, - KeyBinding(_ => e.saveOrUpdate.runNow(), saveOrUpdate, true), - KeyBinding(_ => e.saveOrUpdate.runNow(), saveOrUpdateAlt, true), - KeyBinding(_ => e.openNewSnippetModal.runNow(), openNewSnippetModal, true), - KeyBinding(_ => e.clear.runNow(), clear, true), - KeyBinding(_ => e.clear.runNow(), clearAlt, true), - KeyBinding(_ => e.toggleHelp.runNow(), help, true), - KeyBinding(_ => e.toggleConsole.runNow(), console, true), - KeyBinding(_ => e.formatCode.runNow(), format, true), - KeyBinding(_ => presentationMode(e), presentation, true), - ) + def keymapping(e: CodeEditor) = typings.codemirrorView.mod.keymap.of( + js.Array( + KeyBinding.tabKeybind, + KeyBinding(_ => e.saveOrUpdate.runNow(), saveOrUpdate, true), + KeyBinding(_ => e.saveOrUpdate.runNow(), saveOrUpdateAlt, true), + KeyBinding(_ => e.openNewSnippetModal.runNow(), openNewSnippetModal, true), + KeyBinding(_ => e.clear.runNow(), clear, true), + KeyBinding(_ => e.clear.runNow(), clearAlt, true), + KeyBinding(_ => e.toggleHelp.runNow(), help, true), + KeyBinding(_ => e.toggleConsole.runNow(), console, true), + KeyBinding(_ => e.formatCode.runNow(), format, true), + KeyBinding(_ => presentationMode(e), presentation, true) ) + ) + } case class Key(default: String, linux: String, mac: String, win: String) { @@ -63,22 +62,25 @@ case class Key(default: String, linux: String, mac: String, win: String) { val macAdjusted = if (client.isMac) mac.replace("Meta", "Cmd") else default macAdjusted.replace("Escape", "Esc") } + } object KeyBinding { + val tabKeybind: JSKeyBinding = { val key = new Key("Tab") JSKeyBinding() .setRun(view => - if (!acceptCompletion(view)) { - view.dispatch( - TransactionSpec() - .setChanges(js.Dynamic.literal(from = view.state.selection.main.head, insert = " ").asInstanceOf[ChangeSpec]) - .setSelection(EditorSelection.single(view.state.selection.main.head + 2)) - ) - true - } - else false + if (!acceptCompletion(view)) { + view.dispatch( + TransactionSpec() + .setChanges( + js.Dynamic.literal(from = view.state.selection.main.head, insert = " ").asInstanceOf[ChangeSpec] + ) + .setSelection(EditorSelection.single(view.state.selection.main.head + 2)) + ) + true + } else false ) .setKey(key.default) .setLinux(key.linux) @@ -96,4 +98,5 @@ object KeyBinding { .setWin(key.win) .setPreventDefault(preventDefault) } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/EditorTextOps.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/EditorTextOps.scala index e87d2a36e..8d715aa10 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/EditorTextOps.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/EditorTextOps.scala @@ -2,14 +2,14 @@ package com.olegych.scastie.client.components.editor import typings.codemirrorView.mod._ - object EditorTextOps { val regex = """\.\w*|\w+""".r implicit class EditorTextOpsOps(view: EditorView) { + def lineBeforeCursor: String = { - val pos = view.state.selection.main.from - val line = view.state.doc.lineAt(pos) + val pos = view.state.selection.main.from + val line = view.state.doc.lineAt(pos) val start = Math.max(line.from, pos - 250) line.text.slice(start.toInt - line.from.toInt, pos.toInt - line.from.toInt) } @@ -20,4 +20,5 @@ object EditorTextOps { } } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/InteractiveProvider.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/InteractiveProvider.scala index a035576c6..b541f6951 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/InteractiveProvider.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/InteractiveProvider.scala @@ -1,18 +1,17 @@ package com.olegych.scastie.client.components.editor +import scala.util.Try + import com.olegych.scastie.api import com.olegych.scastie.client._ +import hooks.Hooks.UseStateF import japgolly.scalajs.react._ +import scalajs.js import typings.codemirrorState.mod._ import typings.codemirrorView.mod._ import typings.highlightJs.mod.{HighlightOptions => HLJSOptions} -import typings.markedHighlight.mod._ import typings.marked.mod.marked.MarkedExtension - -import scala.util.Try - -import scalajs.js -import hooks.Hooks.UseStateF +import typings.markedHighlight.mod._ case class InteractiveProvider( dependencies: Set[api.ScalaDependency], @@ -20,13 +19,16 @@ case class InteractiveProvider( metalsStatus: MetalsStatus, updateStatus: MetalsStatus ~=> Callback, isWorksheetMode: Boolean, - isEmbedded: Boolean, -) extends MetalsClient with MetalsAutocompletion with MetalsHover { + isEmbedded: Boolean +) extends MetalsClient + with MetalsAutocompletion + with MetalsHover { def extension: js.Array[Any] = js.Array[Any]( metalsHover, metalsAutocomplete ) + } object InteractiveProvider { @@ -45,9 +47,10 @@ object InteractiveProvider { val interactive = new Compartment() val highlightJS = typings.highlightJs.mod.default + val highlightF: (String, String, String) => String = (str, lang, _) => { if (lang != null && highlightJS.getLanguage(lang) != null && lang != "") { - Try { highlightJS.highlight(str, HLJSOptions(lang)).value}.getOrElse(str) + Try { highlightJS.highlight(str, HLJSOptions(lang)).value }.getOrElse(str) } else { str } @@ -55,19 +58,20 @@ object InteractiveProvider { val marked = typings.marked.mod.marked.`package` marked.use(markedHighlight(SynchronousOptions.apply(highlightF)).asInstanceOf[MarkedExtension]) - marked.setOptions(typings.marked.mod.marked.MarkedOptions() - .setHeaderIds(false) - .setMangle(false) + marked.setOptions( + typings.marked.mod.marked + .MarkedOptions() + .setHeaderIds(false) + .setMangle(false) ) private def wasMetalsToggled(prevProps: CodeEditor, props: CodeEditor): Boolean = (prevProps.metalsStatus == MetalsDisabled && props.metalsStatus == MetalsLoading) || - (prevProps.metalsStatus != MetalsDisabled && props.metalsStatus == MetalsDisabled) + (prevProps.metalsStatus != MetalsDisabled && props.metalsStatus == MetalsDisabled) - private def didConfigChange(prevProps: CodeEditor, props: CodeEditor): Boolean = - props.target != prevProps.target || - props.dependencies != prevProps.dependencies || - props.isWorksheetMode != prevProps.isWorksheetMode + private def didConfigChange(prevProps: CodeEditor, props: CodeEditor): Boolean = props.target != prevProps.target || + props.dependencies != prevProps.dependencies || + props.isWorksheetMode != prevProps.isWorksheetMode def reloadMetalsConfiguration( editorView: UseStateF[CallbackTo, EditorView], @@ -92,4 +96,3 @@ object InteractiveProvider { } } - diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsAutocompletion.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsAutocompletion.scala index 935225722..5e19e7130 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsAutocompletion.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsAutocompletion.scala @@ -1,89 +1,107 @@ package com.olegych.scastie.client.components.editor +import scala.collection.mutable.HashMap +import scala.concurrent.Future + import com.olegych.scastie.api import japgolly.scalajs.react._ +import js.JSConverters._ import org.scalajs.dom +import scalajs.concurrent.JSExecutionContext.Implicits.queue +import scalajs.js +import scalajs.js.Thenable.Implicits._ import typings.codemirrorAutocomplete.anon import typings.codemirrorAutocomplete.mod._ import typings.codemirrorState.mod._ import typings.codemirrorView.mod._ - -import scala.collection.mutable.HashMap -import scala.concurrent.Future - -import scalajs.js -import scalajs.concurrent.JSExecutionContext.Implicits.queue -import scalajs.js.Thenable.Implicits._ -import js.JSConverters._ import EditorTextOps._ trait MetalsAutocompletion extends MetalsClient with DebouncingCapabilities { - val jsRegex = js.RegExp("\\.?\\w*") + val jsRegex = js.RegExp("\\.?\\w*") val selectionPattern = "\\$\\{\\d+:(.*?)\\}".r var wasPreviousIncomplete = true - var previousWord = "" + var previousWord = "" val completionInfoCache = HashMap.empty[String, dom.Node] /* * Creates additionalInsertInstructions e.g autoimport for completions */ - private def createAdditionalTextEdits(insertInstructions: List[api.AdditionalInsertInstructions], view: EditorView): Seq[ChangeSpec] = { + private def createAdditionalTextEdits( + insertInstructions: List[api.AdditionalInsertInstructions], + view: EditorView + ): Seq[ChangeSpec] = { insertInstructions.map(textEdit => { val editRange = textEdit.editRange - val startPos = view.state.doc.line(editRange.startLine).from.toInt + editRange.startChar - val endPos = view.state.doc.line(editRange.endLine).from.toInt + editRange.endChar - js.Dynamic.literal( - from = startPos, - to = endPos, - insert = textEdit.text - ).asInstanceOf[ChangeSpec] + val startPos = view.state.doc.line(editRange.startLine).from.toInt + editRange.startChar + val endPos = view.state.doc.line(editRange.endLine).from.toInt + editRange.endChar + js.Dynamic + .literal( + from = startPos, + to = endPos, + insert = textEdit.text + ) + .asInstanceOf[ChangeSpec] }) } private def prepareInsertionText(completion: api.CompletionItemDTO, lineStart: Int): (EditorSelection, String) = { - val patternIndex = completion.instructions.text.indexOf("$0") + val patternIndex = completion.instructions.text.indexOf("$0") val partiallyCleanedPattern = completion.instructions.text.replace("$0", "") - val offset = if (patternIndex == -1) { - partiallyCleanedPattern.length - } else { - patternIndex - } + val offset = + if (patternIndex == -1) { + partiallyCleanedPattern.length + } else { + patternIndex + } val simpleSelection = EditorSelection.single(lineStart + offset) - selectionPattern.findFirstMatchIn(partiallyCleanedPattern).map { regexMatch => { - val offset = regexMatch.group(0).length - regexMatch.group(1).length - val selection = EditorSelection.single(lineStart + regexMatch.start, lineStart + regexMatch.end - offset) - val adjustedInsertString = partiallyCleanedPattern.substring(0, regexMatch.start) + - regexMatch.group(1) + - partiallyCleanedPattern.substring(regexMatch.end, partiallyCleanedPattern.length) - - (selection, adjustedInsertString) - }}.getOrElse(simpleSelection, partiallyCleanedPattern) + selectionPattern + .findFirstMatchIn(partiallyCleanedPattern) + .map { regexMatch => + { + val offset = regexMatch.group(0).length - regexMatch.group(1).length + val selection = EditorSelection.single(lineStart + regexMatch.start, lineStart + regexMatch.end - offset) + val adjustedInsertString = partiallyCleanedPattern.substring(0, regexMatch.start) + + regexMatch.group(1) + + partiallyCleanedPattern.substring(regexMatch.end, partiallyCleanedPattern.length) + + (selection, adjustedInsertString) + } + } + .getOrElse(simpleSelection, partiallyCleanedPattern) } /* * Creates edit transaction for completion. This enables cursor to be in proper possition after completion is accpeted */ - private def createEditTransaction(view: EditorView, completion: api.CompletionItemDTO, currentCursorPosition: Int): TransactionSpec = { + private def createEditTransaction( + view: EditorView, + completion: api.CompletionItemDTO, + currentCursorPosition: Int + ): TransactionSpec = { val startLinePos = view.state.doc.line(completion.instructions.editRange.startLine).from - val endLinePos = view.state.doc.line(completion.instructions.editRange.endLine).from - val fromPos = startLinePos + completion.instructions.editRange.startChar - val toPos = endLinePos + completion.instructions.editRange.endChar + val endLinePos = view.state.doc.line(completion.instructions.editRange.endLine).from + val fromPos = startLinePos + completion.instructions.editRange.startChar + val toPos = endLinePos + completion.instructions.editRange.endChar val newCursorStartLine = fromPos + completion.additionalInsertInstructions.foldLeft(0)(_ + _.text.length) val (selection, insertText) = prepareInsertionText(completion, newCursorStartLine.toInt) - TransactionSpec().setChangesVarargs( - (js.Dynamic.literal( - from = fromPos.toDouble, - to = toPos.toDouble max currentCursorPosition, - insert = insertText - ).asInstanceOf[ChangeSpec] +: createAdditionalTextEdits(completion.additionalInsertInstructions, view)):_* - ).setSelection(selection) + TransactionSpec() + .setChangesVarargs( + (js.Dynamic + .literal( + from = fromPos.toDouble, + to = toPos.toDouble max currentCursorPosition, + insert = insertText + ) + .asInstanceOf[ChangeSpec] +: createAdditionalTextEdits(completion.additionalInsertInstructions, view)): _* + ) + .setSelection(selection) } type CompletionInfoF = js.Function1[Completion, js.Promise[dom.Node]] @@ -93,18 +111,22 @@ trait MetalsAutocompletion extends MetalsClient with DebouncingCapabilities { */ private def getCompletionInfo(completionItemDTO: api.CompletionItemDTO): CompletionInfoF = { val key = completionItemDTO.symbol.getOrElse(completionItemDTO.label) - lazy val maybeCachedResult = completionInfoCache.get(key) + lazy val maybeCachedResult = completionInfoCache + .get(key) .map(node => js.Promise.resolve[dom.Node](node)) .getOrElse { - makeRequest(api.CompletionInfoRequest(scastieMetalsOptions, completionItemDTO), "completionItemResolve") - .map { maybeText => - parseMetalsResponse[String](maybeText).filter(_.nonEmpty).map { completionInfo => - val node = dom.document.createElement("div") - node.innerHTML = InteractiveProvider.marked(completionInfo) - completionInfoCache.put(key, node) - node - }.getOrElse(null) - }.toJSPromise + makeRequest(api.CompletionInfoRequest(scastieMetalsOptions, completionItemDTO), "completionItemResolve").map { + maybeText => + parseMetalsResponse[String](maybeText) + .filter(_.nonEmpty) + .map { completionInfo => + val node = dom.document.createElement("div") + node.innerHTML = InteractiveProvider.marked(completionInfo) + completionInfoCache.put(key, node) + node + } + .getOrElse(null) + }.toJSPromise } val result: CompletionInfoF = (completion: Completion) => maybeCachedResult @@ -117,8 +139,8 @@ trait MetalsAutocompletion extends MetalsClient with DebouncingCapabilities { if (!matchesPreviousToken) wasPreviousIncomplete = true }) - private val completionsF: js.Function1[CompletionContext, js.Promise[CompletionResult]] = { - ctx => ifSupported { + private val completionsF: js.Function1[CompletionContext, js.Promise[CompletionResult]] = { ctx => + ifSupported { val word = ctx.matchBefore(jsRegex).asInstanceOf[anon.Text] if (!ctx.explicit || (word == null || word.text.isEmpty || (word.from == word.to))) { @@ -128,13 +150,20 @@ trait MetalsAutocompletion extends MetalsClient with DebouncingCapabilities { previousWord = word.text val request = toLSPRequest(ctx.state.doc.toString(), ctx.pos.toInt) - val from = if (word.text.headOption == Some('.')) word.from + 1 else word.from + val from = if (word.text.headOption == Some('.')) word.from + 1 else word.from makeRequest(request, "complete").map(maybeText => parseMetalsResponse[api.ScalaCompletionList](maybeText).map { completionList => val completions = completionList.items.map { - case cmp @ api.CompletionItemDTO(name, detail, tpe, boost, insertInstructions, additionalInsertInstructions, symbol) => - Completion(name.stripSuffix(detail)) + case cmp @ api.CompletionItemDTO( + name, + detail, + tpe, + boost, + insertInstructions, + additionalInsertInstructions, + symbol + ) => Completion(name.stripSuffix(detail)) .setDetail(detail) .setInfo(getCompletionInfo(cmp)) .setType(tpe) @@ -142,8 +171,7 @@ trait MetalsAutocompletion extends MetalsClient with DebouncingCapabilities { .setApplyFunction4((view, _, from, to) => { wasPreviousIncomplete = false Callback(view.dispatch(createEditTransaction(view, cmp, to.toInt))) - } - ) + }) } wasPreviousIncomplete = completionList.isIncomplete val result = CompletionResult(from, completions.toJSArray) @@ -158,12 +186,15 @@ trait MetalsAutocompletion extends MetalsClient with DebouncingCapabilities { private val autocompletionConfig = CompletionConfig() .setInteractionDelay(0) // we want completions to work instantly .setOverrideVarargs(completionsF) - .setActivateOnTyping(false) // we use our own autocompletion trigger with working debounce MetalsAutocompletion.autocompletionTrigger + .setActivateOnTyping( + false + ) // we use our own autocompletion trigger with working debounce MetalsAutocompletion.autocompletionTrigger .setIcons(true) .setDefaultKeymap(true) def metalsAutocomplete: js.Array[Any] = js.Array[Any]( autocompletion(autocompletionConfig), - autocompletionTrigger, + autocompletionTrigger ) + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsClient.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsClient.scala index 59f7e2acf..221016865 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsClient.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsClient.scala @@ -1,22 +1,21 @@ package com.olegych.scastie.client.components.editor +import scala.concurrent.Future +import scala.util.Failure +import scala.util.Success + import com.olegych.scastie.api import com.olegych.scastie.api.EitherFormat.JsEither._ import com.olegych.scastie.client._ import japgolly.scalajs.react._ +import js.JSConverters._ import org.scalajs.dom import play.api.libs.json.Json import play.api.libs.json.Reads import play.api.libs.json.Writes - -import scala.concurrent.Future -import scala.util.Failure -import scala.util.Success - -import scalajs.js import scalajs.concurrent.JSExecutionContext.Implicits.queue +import scalajs.js import scalajs.js.Thenable.Implicits._ -import js.JSConverters._ trait MetalsClient { val updateStatus: MetalsStatus ~=> Callback @@ -35,9 +34,9 @@ trait MetalsClient { parseMetalsResponse[Boolean](maybeText).getOrElse(false) ) res.onComplete { - case Success(true) => updateStatus(MetalsReady).runNow() + case Success(true) => updateStatus(MetalsReady).runNow() case Failure(exception) => updateStatus(NetworkError(exception.getMessage)).runNow() - case _ => + case _ => } res } @@ -47,15 +46,17 @@ trait MetalsClient { * Runs function `f` only when current scastie configuration is supported. */ protected def ifSupported[A](f: => Future[Option[A]]): js.Promise[Option[A]] = { - isConfigurationSupported.flatMap(isSupported => { - if (isSupported) { - updateStatus(MetalsLoading).runNow() - val res = f.map(Option(_)) - res.onComplete(_ => updateStatus(MetalsReady).runNow()) - res - } else - Future.successful(None) - }).map(_.flatten).toJSPromise + isConfigurationSupported + .flatMap(isSupported => { + if (isSupported) { + updateStatus(MetalsLoading).runNow() + val res = f.map(Option(_)) + res.onComplete(_ => updateStatus(MetalsReady).runNow()) + res + } else Future.successful(None) + }) + .map(_.flatten) + .toJSPromise } protected def toLSPRequest(code: String, offset: Int): api.LSPRequestDTO = { @@ -63,21 +64,29 @@ trait MetalsClient { api.LSPRequestDTO(scastieMetalsOptions, offsetParams) } - protected def makeRequest[A](req: A, endpoint: String)(implicit writes: Writes[A]): Future[Option[String]] = { + protected def makeRequest[A](req: A, endpoint: String)( + implicit writes: Writes[A] + ): Future[Option[String]] = { val location = dom.window.location // this is workaround until we migrate all services to proper docker setup or unify the servers - val apiBase = if (location.hostname == "localhost") { - location.protocol ++ "//" ++ location.hostname + ":" ++ "8000" - } else "" + val apiBase = + if (location.hostname == "localhost") { + location.protocol ++ "//" ++ location.hostname + ":" ++ "8000" + } else "" // We don't support metals in embedded so we don't need to map server url - val request = dom.fetch(s"$apiBase/metals/$endpoint", js.Dynamic.literal( - body = Json.toJson(req).toString, - method = dom.HttpMethod.POST - ).asInstanceOf[dom.RequestInit]) + val request = dom.fetch( + s"$apiBase/metals/$endpoint", + js.Dynamic + .literal( + body = Json.toJson(req).toString, + method = dom.HttpMethod.POST + ) + .asInstanceOf[dom.RequestInit] + ) for { - res <- request + res <- request text <- res.text() } yield { if (res.ok) Some(text) @@ -88,20 +97,21 @@ trait MetalsClient { } } - protected def parseMetalsResponse[A](maybeJsonText: Option[String])(implicit readsB: Reads[A]): Option[A] = { + protected def parseMetalsResponse[A](maybeJsonText: Option[String])( + implicit readsB: Reads[A] + ): Option[A] = { maybeJsonText.flatMap(jsonText => { Json.parse(jsonText).asOpt[Either[api.FailureType, A]] match { - case None => - None + case None => None case Some(Left(api.PresentationCompilerFailure(msg))) => updateStatus(MetalsConfigurationError(msg)).runNow() None - case Some(Left(api.NoResult(msg))) => - None + case Some(Left(api.NoResult(msg))) => None case Some(Right(value)) => updateStatus(MetalsReady) Some(value) } }) } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsHover.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsHover.scala index 3484551ce..e2042ce02 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsHover.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/MetalsHover.scala @@ -2,35 +2,37 @@ package com.olegych.scastie.client.components.editor import com.olegych.scastie.api import japgolly.scalajs.react._ +import js.JSConverters._ import org.scalajs.dom -import typings.codemirrorState.mod._ -import typings.codemirrorView.mod._ - -import scalajs.js import scalajs.concurrent.JSExecutionContext.Implicits.queue +import scalajs.js import scalajs.js.Thenable.Implicits._ -import js.JSConverters._ +import typings.codemirrorState.mod._ +import typings.codemirrorView.mod._ trait MetalsHover extends MetalsClient { - private val hovers = hoverTooltip((view, pos, _) => ifSupported { - val request = toLSPRequest(view.state.doc.toString(), pos.toInt) - makeRequest(request, "hover").map(maybeText => - parseMetalsResponse[api.HoverDTO](maybeText).map { hover => - val hoverF: js.Function1[EditorView, TooltipView] = _ => { - val node = dom.document.createElement("div") - node.innerHTML = InteractiveProvider.marked(hover.content) - TooltipView(node.domToHtml.get) - } + private val hovers = hoverTooltip((view, pos, _) => + ifSupported { + val request = toLSPRequest(view.state.doc.toString(), pos.toInt) + + makeRequest(request, "hover").map(maybeText => + parseMetalsResponse[api.HoverDTO](maybeText).map { hover => + val hoverF: js.Function1[EditorView, TooltipView] = _ => { + val node = dom.document.createElement("div") + node.innerHTML = InteractiveProvider.marked(hover.content) + TooltipView(node.domToHtml.get) + } - view.state.wordAt(pos) match { - case range: SelectionRange => Tooltip(hoverF, range.from) - .setEnd(range.to) - case _ => Tooltip(hoverF, pos) + view.state.wordAt(pos) match { + case range: SelectionRange => Tooltip(hoverF, range.from) + .setEnd(range.to) + case _ => Tooltip(hoverF, pos) + } } - } - ) - }.map(_.getOrElse(null)).toJSPromise) + ) + }.map(_.getOrElse(null)).toJSPromise + ) def metalsHover = hovers } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/OnChangeHandler.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/OnChangeHandler.scala index e8a0e208a..c46e00f37 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/OnChangeHandler.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/OnChangeHandler.scala @@ -1,11 +1,10 @@ package com.olegych.scastie.client.components.editor import japgolly.scalajs.react._ +import scalajs.js import typings.codemirrorState.mod._ import typings.codemirrorView.mod._ -import scalajs.js - class OnChangeHandler(onChange: String ~=> Callback) extends js.Object { private def scalaUpdate: js.Function1[ViewUpdate, Unit] = viewUpdate => { @@ -19,6 +18,5 @@ class OnChangeHandler(onChange: String ~=> Callback) extends js.Object { } object OnChangeHandler { - def apply(onChange: String ~=> Callback): Extension = - ViewPlugin.define(_ => new OnChangeHandler(onChange)).extension + def apply(onChange: String ~=> Callback): Extension = ViewPlugin.define(_ => new OnChangeHandler(onChange)).extension } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/SimpleEditor.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/SimpleEditor.scala index 3bb6dad4a..67e0c814a 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/SimpleEditor.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/SimpleEditor.scala @@ -1,75 +1,78 @@ package com.olegych.scastie.client.components.editor import com.olegych.scastie.client.components.editor.OnChangeHandler +import hooks.Hooks.UseStateF import japgolly.scalajs.react._ import org.scalajs.dom.Element +import scalajs.js import typings.codemirrorLanguage.mod import typings.codemirrorState.mod._ import typings.codemirrorView.mod._ - -import scalajs.js import vdom.all._ -import hooks.Hooks.UseStateF final case class SimpleEditor( - readOnly: Boolean, - value: String, - isDarkTheme: Boolean, - onChange: String ~=> Callback, + readOnly: Boolean, + value: String, + isDarkTheme: Boolean, + onChange: String ~=> Callback ) extends Editor { @inline def render: VdomElement = SimpleEditor.hooksComponent(this) } object SimpleEditor { - private def init(props: SimpleEditor, ref: Ref.Simple[Element], editorView: UseStateF[CallbackTo, EditorView]): Callback = - ref.foreachCB(divRef => { - val basicExtensions = js.Array[Any]( - Editor.editorTheme.of(props.codemirrorTheme), - Editor.indentationMarkersExtension, - typings.codemirror.mod.minimalSetup, - mod.StreamLanguage.define(typings.codemirrorLegacyModes.modeClikeMod.scala_), - SyntaxHighlightingTheme.highlightingTheme, - ) - lazy val readOnlyExtensions = js.Array[Any]( - EditorState.readOnly.of(true), - ) - lazy val editableExtensions = js.Array[Any]( - lineNumbers(), - OnChangeHandler(props.onChange), - ) - val editorStateConfig = EditorStateConfig() - .setDoc(props.value) - .setExtensions { - (if (props.readOnly) readOnlyExtensions else editableExtensions) ++ basicExtensions - } + private def init( + props: SimpleEditor, + ref: Ref.Simple[Element], + editorView: UseStateF[CallbackTo, EditorView] + ): Callback = ref.foreachCB(divRef => { + val basicExtensions = js.Array[Any]( + Editor.editorTheme.of(props.codemirrorTheme), + Editor.indentationMarkersExtension, + typings.codemirror.mod.minimalSetup, + mod.StreamLanguage.define(typings.codemirrorLegacyModes.modeClikeMod.scala_), + SyntaxHighlightingTheme.highlightingTheme + ) + lazy val readOnlyExtensions = js.Array[Any]( + EditorState.readOnly.of(true) + ) + lazy val editableExtensions = js.Array[Any]( + lineNumbers(), + OnChangeHandler(props.onChange) + ) + val editorStateConfig = EditorStateConfig() + .setDoc(props.value) + .setExtensions { + (if (props.readOnly) readOnlyExtensions else editableExtensions) ++ basicExtensions + } - val editor = new EditorView(EditorViewConfig() + val editor = new EditorView( + EditorViewConfig() .setState(EditorState.create(editorStateConfig)) .setParent(divRef) - ) + ) - editorView.setState(editor) - }) + editorView.setState(editor) + }) private def updateComponent( - props: SimpleEditor, - ref: Ref.Simple[Element], - prevProps: Option[SimpleEditor], - editorView: UseStateF[CallbackTo, EditorView] + props: SimpleEditor, + ref: Ref.Simple[Element], + prevProps: Option[SimpleEditor], + editorView: UseStateF[CallbackTo, EditorView] ): Callback = { Editor.updateCode(editorView, props) >> Editor.updateTheme(ref, prevProps, props, editorView) } - val hooksComponent = - ScalaFnComponent - .withHooks[SimpleEditor] - .useRef(Ref[Element]) - .useState(new EditorView()) - .useRef[Option[SimpleEditor]](None) - .useLayoutEffectOnMountBy((props, ref, editorView, prevProps) => init(props, ref.value, editorView)) - .useEffectBy((props, ref, editorRef, prevProps) => updateComponent(props, ref.value, prevProps.value, editorRef)) - .useEffectBy((props, _, editorRef, prevProps) => prevProps.set(Some(props))) - .render((props, ref, _, prevProps) => Editor.render(ref.value)) + val hooksComponent = ScalaFnComponent + .withHooks[SimpleEditor] + .useRef(Ref[Element]) + .useState(new EditorView()) + .useRef[Option[SimpleEditor]](None) + .useLayoutEffectOnMountBy((props, ref, editorView, prevProps) => init(props, ref.value, editorView)) + .useEffectBy((props, ref, editorRef, prevProps) => updateComponent(props, ref.value, prevProps.value, editorRef)) + .useEffectBy((props, _, editorRef, prevProps) => prevProps.set(Some(props))) + .render((props, ref, _, prevProps) => Editor.render(ref.value)) + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingHandler.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingHandler.scala index b1868ae7d..e8faee49d 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingHandler.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingHandler.scala @@ -1,33 +1,34 @@ package com.olegych.scastie.client.components.editor -import typings.codemirrorState.mod.ChangeSet -import typings.codemirrorState.mod._ -import typings.codemirrorView.mod._ -import typings.webTreeSitter.mod._ - import scala.collection.mutable.ListBuffer import scalajs.js +import typings.codemirrorState.mod._ +import typings.codemirrorState.mod.ChangeSet +import typings.codemirrorView.mod._ +import typings.webTreeSitter.mod._ -class SyntaxHighlightingHandler(parser: Parser, language: Language, query: Query, initialState: String) extends js.Object { +class SyntaxHighlightingHandler(parser: Parser, language: Language, query: Query, initialState: String) + extends js.Object { val queryCaptureNames = query.captureNames - var tree = parser.parse(initialState) + var tree = parser.parse(initialState) var decorations: DecorationSet = computeDecorations() private def computeDecorations(): DecorationSet = { val rangeSetBuilder = new RangeSetBuilder[Decoration]() - val captures = query.captures(tree.rootNode) + val captures = query.captures(tree.rootNode) - captures.foldLeft(Option.empty[QueryCapture]){ (previousCapture, currentCapture) => + captures.foldLeft(Option.empty[QueryCapture]) { (previousCapture, currentCapture) => if (!previousCapture.exists(_ == currentCapture)) { val startPosition = currentCapture.node.startIndex - val endPosition = currentCapture.node.endIndex + val endPosition = currentCapture.node.endIndex val mark = Decoration.mark( MarkDecorationSpec() .setInclusive(true) - .setClass(currentCapture.name.replace(".", "-"))) + .setClass(currentCapture.name.replace(".", "-")) + ) rangeSetBuilder.add(startPosition, endPosition, mark) } @@ -45,12 +46,14 @@ class SyntaxHighlightingHandler(parser: Parser, language: Language, query: Query private def mapChangesToTSEdits(changes: ChangeSet, originalText: Text, newText: Text): List[Edit] = { val editBuffer = new ListBuffer[Edit]() - changes.iterChanges { (fromA: Double, toA: Double, _, toB: Double, _) => { - val oldEndPosition = indexToTSPoint(originalText, toA) - val newEndPosition = indexToTSPoint(newText, toB) - val startPosition = indexToTSPoint(originalText, fromA) - editBuffer.addOne(Edit(toB, newEndPosition, toA, oldEndPosition, fromA, startPosition)) - }} + changes.iterChanges { (fromA: Double, toA: Double, _, toB: Double, _) => + { + val oldEndPosition = indexToTSPoint(originalText, toA) + val newEndPosition = indexToTSPoint(newText, toB) + val startPosition = indexToTSPoint(originalText, fromA) + editBuffer.addOne(Edit(toB, newEndPosition, toA, oldEndPosition, fromA, startPosition)) + } + } editBuffer.toList @@ -59,10 +62,11 @@ class SyntaxHighlightingHandler(parser: Parser, language: Language, query: Query var update: js.Function1[ViewUpdate, Unit] = viewUpdate => { if (viewUpdate.docChanged) { val newText = viewUpdate.state.doc.toString - val edits = mapChangesToTSEdits(viewUpdate.changes, viewUpdate.startState.doc, viewUpdate.state.doc) + val edits = mapChangesToTSEdits(viewUpdate.changes, viewUpdate.startState.doc, viewUpdate.state.doc) edits.foreach(tree.edit) tree = parser.parse(newText, tree) decorations = computeDecorations() } } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingPlugin.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingPlugin.scala index 5b0ac9252..0e9110c9e 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingPlugin.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingPlugin.scala @@ -1,45 +1,45 @@ package com.olegych.scastie.client.components.editor -import typings.webTreeSitter.mod._ +import japgolly.scalajs.react._ import org.scalablytyped.runtime.StObject +import org.scalajs.dom import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._ -import japgolly.scalajs.react._ +import scalajs.js import typings.codemirrorState.mod._ import typings.codemirrorView.mod._ -import scalajs.js -import org.scalajs.dom +import typings.webTreeSitter.mod._ class SyntaxHighlightingPlugin(editorView: hooks.Hooks.UseStateF[CallbackTo, EditorView]) { val syntaxHighlightingExtension = new Compartment() - val fallbackExtension = typings.codemirrorLanguage.mod.StreamLanguage.define(typings.codemirrorLegacyModes.modeClikeMod.scala_).extension + val fallbackExtension = + typings.codemirrorLanguage.mod.StreamLanguage.define(typings.codemirrorLegacyModes.modeClikeMod.scala_).extension val location = dom.window.location + // this is workaround until we migrate all services to proper docker setup or unify the servers - val apiBase = if (location.hostname == "localhost") { - location.protocol ++ "//" ++ location.hostname + ":" ++ "9000" - } else if (location.protocol == "file:") { - "http://localhost:9000" - } else { - "https://scastie.scala-lang.org" - } + val apiBase = + if (location.hostname == "localhost") { + location.protocol ++ "//" ++ location.hostname + ":" ++ "9000" + } else if (location.protocol == "file:") { + "http://localhost:9000" + } else { + "https://scastie.scala-lang.org" + } val initOptions = new js.Object { - val apiBaseField = apiBase - def locateFile(scriptName: String, scriptDirectory: String): String = - s"$apiBaseField/public/tree-sitter.wasm" + val apiBaseField = apiBase + def locateFile(scriptName: String, scriptDirectory: String): String = s"$apiBaseField/public/tree-sitter.wasm" } - private val fetchTSWasm = init(initOptions) - .toFuture + private val fetchTSWasm = init(initOptions).toFuture .flatMap(_ => Language.load(s"$apiBase/public/tree-sitter-scala.wasm").toFuture) - val highlightQuery = dom.fetch(s"$apiBase/public/highlights.scm") for { language <- fetchTSWasm - query <- highlightQuery.toFuture - text <- query.text().toFuture + query <- highlightQuery.toFuture + text <- query.text().toFuture } yield { val parser = new TreesitterParser() parser.setLanguage(language) @@ -48,16 +48,21 @@ class SyntaxHighlightingPlugin(editorView: hooks.Hooks.UseStateF[CallbackTo, Edi } def switchToTreesitterParser(scalaParser: Parser, language: Language, query: Query): Unit = { - val extension = ViewPlugin.define(editorView => - new SyntaxHighlightingHandler(scalaParser, language, query, editorView.state.doc.toString), - PluginSpec[SyntaxHighlightingHandler]().setDecorations(_.decorations) - ).extension + val extension = ViewPlugin + .define( + editorView => new SyntaxHighlightingHandler(scalaParser, language, query, editorView.state.doc.toString), + PluginSpec[SyntaxHighlightingHandler]().setDecorations(_.decorations) + ) + .extension - val effects = syntaxHighlightingExtension.reconfigure(extension) + val effects = syntaxHighlightingExtension.reconfigure(extension) val transactionSpec = TransactionSpec().setEffects(effects) - editorView.modState(editorView => { + editorView + .modState(editorView => { editorView.dispatch(transactionSpec) editorView - }).runNow() + }) + .runNow() } + } diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingTheme.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingTheme.scala index 8c8fef442..d779fa5ef 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingTheme.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/SyntaxHighlightingTheme.scala @@ -1,10 +1,9 @@ package com.olegych.scastie.client.components.editor +import scalajs.js import typings.codemirrorLanguage.mod import typings.lezerHighlight.mod.tags -import scalajs.js - object SyntaxHighlightingTheme { private val highlightStyle = mod.HighlightStyle.define( @@ -63,7 +62,7 @@ object SyntaxHighlightingTheme { mod.TagStyle(tags.typeName).setClass("type"), mod.TagStyle(tags.typeOperator).setClass("type-qualifier"), mod.TagStyle(tags.unit).setClass("none"), - mod.TagStyle(tags.variableName).setClass("function"), + mod.TagStyle(tags.variableName).setClass("function") ) ) diff --git a/client/src/main/scala/com.olegych.scastie.client/components/editor/TreesitterParser.scala b/client/src/main/scala/com.olegych.scastie.client/components/editor/TreesitterParser.scala index b9b9d3400..9cfdb341e 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/editor/TreesitterParser.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/editor/TreesitterParser.scala @@ -1,13 +1,12 @@ package com.olegych.scastie.client.components.editor -import typings.webTreeSitter.mod.Parser +import scala.scalajs.js.annotation.JSGlobal + import org.scalablytyped.runtime.StObject import scalajs.js import scalajs.js.annotation.JSImport -import scala.scalajs.js.annotation.JSGlobal +import typings.webTreeSitter.mod.Parser @JSGlobal("Treesitter") @js.native -class TreesitterParser() extends StObject with Parser { - -} +class TreesitterParser() extends StObject with Parser {} diff --git a/client/src/main/scala/com.olegych.scastie.client/components/package.scala b/client/src/main/scala/com.olegych.scastie.client/components/package.scala index 48cfb7c85..825d1680b 100644 --- a/client/src/main/scala/com.olegych.scastie.client/components/package.scala +++ b/client/src/main/scala/com.olegych.scastie.client/components/package.scala @@ -1,101 +1,71 @@ package com.olegych.scastie.client import com.olegych.scastie.api._ - +import japgolly.scalajs.react.Callback import japgolly.scalajs.react.Reusability import japgolly.scalajs.react.Reusable -import japgolly.scalajs.react.Callback import org.scalajs.dom.HTMLElement package object components { val reusableEmpty: Reusable[Callback] = Reusable.always(Callback.empty) - implicit val reusabilityInputs: Reusability[Inputs] = - Reusability.byRefOr_== + implicit val reusabilityInputs: Reusability[Inputs] = Reusability.byRefOr_== - implicit val reusabilityUser: Reusability[User] = - Reusability.byRef || Reusability.derive[User] + implicit val reusabilityUser: Reusability[User] = Reusability.byRef || Reusability.derive[User] - implicit val snippetIdReuse: Reusability[SnippetId] = - Reusability.byRefOr_== + implicit val snippetIdReuse: Reusability[SnippetId] = Reusability.byRefOr_== - implicit val viewReuse: Reusability[View] = - Reusability.byRefOr_== + implicit val viewReuse: Reusability[View] = Reusability.byRefOr_== - implicit val scalaTargetReuse: Reusability[ScalaTarget] = - Reusability.byRefOr_== + implicit val scalaTargetReuse: Reusability[ScalaTarget] = Reusability.byRefOr_== - implicit val pageReuse: Reusability[Page] = - Reusability.byRefOr_== + implicit val pageReuse: Reusability[Page] = Reusability.byRefOr_== - implicit val scalaTargetTypeReuse: Reusability[ScalaTargetType] = - Reusability.byRefOr_== + implicit val scalaTargetTypeReuse: Reusability[ScalaTargetType] = Reusability.byRefOr_== - implicit val scalaScalaDependency: Reusability[ScalaDependency] = - Reusability.byRefOr_== + implicit val scalaScalaDependency: Reusability[ScalaDependency] = Reusability.byRefOr_== - implicit val attachedDomsReuse: Reusability[Map[String, HTMLElement]] = - Reusability.byRef || - Reusability.by(_.keys.toSet) + implicit val attachedDomsReuse: Reusability[Map[String, HTMLElement]] = Reusability.byRef || + Reusability.by(_.keys.toSet) - implicit val releaseOptionsReuse: Reusability[ReleaseOptions] = - Reusability.byRefOr_== + implicit val releaseOptionsReuse: Reusability[ReleaseOptions] = Reusability.byRefOr_== - implicit val projectReuse: Reusability[Project] = - Reusability.byRefOr_== + implicit val projectReuse: Reusability[Project] = Reusability.byRefOr_== - implicit val librariesFromReuse: Reusability[Map[ScalaDependency, Project]] = - Reusability.byRefOr_== + implicit val librariesFromReuse: Reusability[Map[ScalaDependency, Project]] = Reusability.byRefOr_== - implicit val instrumentationReuse: Reusability[Set[Instrumentation]] = - Reusability.byRefOr_== + implicit val instrumentationReuse: Reusability[Set[Instrumentation]] = Reusability.byRefOr_== - implicit val compilationInfosReuse: Reusability[Set[Problem]] = - Reusability.byRefOr_== + implicit val compilationInfosReuse: Reusability[Set[Problem]] = Reusability.byRefOr_== - implicit val runtimeErrorReuse: Reusability[Option[RuntimeError]] = - Reusability.byRefOr_== + implicit val runtimeErrorReuse: Reusability[Option[RuntimeError]] = Reusability.byRefOr_== - implicit val consoleOutputsReuse: Reusability[Vector[ConsoleOutput]] = - Reusability.byRefOr_== + implicit val consoleOutputsReuse: Reusability[Vector[ConsoleOutput]] = Reusability.byRefOr_== - implicit val snippetSummaryReuse: Reusability[List[SnippetSummary]] = - Reusability.byRefOr_== + implicit val snippetSummaryReuse: Reusability[List[SnippetSummary]] = Reusability.byRefOr_== - implicit val consoleStateReuse: Reusability[ConsoleState] = - Reusability.byRefOr_== + implicit val consoleStateReuse: Reusability[ConsoleState] = Reusability.byRefOr_== - implicit def reusabilityEventStream[T]: Reusability[EventStream[T]] = - Reusability.always + implicit def reusabilityEventStream[T]: Reusability[EventStream[T]] = Reusability.always - implicit val modalStateReuse: Reusability[ModalState] = - Reusability.derive[ModalState] + implicit val modalStateReuse: Reusability[ModalState] = Reusability.derive[ModalState] - implicit val snippetStateReuse: Reusability[SnippetState] = - Reusability.derive[SnippetState] + implicit val snippetStateReuse: Reusability[SnippetState] = Reusability.derive[SnippetState] - implicit val consoleOutputReuse: Reusability[ConsoleOutput] = - Reusability.byRefOr_== + implicit val consoleOutputReuse: Reusability[ConsoleOutput] = Reusability.byRefOr_== - implicit val outputsReuse: Reusability[Outputs] = - Reusability.derive[Outputs] + implicit val outputsReuse: Reusability[Outputs] = Reusability.derive[Outputs] - implicit val sbtRunnerStateReuse: Reusability[Option[Vector[SbtRunnerState]]] = - Reusability.byRefOr_== + implicit val sbtRunnerStateReuse: Reusability[Option[Vector[SbtRunnerState]]] = Reusability.byRefOr_== - implicit val statusStateReuse: Reusability[StatusState] = - Reusability.derive[StatusState] + implicit val statusStateReuse: Reusability[StatusState] = Reusability.derive[StatusState] - implicit val embeddedOptionsReuse: Reusability[EmbeddedOptions] = - Reusability.derive[EmbeddedOptions] + implicit val embeddedOptionsReuse: Reusability[EmbeddedOptions] = Reusability.derive[EmbeddedOptions] - implicit val metalsStatusReuse: Reusability[MetalsStatus] = - Reusability.byRefOr_== + implicit val metalsStatusReuse: Reusability[MetalsStatus] = Reusability.byRefOr_== - implicit val scastieStateReuse: Reusability[ScastieState] = - Reusability.derive[ScastieState] + implicit val scastieStateReuse: Reusability[ScastieState] = Reusability.derive[ScastieState] - implicit val scastieBackendReuse: Reusability[ScastieBackend] = - Reusability.byRefOr_== + implicit val scastieBackendReuse: Reusability[ScastieBackend] = Reusability.byRefOr_== } diff --git a/client/src/main/scala/com.olegych.scastie.client/package.scala b/client/src/main/scala/com.olegych.scastie.client/package.scala index ac7f8756b..c4304bed7 100644 --- a/client/src/main/scala/com.olegych.scastie.client/package.scala +++ b/client/src/main/scala/com.olegych.scastie.client/package.scala @@ -1,23 +1,21 @@ package com.olegych.scastie -import play.api.libs.json._ - import org.scalajs.dom.window +import play.api.libs.json._ package object client { def dontSerialize[T](fallback: T): Format[T] = new Format[T] { - def writes(v: T): JsValue = JsNull + def writes(v: T): JsValue = JsNull def reads(json: JsValue): JsResult[T] = JsSuccess(fallback) } def dontSerializeOption[T]: Format[T] = new Format[T] { - def writes(v: T): JsValue = JsNull + def writes(v: T): JsValue = JsNull def reads(json: JsValue): JsResult[T] = JsSuccess(null.asInstanceOf[T]) } - def dontSerializeList[T]: Format[List[T]] = - dontSerialize(List()) + def dontSerializeList[T]: Format[List[T]] = dontSerialize(List()) val isMac: Boolean = window.navigator.userAgent.contains("Mac") val isMobile: Boolean = "Android|webOS|Mobi|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Samsung".r.unanchored diff --git a/deployment/announce.scala b/deployment/announce.scala index 71688c536..592e8c1fe 100644 --- a/deployment/announce.scala +++ b/deployment/announce.scala @@ -1,34 +1,34 @@ // scp scastie@scastie.scala-lang.org:users.txt users.txt + object Announce { import java.nio.file._ val users = Files.readAllLines(Paths.get("users.txt")).toArray val slice = 50 - val from = 800 + val from = 800 - users.drop(from).sliding(slice, slice).toList.zipWithIndex.foreach { - case (group, i) => - val start = from + i * slice - val end = start + group.size + users.drop(from).sliding(slice, slice).toList.zipWithIndex.foreach { case (group, i) => + val start = from + i * slice + val end = start + group.size - val message = - s"""|Scastie Beta Opens ($start - $end) - | - |Hello, - | - |Thanks for signing up to the Beta. We are now opening spots $start to $end! - | - |This means you can go to https://scastie.scala-lang.org and you will now have access. Oh, and please, please, please give us feedback! - | - |${group.map(user => "@" + user).mkString(System.lineSeparator)} - | - |Thanks, - |Guillaume""".stripMargin + val message = + s"""|Scastie Beta Opens ($start - $end) + | + |Hello, + | + |Thanks for signing up to the Beta. We are now opening spots $start to $end! + | + |This means you can go to https://scastie.scala-lang.org and you will now have access. Oh, and please, please, please give us feedback! + | + |${group.map(user => "@" + user).mkString(System.lineSeparator)} + | + |Thanks, + |Guillaume""".stripMargin - val dest = Paths.get("beta", start.toString) - if (Files.exists(dest)) { - Files.delete(dest) - } - Files.write(dest, message.getBytes, StandardOpenOption.CREATE_NEW) + val dest = Paths.get("beta", start.toString) + if (Files.exists(dest)) { + Files.delete(dest) + } + Files.write(dest, message.getBytes, StandardOpenOption.CREATE_NEW) } } diff --git a/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/Instrument.scala b/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/Instrument.scala index 179510956..b1aa28780 100644 --- a/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/Instrument.scala +++ b/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/Instrument.scala @@ -1,112 +1,108 @@ package com.olegych.scastie.instrumentation -import com.olegych.scastie.api.ScalaTarget._ -import com.olegych.scastie.api.{Inputs, Instrumentation, ScalaTarget, ScalaTargetType} - import scala.collection.immutable.Seq import scala.meta._ import scala.meta.inputs.Position import scala.meta.parsers.Parsed import scala.util.control.NonFatal +import com.olegych.scastie.api.{Inputs, Instrumentation, ScalaTarget, ScalaTargetType} +import com.olegych.scastie.api.ScalaTarget._ + sealed trait InstrumentationFailure object InstrumentationFailure { - case object HasMainMethod extends InstrumentationFailure - case object UnsupportedDialect extends InstrumentationFailure - case class ParsingError(error: Parsed.Error) extends InstrumentationFailure + case object HasMainMethod extends InstrumentationFailure + case object UnsupportedDialect extends InstrumentationFailure + case class ParsingError(error: Parsed.Error) extends InstrumentationFailure case class InternalError(exception: Throwable) extends InstrumentationFailure } object Instrument { + def getParsingLineOffset(inputs: Inputs): Int = { if (inputs.isWorksheetMode) -1 else 0 } + def getExceptionLineOffset(inputs: Inputs): Int = { if (inputs.isWorksheetMode) -2 else 0 } + def getMessageLineOffset(inputs: Inputs): Int = { if (inputs.isWorksheetMode) -2 else 0 } import InstrumentationFailure._ - private val instrumentedObject = Instrumentation.instrumentedObject + private val instrumentedObject = Instrumentation.instrumentedObject private val instrumentationMethod = "instrumentations$" - private val instrumentationMap = "instrumentationMap$" + private val instrumentationMap = "instrumentationMap$" - private val emptyMapT = "_root_.scala.collection.mutable.Map.empty" - private val jsExportT = "_root_.scala.scalajs.js.annotation.JSExport" - private val jsExportTopLevelT = - "_root_.scala.scalajs.js.annotation.JSExportTopLevel" + private val emptyMapT = "_root_.scala.collection.mutable.Map.empty" + private val jsExportT = "_root_.scala.scalajs.js.annotation.JSExport" + private val jsExportTopLevelT = "_root_.scala.scalajs.js.annotation.JSExportTopLevel" - private val apiPackage = "_root_.com.olegych.scastie.api" - private val positionT = s"$apiPackage.Position" - private val renderT = s"$apiPackage.Render" - private val runtimeErrorT = s"$apiPackage.RuntimeError" + private val apiPackage = "_root_.com.olegych.scastie.api" + private val positionT = s"$apiPackage.Position" + private val renderT = s"$apiPackage.Render" + private val runtimeErrorT = s"$apiPackage.RuntimeError" private val instrumentationT = s"$apiPackage.Instrumentation" - private val runtimeT = s"$apiPackage.runtime.Runtime" - private val domhookT = s"$apiPackage.runtime.DomHook" + private val runtimeT = s"$apiPackage.runtime.Runtime" + private val domhookT = s"$apiPackage.runtime.DomHook" - private val elemArrayT = - "_root_.scala.scalajs.js.Array[_root_.org.scalajs.dom.raw.HTMLElement]" + private val elemArrayT = "_root_.scala.scalajs.js.Array[_root_.org.scalajs.dom.raw.HTMLElement]" private def posToApi(position: Position, offset: Int) = { val (x, y) = position match { - case Position.None => (0, 0) - case Position.Range(_, start, end) => - (start - offset, end - offset) + case Position.None => (0, 0) + case Position.Range(_, start, end) => (start - offset, end - offset) } s"$positionT($x, $y)" } def instrumentOne(term: Term, tpeTree: Option[Type], offset: Int, isScalaJs: Boolean): Patch = { - val treeQuote = - tpeTree match { - case None => s"val $$t = $term" - case Some(tpe) => s"val $$t: $tpe = $term" - } + val treeQuote = tpeTree match { + case None => s"val $$t = $term" + case Some(tpe) => s"val $$t: $tpe = $term" + } val renderCall = if (!isScalaJs) s"$runtimeT.render($$t);" else s"$runtimeT.render($$t, attach _);" - val replacement = - Seq( - "scala.Predef.locally {", - treeQuote + "; ", - s"$instrumentationMap(${posToApi(term.pos, offset)}) = $renderCall", - "$t}" - ).mkString("") + val replacement = Seq( + "scala.Predef.locally {", + treeQuote + "; ", + s"$instrumentationMap(${posToApi(term.pos, offset)}) = $renderCall", + "$t}" + ).mkString("") Patch(term.tokens.head, term.tokens.last, replacement) } private def instrument(source: Source, offset: Int, isScalaJs: Boolean): String = { - val instrumentedCodePatches = - source.stats.collect { - case c: Defn.Object if c.name.value == instrumentedObject => - val openCurlyBrace = c.templ.tokens.find(_.toString == "{").get + val instrumentedCodePatches = source.stats.collect { + case c: Defn.Object if c.name.value == instrumentedObject => + val openCurlyBrace = c.templ.tokens.find(_.toString == "{").get - val instrumentationMapCode = Seq( - s"{ private val $instrumentationMap = $emptyMapT[$positionT, $renderT]", - s"def $instrumentationMethod = $instrumentationMap.toList.map{ case (pos, r) => $instrumentationT(pos, r) }" - ).mkString("", ";", ";") + val instrumentationMapCode = Seq( + s"{ private val $instrumentationMap = $emptyMapT[$positionT, $renderT]", + s"def $instrumentationMethod = $instrumentationMap.toList.map{ case (pos, r) => $instrumentationT(pos, r) }" + ).mkString("", ";", ";") - val instrumentationMapPatch = - Patch(openCurlyBrace, openCurlyBrace, instrumentationMapCode) + val instrumentationMapPatch = Patch(openCurlyBrace, openCurlyBrace, instrumentationMapCode) - instrumentationMapPatch +: - c.templ.stats + instrumentationMapPatch +: + c.templ.stats .filter { case _: Term.EndMarker => false case _ => true } - .collect { - case term: Term => instrumentOne(term, None, offset, isScalaJs) + .collect { case term: Term => + instrumentOne(term, None, offset, isScalaJs) } - }.flatten + }.flatten val instrumentedCode = Patch(source.tokens, instrumentedCodePatches) @@ -144,9 +140,8 @@ object Instrument { case _ => false } } - val apps = Set("App", "IOApp") - def hasApp(templ: Template): Boolean = - templ.inits.exists(p => apps(p.syntax)) + val apps = Set("App", "IOApp") + def hasApp(templ: Template): Boolean = templ.inits.exists(p => apps(p.syntax)) source.stats.exists { case c: Defn.Object if c.name.value == instrumentedObject => @@ -163,7 +158,7 @@ object Instrument { def apply(code: String, target: ScalaTarget): Either[InstrumentationFailure, String] = { val runtimeImport = target match { case Scala3(scalaVersion) => "import _root_.com.olegych.scastie.api.runtime.*" - case _ => "import _root_.com.olegych.scastie.api.runtime._" + case _ => "import _root_.com.olegych.scastie.api.runtime._" } val isScalaJs = target.targetType == ScalaTargetType.JS @@ -172,7 +167,7 @@ object Instrument { if (!isScalaJs) s"object $instrumentedObject extends ScastieApp {" else s"object $instrumentedObject extends ScastieApp with $domhookT {" val prelude = s"""$runtimeImport\n$classBegin""" - val code0 = s"""$prelude\n$code\n}""" + val code0 = s"""$prelude\n$code\n}""" def typelevel(scalaVersion: String): Option[Dialect] = { if (scalaVersion.startsWith("2.12")) Some(dialects.Typelevel212) @@ -201,16 +196,15 @@ object Instrument { try { code0.parse[Source] match { case parsed: Parsed.Success[_] => - if (!hasMainMethod(parsed.get)) - Right(instrument(parsed.get, prelude.length + 1, isScalaJs)) + if (!hasMainMethod(parsed.get)) Right(instrument(parsed.get, prelude.length + 1, isScalaJs)) else Left(HasMainMethod) case e: Parsed.Error => Left(ParsingError(e)) } } catch { - case NonFatal(e) => - Left(InternalError(e)) + case NonFatal(e) => Left(InternalError(e)) } case None => Left(UnsupportedDialect) } } + } diff --git a/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/InstrumentedInputs.scala b/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/InstrumentedInputs.scala index 51144df79..23c2f09c6 100644 --- a/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/InstrumentedInputs.scala +++ b/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/InstrumentedInputs.scala @@ -2,12 +2,12 @@ package com.olegych.scastie.instrumentation import java.io.{PrintWriter, StringWriter} import java.time.Instant +import scala.meta.parsers.Parsed import com.olegych.scastie.api._ -import scala.meta.parsers.Parsed - case class InstrumentationFailureReport(message: String, line: Option[Int]) { + def toProgress(snippetId: SnippetId): SnippetProgress = { SnippetProgress.default.copy( ts = Some(Instant.now.toEpochMilli), @@ -15,9 +15,11 @@ case class InstrumentationFailureReport(message: String, line: Option[Int]) { compilationInfos = List(Problem(Error, line, message)) ) } + } object InstrumentedInputs { + def apply(inputs0: Inputs): Either[InstrumentationFailureReport, InstrumentedInputs] = { if (inputs0.isWorksheetMode) { val instrumented = Instrument(inputs0.code, inputs0.target).map { instrumentedCode => @@ -25,8 +27,7 @@ object InstrumentedInputs { } instrumented match { - case Right(inputs) => - success(inputs) + case Right(inputs) => success(inputs) case Left(error) => import InstrumentationFailure._ @@ -40,12 +41,14 @@ object InstrumentedInputs { case ParsingError(error) => val lineOffset = Instrument.getParsingLineOffset(inputs0) - val errorLine = (error.pos.startLine + lineOffset) max 1 - Right(InstrumentedInputs( - inputs = inputs0.copy(code = error.pos.input.text), - isForcedProgramMode = false, - optionalParsingError = Some(InstrumentationFailureReport(error.message, Some(errorLine))) - )) + val errorLine = (error.pos.startLine + lineOffset) max 1 + Right( + InstrumentedInputs( + inputs = inputs0.copy(code = error.pos.input.text), + isForcedProgramMode = false, + optionalParsingError = Some(InstrumentationFailureReport(error.message, Some(errorLine))) + ) + ) case InternalError(exception) => val errors = new StringWriter() @@ -64,10 +67,11 @@ object InstrumentedInputs { private def success(inputs: Inputs): Either[InstrumentationFailureReport, InstrumentedInputs] = { Right(InstrumentedInputs(inputs, isForcedProgramMode = false)) } + } case class InstrumentedInputs( - inputs: Inputs, - isForcedProgramMode: Boolean, - optionalParsingError: Option[InstrumentationFailureReport] = None + inputs: Inputs, + isForcedProgramMode: Boolean, + optionalParsingError: Option[InstrumentationFailureReport] = None ) diff --git a/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/Patch.scala b/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/Patch.scala index bc61c344e..192ae1c8e 100644 --- a/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/Patch.scala +++ b/instrumentation/src/main/scala/com.olegych.scastie.instrumentation/Patch.scala @@ -5,12 +5,13 @@ import scala.meta._ import scala.meta.tokens.Token case class Patch(from: Token, to: Token, replace: String) { - def insideRange(token: Token): Boolean = - (token.input eq from.input) && - token.end <= to.end && - token.start >= from.start + + def insideRange(token: Token): Boolean = (token.input eq from.input) && + token.end <= to.end && + token.start >= from.start val tokens: scala.Seq[Token] = replace.tokenize.get.tokens.toSeq + def runOn(str: Seq[Token]): Seq[Token] = { str.flatMap { case `from` => tokens @@ -18,20 +19,24 @@ case class Patch(from: Token, to: Token, replace: String) { case x => Seq(x) } } + } object Patch { + def verifyPatches(patches: Seq[Patch]): Unit = { // TODO(olafur) assert there's no conflicts. } + def apply(input: Seq[Token], patches: Seq[Patch]): String = { verifyPatches(patches) // TODO(olafur) optimize, this is SUPER inefficient patches - .foldLeft(input) { - case (s, p) => p.runOn(s) + .foldLeft(input) { case (s, p) => + p.runOn(s) } .map(_.syntax) .mkString("") } + } diff --git a/instrumentation/src/test/scala/com.olegych.scastie.instrumentation/Diff.scala b/instrumentation/src/test/scala/com.olegych.scastie.instrumentation/Diff.scala index 3409a685d..50aab123f 100644 --- a/instrumentation/src/test/scala/com.olegych.scastie.instrumentation/Diff.scala +++ b/instrumentation/src/test/scala/com.olegych.scastie.instrumentation/Diff.scala @@ -3,9 +3,10 @@ package com.olegych.scastie.instrumentation import com.olegych.scastie.util.ScastieFileUtil case class DiffFailure(title: String, expected: String, obtained: String, diff: String) - extends Exception(title + "\n" + Diff.error2message(obtained, expected)) + extends Exception(title + "\n" + Diff.error2message(obtained, expected)) object Diff { + def error2message(obtained: String, expected: String): String = { ScastieFileUtil.write(new java.io.File("target/obtained.scala").toPath, obtained, truncate = true) val sb = new StringBuilder @@ -13,18 +14,18 @@ object Diff { sb.append("\n") sb.append(s""" - ## Obtained - #${trailingSpace(obtained)} + ## Obtained + #${trailingSpace(obtained)} """.stripMargin('#')) sb.append(s""" - ## Expected - #${trailingSpace(expected)} + ## Expected + #${trailingSpace(expected)} """.stripMargin('#')) sb.append(s""" - ## Diff - #${trailingSpace(compareContents(obtained, expected))} + ## Diff + #${trailingSpace(compareContents(obtained, expected))} """.stripMargin('#')) sb.toString() } @@ -41,7 +42,7 @@ object Diff { def compareContents(obtained: String, expected: String): String = { compareContents( expected = expected.replace("\r\n", "\n").trim.split("\n").toList, - obtained = obtained.replace("\r\n", "\n").trim.split("\n").toList, + obtained = obtained.replace("\r\n", "\n").trim.split("\n").toList ) } @@ -49,16 +50,16 @@ object Diff { import scala.jdk.CollectionConverters._ val diff = difflib.DiffUtils.diff(expected.asJava, obtained.asJava) if (diff.getDeltas.isEmpty) "" - else - difflib.DiffUtils - .generateUnifiedDiff( - "expected", - "obtained", - expected.asJava, - diff, - 1 - ) - .asScala - .mkString("\n") + else difflib.DiffUtils + .generateUnifiedDiff( + "expected", + "obtained", + expected.asJava, + diff, + 1 + ) + .asScala + .mkString("\n") } + } diff --git a/instrumentation/src/test/scala/com.olegych.scastie.instrumentation/InstrumentSpecs.scala b/instrumentation/src/test/scala/com.olegych.scastie.instrumentation/InstrumentSpecs.scala index c2b4ceed2..cb192c5b4 100644 --- a/instrumentation/src/test/scala/com.olegych.scastie.instrumentation/InstrumentSpecs.scala +++ b/instrumentation/src/test/scala/com.olegych.scastie.instrumentation/InstrumentSpecs.scala @@ -1,21 +1,21 @@ package com.olegych.scastie package instrumentation +import java.nio.file._ +import scala.jdk.CollectionConverters._ + import com.olegych.scastie.api.ScalaTarget import com.olegych.scastie.util.ScastieFileUtil.slurp import org.scalatest.funsuite.AnyFunSuite -import java.nio.file._ -import scala.jdk.CollectionConverters._ - class InstrumentSpecs extends AnyFunSuite { import InstrumentationFailure._ private val testFiles = { val path = Paths.get("instrumentation", "src", "test", "resources") - val s = Files.newDirectoryStream(path) - val t = s.asScala.toList.filter(_.endsWith(".scala")) + val s = Files.newDirectoryStream(path) + val t = s.asScala.toList.filter(_.endsWith(".scala")) s.close() t } @@ -50,13 +50,11 @@ class InstrumentSpecs extends AnyFunSuite { } test("extends App trait fails") { - val Left(HasMainMethod) = - Instrument("object Main extends App { }", ScalaTarget.Jvm.default) + val Left(HasMainMethod) = Instrument("object Main extends App { }", ScalaTarget.Jvm.default) } test("with App trait fails") { - val Left(HasMainMethod) = - Instrument("trait Foo; object Main extends Foo with App { }", ScalaTarget.Jvm.default) + val Left(HasMainMethod) = Instrument("trait Foo; object Main extends Foo with App { }", ScalaTarget.Jvm.default) } test("extends App primary fails") { diff --git a/project/CopyRecursively.scala b/project/CopyRecursively.scala index 837d0c473..120301a32 100644 --- a/project/CopyRecursively.scala +++ b/project/CopyRecursively.scala @@ -1,7 +1,8 @@ -import java.nio.file.{Path, Files, SimpleFileVisitor, FileVisitResult} +import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} import java.nio.file.attribute.BasicFileAttributes object CopyRecursively { + def apply(source: Path, destination: Path, directoryFilter: (Path, Int) => Boolean): Unit = { Files.walkFileTree( @@ -9,20 +10,21 @@ object CopyRecursively { new CopyVisitor(source, destination, directoryFilter) ) } + } -class CopyVisitor(source: Path, destination: Path, directoryFilter: (Path, Int) => Boolean) extends SimpleFileVisitor[Path] { +class CopyVisitor(source: Path, destination: Path, directoryFilter: (Path, Int) => Boolean) + extends SimpleFileVisitor[Path] { - private def relative(subPath: Path): Path = - destination.resolve(source.relativize(subPath)) + private def relative(subPath: Path): Path = destination.resolve(source.relativize(subPath)) private def pathDepth(dir: Path): Int = { dir.getNameCount - source.getNameCount - 1 } override def preVisitDirectory( - dir: Path, - attrs: BasicFileAttributes + dir: Path, + attrs: BasicFileAttributes ): FileVisitResult = { def copy(): FileVisitResult = { @@ -43,4 +45,5 @@ class CopyVisitor(source: Path, destination: Path, directoryFilter: (Path, Int) Files.copy(file, relative(file)) FileVisitResult.CONTINUE } + } diff --git a/project/Deployment.scala b/project/Deployment.scala index 23a276229..7f1794f71 100644 --- a/project/Deployment.scala +++ b/project/Deployment.scala @@ -1,113 +1,121 @@ -import sbt._ -import Keys._ - -import SbtShared.gitHashNow - import java.io.File import java.nio.file._ import java.nio.file.attribute._ import java.nio.file.StandardCopyOption.REPLACE_EXISTING +import java.time.LocalDateTime +import com.typesafe import com.typesafe.config.ConfigFactory import com.typesafe.sbt.SbtNativePackager.Universal +import sbt._ import sbtdocker.DockerKeys.{docker, dockerBuildAndPush, imageNames} import sbtdocker.ImageName import sys.process._ -import java.time.LocalDateTime -import com.typesafe +import Keys._ +import SbtShared.gitHashNow object Deployment { + def settings(server: Project, sbtRunner: Project, metalsRunner: Project): Seq[Def.Setting[Task[Unit]]] = Seq( - deploy := deployTask(server, sbtRunner, metalsRunner, Production).value, - deployStaging := deployTask(server, sbtRunner, metalsRunner, Staging).value, - publishContainers := publishContainers(sbtRunner, metalsRunner).value, + deploy := deployTask(server, sbtRunner, metalsRunner, Production).value, + deployStaging := deployTask(server, sbtRunner, metalsRunner, Staging).value, + publishContainers := publishContainers(sbtRunner, metalsRunner).value, generateDeploymentScripts := generateDeploymentScriptsTask(server, sbtRunner, metalsRunner).value, - deployLocal := deployLocalTask(server, sbtRunner, metalsRunner).value + deployLocal := deployLocalTask(server, sbtRunner, metalsRunner).value ) - lazy val deploy = taskKey[Unit]("Deploy server and sbt instances") - lazy val deployStaging = taskKey[Unit]("Deploy server and sbt instances with staging configuration") - lazy val publishContainers = taskKey[Unit]("Publishes sbt runners and metals runner to docker repository") + lazy val deploy = taskKey[Unit]("Deploy server and sbt instances") + lazy val deployStaging = taskKey[Unit]("Deploy server and sbt instances with staging configuration") + lazy val publishContainers = taskKey[Unit]("Publishes sbt runners and metals runner to docker repository") lazy val generateDeploymentScripts = taskKey[Unit]("Generates deployment scripts with production configuration.") - lazy val deployLocal = taskKey[Unit]("Deploy locally") - - def deployTask(server: Project, sbtRunner: Project, metalsRunner: Project, deploymentType: DeploymentType): Def.Initialize[Task[Unit]] = - Def.task { - val deployment = deploymentTask(sbtRunner, metalsRunner, deploymentType).value - val serverZip = (server / Universal / packageBin).value.toPath - - deployment.deploy(serverZip) - } - - def publishContainers(sbtRunner: Project, metalsRunner: Project): Def.Initialize[Task[Unit]] = - Def.task { - (sbtRunner / dockerBuildAndPush).value - (metalsRunner / dockerBuildAndPush).value - } + lazy val deployLocal = taskKey[Unit]("Deploy locally") + + def deployTask( + server: Project, + sbtRunner: Project, + metalsRunner: Project, + deploymentType: DeploymentType + ): Def.Initialize[Task[Unit]] = Def.task { + val deployment = deploymentTask(sbtRunner, metalsRunner, deploymentType).value + val serverZip = (server / Universal / packageBin).value.toPath + + deployment.deploy(serverZip) + } - def generateDeploymentScriptsTask(server: Project, sbtRunner: Project, metalsRunner: Project): Def.Initialize[Task[Unit]] = - Def.task { - val deployment = deploymentTask(sbtRunner, metalsRunner, Production).value - deployment.generateDeploymentScripts() - } + def publishContainers(sbtRunner: Project, metalsRunner: Project): Def.Initialize[Task[Unit]] = Def.task { + (sbtRunner / dockerBuildAndPush).value + (metalsRunner / dockerBuildAndPush).value + } + def generateDeploymentScriptsTask( + server: Project, + sbtRunner: Project, + metalsRunner: Project + ): Def.Initialize[Task[Unit]] = Def.task { + val deployment = deploymentTask(sbtRunner, metalsRunner, Production).value + deployment.generateDeploymentScripts() + } def deployLocalTask(server: Project, sbtRunner: Project, metalsRunner: Project): Def.Initialize[Task[Unit]] = Def.task { val deployment = deploymentTask(sbtRunner, metalsRunner, Local).value - val serverZip = (server / Universal / packageBin).value.toPath + val serverZip = (server / Universal / packageBin).value.toPath (sbtRunner / docker).value (metalsRunner / docker).value deployment.deployLocal(serverZip) } - private def deploymentTask(sbtRunner: Project, metalsRunner: Project, deploymentType: DeploymentType): Def.Initialize[Task[Deployment]] = - Def.task { - new Deployment( - rootFolder = (ThisBuild / baseDirectory).value, - version = version.value, - sbtDockerImage = (sbtRunner / docker / imageNames).value.head, - metalsDockerImage = (metalsRunner / docker / imageNames).value.head, - deploymentType = deploymentType, - logger = streams.value.log - ) - } + private def deploymentTask( + sbtRunner: Project, + metalsRunner: Project, + deploymentType: DeploymentType + ): Def.Initialize[Task[Deployment]] = Def.task { + new Deployment( + rootFolder = (ThisBuild / baseDirectory).value, + version = version.value, + sbtDockerImage = (sbtRunner / docker / imageNames).value.head, + metalsDockerImage = (metalsRunner / docker / imageNames).value.head, + deploymentType = deploymentType, + logger = streams.value.log + ) + } + } sealed trait DeploymentType -case object Local extends DeploymentType -case object Staging extends DeploymentType +case object Local extends DeploymentType +case object Staging extends DeploymentType case object Production extends DeploymentType class ScastieConfig(val configurationFile: File) { - val config = ConfigFactory.parseFile(configurationFile) + val config = ConfigFactory.parseFile(configurationFile) val userName = "scastie" - val serverConfig = config.getConfig("com.olegych.scastie.web") + val serverConfig = config.getConfig("com.olegych.scastie.web") val serverHostname = serverConfig.getString("hostname") val serverAkkaPort = serverConfig.getInt("akka-port") - val metalsConfig = config.getConfig("scastie.metals") - val metalsPort = metalsConfig.getInt("port") + val metalsConfig = config.getConfig("scastie.metals") + val metalsPort = metalsConfig.getInt("port") val cacheExpireInSeconds = metalsConfig.getInt("cache-expire-in-seconds") - val balancerConfig = config.getConfig("com.olegych.scastie.balancer") - val runnersHostname = balancerConfig.getString("remote-hostname") + val balancerConfig = config.getConfig("com.olegych.scastie.balancer") + val runnersHostname = balancerConfig.getString("remote-hostname") val sbtRunnersPortsStart = balancerConfig.getInt("remote-sbt-ports-start") - val containerType = balancerConfig.getString("snippets-storage") + val containerType = balancerConfig.getString("snippets-storage") private val sbtRunnersPortsSize = balancerConfig.getInt("remote-sbt-ports-size") - val sbtRunnersPortsEnd = sbtRunnersPortsStart + sbtRunnersPortsSize - 1 + val sbtRunnersPortsEnd = sbtRunnersPortsStart + sbtRunnersPortsSize - 1 } object ScastieConfig { - def ofType(deploymentType: DeploymentType, deploymentFolder: File): ScastieConfig = - deploymentType match { - case Local => new ScastieConfig(deploymentFolder / "local.conf") - case Staging => new ScastieConfig(deploymentFolder / "staging.conf") - case Production => new ScastieConfig(deploymentFolder / "production.conf") - } + + def ofType(deploymentType: DeploymentType, deploymentFolder: File): ScastieConfig = deploymentType match { + case Local => new ScastieConfig(deploymentFolder / "local.conf") + case Staging => new ScastieConfig(deploymentFolder / "staging.conf") + case Production => new ScastieConfig(deploymentFolder / "production.conf") + } def logbackConfigPath(deploymentFolder: File): Path = (deploymentFolder / "logback.xml").toPath @@ -122,16 +130,16 @@ class Deployment( val logger: Logger ) { val deploymentFolder = rootFolder / "deployment" - val config = ScastieConfig.ofType(deploymentType, deploymentFolder) + val config = ScastieConfig.ofType(deploymentType, deploymentFolder) - val sbtDockerNamespace = sbtDockerImage.namespace.get + val sbtDockerNamespace = sbtDockerImage.namespace.get val sbtDockerRepository = sbtDockerImage.repository - val metalsDockerNamespace = metalsDockerImage.namespace.get + val metalsDockerNamespace = metalsDockerImage.namespace.get val metalsDockerRepository = metalsDockerImage.repository def deploy(serverZip: Path) = { - val time = LocalDateTime.now() + val time = LocalDateTime.now() val outputPath = deploymentFolder.toPath.resolve(s"generated-scripts-$time") if (!Files.exists(outputPath)) { @@ -139,8 +147,7 @@ class Deployment( } // the deployment will be sequential so if anything fails, other services will keep working. - val success = - createAndVerifyDeploymentScriptsData(outputPath) && + val success = createAndVerifyDeploymentScriptsData(outputPath) && deployRunners() && deployMetalsRunner() && deployServer(serverZip) @@ -150,13 +157,13 @@ class Deployment( /* Create runner script which will be used during deployment to start SBT runners docker containers */ def createRunnersStartupScript(scriptOutputDirectory: Path): Path = { - val fileName = if (deploymentType == Staging) "start-runners-staging.sh" else "start-runners.sh" + val fileName = if (deploymentType == Staging) "start-runners-staging.sh" else "start-runners.sh" val scriptPath = scriptOutputDirectory.resolve(fileName) val dockerImagePath = deploymentType match { - case Local => s"$sbtDockerNamespace/$sbtDockerRepository:$gitHashNow" + case Local => s"$sbtDockerNamespace/$sbtDockerRepository:$gitHashNow" case Production => s"$sbtDockerNamespace/$sbtDockerRepository:latest" - case Staging => s"$sbtDockerNamespace/$sbtDockerRepository:latest" + case Staging => s"$sbtDockerNamespace/$sbtDockerRepository:latest" } val containerName = if (deploymentType == Staging) "scastie-sbt-runner-staging" else "scastie-sbt-runner" @@ -180,7 +187,6 @@ class Deployment( | $dockerImagePath |done""".stripMargin - Files.write(scriptPath, runnersStartupScriptContent.getBytes()) setPosixFilePermissions(scriptPath, executablePermissions) @@ -189,29 +195,28 @@ class Deployment( /* Create metals script which will be used during deployment to start metals docker container */ def createMetalsStartupScript(scriptOutputDirectory: Path): Path = { - val fileName = if (deploymentType == Staging) "start-metals-staging.sh" else "start-metals.sh" + val fileName = if (deploymentType == Staging) "start-metals-staging.sh" else "start-metals.sh" val scriptPath = scriptOutputDirectory.resolve(fileName) val dockerImagePath = deploymentType match { - case Local => s"$metalsDockerNamespace/$metalsDockerRepository:$gitHashNow" + case Local => s"$metalsDockerNamespace/$metalsDockerRepository:$gitHashNow" case Production => s"$metalsDockerNamespace/$metalsDockerRepository:latest" - case Staging => s"$metalsDockerNamespace/$metalsDockerRepository:latest" + case Staging => s"$metalsDockerNamespace/$metalsDockerRepository:latest" } val containerName = if (deploymentType == Staging) "scastie-metals-runner-staging" else "scastie-metals-runner" - val metalsRunnerStartupScriptContent: String = - s"""#!/usr/bin/env bash - |echo "Starting Metals: Port ${config.metalsPort}" - |docker run \\ - | --restart=always \\ - | --name=$containerName \\ - | -p ${config.metalsPort}:${config.metalsPort} \\ - | -d \\ - | -e PORT=${config.metalsPort} \\ - | -e CACHE_EXPIRE_IN_SECONDS=${config.cacheExpireInSeconds} \\ - | -e IS_DOCKER=true \\ - | $dockerImagePath""".stripMargin + val metalsRunnerStartupScriptContent: String = s"""#!/usr/bin/env bash + |echo "Starting Metals: Port ${config.metalsPort}" + |docker run \\ + | --restart=always \\ + | --name=$containerName \\ + | -p ${config.metalsPort}:${config.metalsPort} \\ + | -d \\ + | -e PORT=${config.metalsPort} \\ + | -e CACHE_EXPIRE_IN_SECONDS=${config.cacheExpireInSeconds} \\ + | -e IS_DOCKER=true \\ + | $dockerImagePath""".stripMargin Files.write(scriptPath, metalsRunnerStartupScriptContent.getBytes()) setPosixFilePermissions(scriptPath, executablePermissions) @@ -221,15 +226,15 @@ class Deployment( /* Compares script with its remote version */ def compareScriptWithRemote(scriptPath: Path): Boolean = { - val uri = s"${config.userName}@${config.runnersHostname}" + val uri = s"${config.userName}@${config.runnersHostname}" val remoteScriptPath = scriptPath.getFileName().toString() - val exitCode = Process(s"ssh $uri cat $remoteScriptPath") #| (s"diff - $scriptPath") ! logger + val exitCode = Process(s"ssh $uri cat $remoteScriptPath") #| (s"diff - $scriptPath") ! logger logger.info(s"EXIT CODE $exitCode") exitCode == 0 } def generateDeploymentScripts() = { - val time = LocalDateTime.now() + val time = LocalDateTime.now() val outputPath = deploymentFolder.toPath.resolve(s"generated-scripts-$time") if (!Files.exists(outputPath)) { @@ -240,7 +245,6 @@ class Deployment( createMetalsStartupScript(outputPath) } - /* * Verifies if deployment scripts are up to date on remote * We don't want to automatically sync files between servers, as if it is misconfigured @@ -250,33 +254,37 @@ class Deployment( * By manually copying the files, we are ensuring that everything is properly configured. */ def createAndVerifyDeploymentScriptsData(scriptOutputDirectory: Path): Boolean = { - val deploymentScript : Path = (deploymentFolder / "deploy.sh").toPath + val deploymentScript: Path = (deploymentFolder / "deploy.sh").toPath val runnerContainersStartupScript: Path = createRunnersStartupScript(scriptOutputDirectory) - val metalsContainerStartupScript: Path = createMetalsStartupScript(scriptOutputDirectory) - - List(deploymentScript, runnerContainersStartupScript, metalsContainerStartupScript).map { script => - val isUpToDate: Boolean = compareScriptWithRemote(script) - if (!isUpToDate) { - val remoteScriptPath = script.getFileName().toString() - logger.error(s"Deployment stopped. Script: $script is not up to date with remote version $remoteScriptPath or could not be validated. You have to update it manually. It should be located in the user home directory.") + val metalsContainerStartupScript: Path = createMetalsStartupScript(scriptOutputDirectory) + + List(deploymentScript, runnerContainersStartupScript, metalsContainerStartupScript) + .map { script => + val isUpToDate: Boolean = compareScriptWithRemote(script) + if (!isUpToDate) { + val remoteScriptPath = script.getFileName().toString() + logger.error( + s"Deployment stopped. Script: $script is not up to date with remote version $remoteScriptPath or could not be validated. You have to update it manually. It should be located in the user home directory." + ) + } + isUpToDate } - isUpToDate - }.forall(_ == true) + .forall(_ == true) } def deployRunners(): Boolean = { - val uri = s"${config.userName}@${config.runnersHostname}" + val uri = s"${config.userName}@${config.runnersHostname}" val exitCode = Process(s"ssh $uri ./deploy.sh SBT $deploymentType") ! logger exitCode == 0 } def deployMetalsRunner(): Boolean = { - val uri = s"${config.userName}@${config.runnersHostname}" + val uri = s"${config.userName}@${config.runnersHostname}" val exitCode = Process(s"ssh $uri ./deploy.sh Metals $deploymentType") ! logger exitCode == 0 } - //#################################################################################################################// + // #################################################################################################################// def deployLocal(serverZip: Path): Unit = { val destination = rootFolder.toPath.resolve("local") @@ -296,10 +304,9 @@ class Deployment( val deploymentFiles = deployServerFiles(serverZip, destination, local = true).files - deploymentFiles.foreach( - file => - Files - .copy(file, destination.resolve(file.getFileName), REPLACE_EXISTING) + deploymentFiles.foreach(file => + Files + .copy(file, destination.resolve(file.getFileName), REPLACE_EXISTING) ) logger.success("Local deployment script are in ./local directory.") @@ -308,30 +315,31 @@ class Deployment( def deployServer(serverZip: Path): Boolean = { val serverScriptDir = Files.createTempDirectory("server") - val deploymentFiles = - deployServerFiles(serverZip, serverScriptDir, local = false) + val deploymentFiles = deployServerFiles(serverZip, serverScriptDir, local = false) deploymentFiles.files.foreach(rsyncServer) val scriptFileName = deploymentFiles.serverScript.getFileName - val uri = config.userName + "@" + config.serverHostname - val exitCode = Process(s"ssh $uri ./$scriptFileName") ! logger + val uri = config.userName + "@" + config.serverHostname + val exitCode = Process(s"ssh $uri ./$scriptFileName") ! logger exitCode == 0 } case class DeploymentFiles( - serverZip: Path, - serverScript: Path, - productionConfig: Path, - logbackConfig: Path, - secretConfig: Option[Path] = None + serverZip: Path, + serverScript: Path, + productionConfig: Path, + logbackConfig: Path, + secretConfig: Option[Path] = None ) { + def files: List[Path] = List( serverZip, serverScript, productionConfig, logbackConfig ) ++ secretConfig.toList + } private def deployServerFiles(serverZip: Path, destination: Path, local: Boolean): DeploymentFiles = { @@ -339,10 +347,10 @@ class Deployment( val serverScript = destination.resolve("server.sh") - val configFileName = config.configurationFile.toPath().getFileName - val logbackConfig = ScastieConfig.logbackConfigPath(deploymentFolder) - val logbackConfigFileName= logbackConfig.getFileName() - val serverZipFileName = serverZip.getFileName.toString.replace(".zip", "") + val configFileName = config.configurationFile.toPath().getFileName + val logbackConfig = ScastieConfig.logbackConfigPath(deploymentFolder) + val logbackConfigFileName = logbackConfig.getFileName() + val serverZipFileName = serverZip.getFileName.toString.replace(".zip", "") val regex = "^[a-zA-Z0-9]+$".r if (regex.findFirstIn(config.userName).isEmpty) @@ -352,34 +360,33 @@ class Deployment( if (!local) s"/home/${config.userName}/" else "" - val isMongoDB = config.containerType == "mongo" + val isMongoDB = config.containerType == "mongo" val mongodbConfig = if (deploymentType == Production) "mongodb-prod.conf" else "mongodb-staging.conf" - val content = - s"""|#!/usr/bin/env bash - | - |whoami - | - |if [ -e ${baseDir}RUNNING_PID ]; then - | kill -9 `cat ${baseDir}RUNNING_PID` - |fi - | - |if [ ! -f ${baseDir}${mongodbConfig} ] && ${isMongoDB}; then - | echo "mongodb configuration file: ${baseDir}${mongodbConfig} is missing" - | exit 1 - |fi - | - |rm -rf ${baseDir}server/* - |unzip -o -d ${baseDir}server ${baseDir}$serverZipFileName - |mv ${baseDir}server/$serverZipFileName/* ${baseDir}server/ - |rm -rf ${baseDir}server/$serverZipFileName - | - |nohup ${baseDir}server/bin/server \\ - | -J-Xmx1G \\ - | -Dconfig.file=${baseDir}${configFileName} \\ - | -Dlogback.configurationFile=${baseDir}${logbackConfigFileName} \\ - | &>/dev/null & - |""".stripMargin + val content = s"""|#!/usr/bin/env bash + | + |whoami + | + |if [ -e ${baseDir}RUNNING_PID ]; then + | kill -9 `cat ${baseDir}RUNNING_PID` + |fi + | + |if [ ! -f ${baseDir}${mongodbConfig} ] && ${isMongoDB}; then + | echo "mongodb configuration file: ${baseDir}${mongodbConfig} is missing" + | exit 1 + |fi + | + |rm -rf ${baseDir}server/* + |unzip -o -d ${baseDir}server ${baseDir}$serverZipFileName + |mv ${baseDir}server/$serverZipFileName/* ${baseDir}server/ + |rm -rf ${baseDir}server/$serverZipFileName + | + |nohup ${baseDir}server/bin/server \\ + | -J-Xmx1G \\ + | -Dconfig.file=${baseDir}${configFileName} \\ + | -Dlogback.configurationFile=${baseDir}${logbackConfigFileName} \\ + | &>/dev/null & + |""".stripMargin Files.write(serverScript, content.getBytes) setPosixFilePermissions(serverScript, executablePermissions) @@ -391,25 +398,25 @@ class Deployment( serverScript, config.configurationFile.toPath(), logbackConfig, - None, + None ) } - private def rsync(file: Path, userName: String, hostname: String, logger: Logger): Unit = { - val uri = userName + "@" + hostname + val uri = userName + "@" + hostname val fileName = file.getFileName Process(s"rsync $file $uri:$fileName") ! logger } - private def rsyncServer(file: Path) = - rsync(file, config.userName, config.serverHostname, logger) + private def rsyncServer(file: Path) = rsync(file, config.userName, config.serverHostname, logger) private val executablePermissions = PosixFilePermissions.fromString("rwxr-xr-x") + private def setPosixFilePermissions(path: Path, perms: java.util.Set[PosixFilePermission]): Path = { try Files.setPosixFilePermissions(path, perms) catch { case e: Exception => path } } + } diff --git a/project/DockerHelper.scala b/project/DockerHelper.scala index 423b7549d..7ca22a2c1 100644 --- a/project/DockerHelper.scala +++ b/project/DockerHelper.scala @@ -1,7 +1,7 @@ -import sbtdocker.DockerPlugin.autoImport._ - import java.nio.file.Path +import sbtdocker.DockerPlugin.autoImport._ + object DockerHelper { val alpineImageName = "alpine:3.17" @@ -36,10 +36,10 @@ object DockerHelper { env("LANG", "en_US.UTF-8") env("HOME", userHome) - val artifactName = artifactZip.getFileName.toString.replace(".zip", "") + val artifactName = artifactZip.getFileName.toString.replace(".zip", "") val artifactZipFileName = artifactZip.getFileName.toString - val artifactTargetPath = s"/app/$artifactZipFileName" - val configDestination = "/home/scastie/config.conf" + val artifactTargetPath = s"/app/$artifactZipFileName" + val configDestination = "/home/scastie/config.conf" add(configPath.toFile, configDestination) add(artifactZip.toFile, artifactTargetPath) @@ -55,17 +55,18 @@ object DockerHelper { } } - - def apply(baseDirectory: Path, - sbtTargetDir: Path, - sbtScastie: String, - ivyHome: Path, - organization: String, - artifact: Path, - sbtVersion: String): Dockerfile = { + def apply( + baseDirectory: Path, + sbtTargetDir: Path, + sbtScastie: String, + ivyHome: Path, + organization: String, + artifact: Path, + sbtVersion: String + ): Dockerfile = { val artifactTargetPath = s"/app/${artifact.getFileName()}" - val generatedProjects = new GenerateProjects(sbtTargetDir) + val generatedProjects = new GenerateProjects(sbtTargetDir) generatedProjects.generateSbtProjects() val logbackConfDestination = "/home/scastie/logback.xml" @@ -86,7 +87,7 @@ object DockerHelper { destination = ivyLocalTemp, directoryFilter = { (dir, depth) => lazy val isSbtScastiePath = dir.getName(0).toString == sbtScastie - lazy val dirName = dir.getFileName.toString + lazy val dirName = dir.getFileName.toString if (depth == 1) { dirName == SbtShared.versionNow || dirName == SbtShared.versionRuntime || isSbtScastiePath @@ -170,4 +171,5 @@ object DockerHelper { ) } } + } diff --git a/project/GenerateProjects.scala b/project/GenerateProjects.scala index 751b75765..4e85b1c7b 100644 --- a/project/GenerateProjects.scala +++ b/project/GenerateProjects.scala @@ -17,7 +17,7 @@ class GenerateProjects(sbtTargetDir: Path) { ) def scala(version: String): Inputs = defaultWithMain.copy( - target = ScalaTarget.Jvm(version), + target = ScalaTarget.Jvm(version) ) val scala212 = scala(BuildInfo.latest212) @@ -29,7 +29,7 @@ class GenerateProjects(sbtTargetDir: Path) { val scalaJs = Inputs.default.copy( code = """@_root_.scala.scalajs.js.annotation.JSExportTopLevel("ScastiePlaygroundMain") class Test""".stripMargin, - target = ScalaTarget.Js.default, + target = ScalaTarget.Js.default ) List( @@ -37,24 +37,22 @@ class GenerateProjects(sbtTargetDir: Path) { (scala213, "scala213"), (dotty, "dotty"), (scalaJs, "scalaJs") - ).map { - case (inputs, name) => - new GeneratedProject( - inputs, - projectTarget.resolve(name) - ) + ).map { case (inputs, name) => + new GeneratedProject( + inputs, + projectTarget.resolve(name) + ) } } - def generateSbtProjects(): Unit = - projects.foreach(_.generateSbtProject) + def generateSbtProjects(): Unit = projects.foreach(_.generateSbtProject) } class GeneratedProject(inputs: Inputs, sbtDir: Path) { - private val buildFile = sbtDir.resolve("build.sbt") + private val buildFile = sbtDir.resolve("build.sbt") private val projectDir = sbtDir.resolve("project") private val pluginFile = projectDir.resolve("plugins.sbt") - private val codeFile = sbtDir.resolve("src/main/scala/main.scala") + private val codeFile = sbtDir.resolve("src/main/scala/main.scala") def generateSbtProject(): Unit = { Files.createDirectories(projectDir) @@ -76,4 +74,5 @@ class GeneratedProject(inputs: Inputs, sbtDir: Path) { s"""cd $dest/$dir && sbt "${inputs.target.sbtRunCommand(true)}"""" } + } diff --git a/runtime-scala/src/main/scala/com.olegych.scastie.api.runtime/SharedRuntime.scala b/runtime-scala/src/main/scala/com.olegych.scastie.api.runtime/SharedRuntime.scala index c4f5b2ad9..4ddd53d37 100644 --- a/runtime-scala/src/main/scala/com.olegych.scastie.api.runtime/SharedRuntime.scala +++ b/runtime-scala/src/main/scala/com.olegych.scastie.api.runtime/SharedRuntime.scala @@ -4,6 +4,7 @@ package runtime import play.api.libs.json.Json protected[runtime] trait SharedRuntime { + def write(instrumentations: List[Instrumentation]): String = { if (instrumentations.isEmpty) "" else Json.stringify(Json.toJson(instrumentations)) } @@ -26,4 +27,5 @@ protected[runtime] trait SharedRuntime { Value(out, typeName.replace(Instrumentation.instrumentedObject + ".", "")) } } + } diff --git a/runtime-scala/src/main/scala/com.olegych.scastie.api.runtime/package.scala b/runtime-scala/src/main/scala/com.olegych.scastie.api.runtime/package.scala index 18520cf15..6d46791cf 100644 --- a/runtime-scala/src/main/scala/com.olegych.scastie.api.runtime/package.scala +++ b/runtime-scala/src/main/scala/com.olegych.scastie.api.runtime/package.scala @@ -7,11 +7,11 @@ package object runtime { val Html: api.Html.type = api.Html - def image(path: String): Html = Runtime.image(path) + def image(path: String): Html = Runtime.image(path) def toBase64(in: BufferedImage): Html = Runtime.toBase64(in) implicit class HtmlHelper(val sc: StringContext) extends AnyVal { - def html(args: Any*) = Html(sc.s(args: _*)) + def html(args: Any*) = Html(sc.s(args: _*)) def htmlRaw(args: Any*) = Html(sc.raw(args: _*)) } diff --git a/runtime-scala/src/main/scalajs/com.olegych.scastie.api.runtime/DomHook.scala b/runtime-scala/src/main/scalajs/com.olegych.scastie.api.runtime/DomHook.scala index 611db58bf..b9294cfb1 100644 --- a/runtime-scala/src/main/scalajs/com.olegych.scastie.api.runtime/DomHook.scala +++ b/runtime-scala/src/main/scalajs/com.olegych.scastie.api.runtime/DomHook.scala @@ -1,12 +1,11 @@ package com.olegych.scastie.api package runtime -import org.scalajs.dom.HTMLElement -import scala.scalajs.js - +import java.util.UUID import scala.collection.mutable.Buffer +import scala.scalajs.js -import java.util.UUID +import org.scalajs.dom.HTMLElement trait DomHook { private val elements = Buffer.empty[HTMLElement] diff --git a/runtime-scala/src/main/scalajs/com.olegych.scastie.api.runtime/Runtime.scala b/runtime-scala/src/main/scalajs/com.olegych.scastie.api.runtime/Runtime.scala index b73cb1112..74d3228a0 100644 --- a/runtime-scala/src/main/scalajs/com.olegych.scastie.api.runtime/Runtime.scala +++ b/runtime-scala/src/main/scalajs/com.olegych.scastie.api.runtime/Runtime.scala @@ -1,21 +1,22 @@ package com.olegych.scastie.api package runtime -import play.api.libs.json.Json - -import org.scalajs.dom.HTMLElement - -import java.util.UUID - import java.awt.image.BufferedImage - +import java.util.UUID import scala.reflect.ClassTag +import org.scalajs.dom.HTMLElement +import play.api.libs.json.Json + object Runtime extends SharedRuntime { + def write(in: Either[Option[RuntimeError], List[Instrumentation]]): String = { Json.stringify(Json.toJson(ScalaJsResult(in))) } - def render[T](a: T, attach: HTMLElement => UUID)(implicit _ct: ClassTag[T] = null): Render = { + + def render[T](a: T, attach: HTMLElement => UUID)( + implicit _ct: ClassTag[T] = null + ): Render = { val ct = Option(_ct) a match { case element: HTMLElement => { @@ -26,11 +27,10 @@ object Runtime extends SharedRuntime { } } - def image(path: String): Html = - throw new Exception("image(path: String): Html works only on the jvm") + def image(path: String): Html = throw new Exception("image(path: String): Html works only on the jvm") + + def toBase64(in: BufferedImage): Html = throw new Exception( + "toBase64(in: BufferedImage): Html works only on the jvm" + ) - def toBase64(in: BufferedImage): Html = - throw new Exception( - "toBase64(in: BufferedImage): Html works only on the jvm" - ) } diff --git a/runtime-scala/src/main/scalajvm-2/com.olegych.scastie.api.runtime/Runtime.scala b/runtime-scala/src/main/scalajvm-2/com.olegych.scastie.api.runtime/Runtime.scala index 4476d1bc9..f70c67846 100644 --- a/runtime-scala/src/main/scalajvm-2/com.olegych.scastie.api.runtime/Runtime.scala +++ b/runtime-scala/src/main/scalajvm-2/com.olegych.scastie.api.runtime/Runtime.scala @@ -1,13 +1,18 @@ package com.olegych.scastie.api package runtime -import scala.reflect.ClassTag import scala.reflect.runtime.universe._ +import scala.reflect.ClassTag object Runtime extends JvmRuntime { - def render[T](a: T)(implicit _ct: ClassTag[T] = null, _tt: TypeTag[T] = null): Render = { + + def render[T](a: T)( + implicit _ct: ClassTag[T] = null, + _tt: TypeTag[T] = null + ): Render = { val ct = Option(_ct) val tt = Option(_tt) super.render(a, tt.map(_.tpe.toString).orElse(ct.map(_.toString)).getOrElse("")) } + } diff --git a/runtime-scala/src/main/scalajvm-3/com.olegych.scastie.api.runtime/Runtime.scala b/runtime-scala/src/main/scalajvm-3/com.olegych.scastie.api.runtime/Runtime.scala index 7431e4de0..6f0ec6e8b 100644 --- a/runtime-scala/src/main/scalajvm-3/com.olegych.scastie.api.runtime/Runtime.scala +++ b/runtime-scala/src/main/scalajvm-3/com.olegych.scastie.api.runtime/Runtime.scala @@ -4,9 +4,11 @@ package runtime import scala.quoted.* object Runtime extends JvmRuntime: - inline def render[T](a: T): Render = ${_render('a)} + inline def render[T](a: T): Render = ${ _render('a) } - private def _render[T: Type](a: Expr[T])(using Quotes): Expr[Render] = + private def _render[T: Type](a: Expr[T])( + using Quotes + ): Expr[Render] = import quotes.reflect.* val t = TypeRepr.of[T] - '{Runtime.render($a, ${Expr(t.show)})} + '{ Runtime.render($a, ${ Expr(t.show) }) } diff --git a/runtime-scala/src/main/scalajvm/com.olegych.scastie.api.runtime/Runtime.scala b/runtime-scala/src/main/scalajvm/com.olegych.scastie.api.runtime/Runtime.scala index 540375e63..98358389f 100644 --- a/runtime-scala/src/main/scalajvm/com.olegych.scastie.api.runtime/Runtime.scala +++ b/runtime-scala/src/main/scalajvm/com.olegych.scastie.api.runtime/Runtime.scala @@ -1,12 +1,14 @@ package com.olegych.scastie.api package runtime -import javax.imageio.ImageIO +import java.awt.image.BufferedImage import java.io.{ByteArrayOutputStream, File} import java.util.Base64 -import java.awt.image.BufferedImage + +import javax.imageio.ImageIO protected[runtime] trait JvmRuntime extends SharedRuntime { + def image(path: String): Html = { val in = ImageIO.read(new File(path)) toBase64(in) @@ -14,8 +16,8 @@ protected[runtime] trait JvmRuntime extends SharedRuntime { def toBase64(in: BufferedImage): Html = { val width = in.getWidth - val os = new ByteArrayOutputStream - val b64 = Base64.getEncoder.wrap(os) + val os = new ByteArrayOutputStream + val b64 = Base64.getEncoder.wrap(os) ImageIO.write(in, "png", b64) val encoded = os.toString("UTF-8") @@ -28,4 +30,5 @@ protected[runtime] trait JvmRuntime extends SharedRuntime { folded = true ) } + } diff --git a/sbt-runner/src/main/scala/com.olegych.scastie.sbt/FormatActor.scala b/sbt-runner/src/main/scala/com.olegych.scastie.sbt/FormatActor.scala index 82bef134e..1b482310d 100644 --- a/sbt-runner/src/main/scala/com.olegych.scastie.sbt/FormatActor.scala +++ b/sbt-runner/src/main/scala/com.olegych.scastie.sbt/FormatActor.scala @@ -5,14 +5,15 @@ import akka.actor.Actor import com.olegych.scastie.api.FormatRequest import com.olegych.scastie.api.FormatResponse import com.olegych.scastie.api.ScalaTarget -import org.scalafmt.Formatted -import org.scalafmt.Scalafmt +import org.scalafmt.config.NamedDialect import org.scalafmt.config.ScalafmtConfig import org.scalafmt.config.ScalafmtRunner -import org.scalafmt.config.NamedDialect +import org.scalafmt.Formatted +import org.scalafmt.Scalafmt import org.slf4j.LoggerFactory object FormatActor { + private[sbt] def format(code: String, isWorksheetMode: Boolean, scalaTarget: ScalaTarget): Either[String, String] = { val config: ScalafmtConfig = { val dialect = @@ -22,10 +23,8 @@ object FormatActor { else NamedDialect.scala213 val runner = { - if (isWorksheetMode && scalaTarget.hasWorksheetMode) - ScalafmtRunner.sbt - else - ScalafmtRunner.default + if (isWorksheetMode && scalaTarget.hasWorksheetMode) ScalafmtRunner.sbt + else ScalafmtRunner.default }.withDialect(dialect) ScalafmtConfig.default.copy(runner = runner) @@ -36,17 +35,18 @@ object FormatActor { case Formatted.Failure(failure) => Left(failure.toString) } } + } class FormatActor() extends Actor { import FormatActor._ private val log = LoggerFactory.getLogger(getClass) - override def receive: Receive = { - case FormatRequest(code, isWorksheetMode, scalaTarget) => - log.info(s"format (isWorksheetMode: $isWorksheetMode)") - log.info(code) + override def receive: Receive = { case FormatRequest(code, isWorksheetMode, scalaTarget) => + log.info(s"format (isWorksheetMode: $isWorksheetMode)") + log.info(code) - sender() ! FormatResponse(format(code, isWorksheetMode, scalaTarget)) + sender() ! FormatResponse(format(code, isWorksheetMode, scalaTarget)) } + } diff --git a/sbt-runner/src/main/scala/com.olegych.scastie.sbt/OutputExtractor.scala b/sbt-runner/src/main/scala/com.olegych.scastie.sbt/OutputExtractor.scala index 8709802be..f0ffdce07 100644 --- a/sbt-runner/src/main/scala/com.olegych.scastie.sbt/OutputExtractor.scala +++ b/sbt-runner/src/main/scala/com.olegych.scastie.sbt/OutputExtractor.scala @@ -1,6 +1,7 @@ package com.olegych.scastie.sbt import java.time.Instant +import scala.util.control.NonFatal import com.olegych.scastie.api._ import com.olegych.scastie.instrumentation.Instrument @@ -8,25 +9,26 @@ import com.olegych.scastie.sbt.SbtProcess._ import org.slf4j.LoggerFactory import play.api.libs.json._ -import scala.util.control.NonFatal +class OutputExtractor( + getScalaJsContent: () => Option[String], + getScalaJsSourceMapContent: () => Option[String], + isProduction: Boolean, + promptUniqueId: String +) { -class OutputExtractor(getScalaJsContent: () => Option[String], - getScalaJsSourceMapContent: () => Option[String], - isProduction: Boolean, - promptUniqueId: String) { def extractProgress(output: ProcessOutput, sbtRun: SbtRun, isReloading: Boolean): SnippetProgress = { import sbtRun._ - val problems = extractProblems(output.line, Instrument.getMessageLineOffset(inputs), inputs.isWorksheetMode) + val problems = extractProblems(output.line, Instrument.getMessageLineOffset(inputs), inputs.isWorksheetMode) val instrumentations = extract[List[Instrumentation]](output.line) - val runtimeError = extractRuntimeError(output.line, Instrument.getExceptionLineOffset(inputs)) - val sbtOutput = extract[ConsoleOutput.SbtOutput](output.line) + val runtimeError = extractRuntimeError(output.line, Instrument.getExceptionLineOffset(inputs)) + val sbtOutput = extract[ConsoleOutput.SbtOutput](output.line) // sbt plugin is not loaded at this stage. we need to drop those messages val hiddenInitializationMessages = List( "WARNING: A terminally deprecated method in java.lang.System has been called", "WARNING: System::setSecurityManager has been called", "WARNING: Please consider reporting this to the maintainers", - "WARNING: System::setSecurityManager will be removed in a future release", + "WARNING: System::setSecurityManager will be removed in a future release" ) val isHiddenSbtMessage = @@ -37,14 +39,15 @@ class OutputExtractor(getScalaJsContent: () => Option[String], val isScalaJs = inputs.target.targetType == ScalaTargetType.JS val userOutput = - if (problems.toList.flatten.isEmpty - && instrumentations.toList.flatten.isEmpty - && runtimeError.isEmpty - && !isDone - && !isHiddenSbtMessage - && !isReloading - && sbtOutput.isEmpty) - Some(output) + if ( + problems.toList.flatten.isEmpty + && instrumentations.toList.flatten.isEmpty + && runtimeError.isEmpty + && !isDone + && !isHiddenSbtMessage + && !isReloading + && sbtOutput.isEmpty + ) Some(output) else None val (scalaJsContent, scalaJsSourceMapContent) = @@ -82,38 +85,35 @@ class OutputExtractor(getScalaJsContent: () => Option[String], ) } - private implicit val formatSourceMap: OFormat[SourceMap] = - Json.format[SourceMap] + private implicit val formatSourceMap: OFormat[SourceMap] = Json.format[SourceMap] private case class SourceMap( - version: Int, - file: String, - mappings: String, - sources: List[String], - names: List[String], - lineCount: Int + version: Int, + file: String, + mappings: String, + sources: List[String], + names: List[String], + lineCount: Int ) private def remapSourceMap( - snippetId: SnippetId + snippetId: SnippetId )(sourceMapRaw: String): String = { Json .fromJson[SourceMap](Json.parse(sourceMapRaw)) .asOpt .map { sourceMap => - val sourceMap0 = - sourceMap.copy( - sources = sourceMap.sources.map( - source => - if (source.startsWith(ScalaTarget.Js.sourceUUID)) { - val host = - if (isProduction) "https://scastie.scala-lang.org" - else "http://localhost:9000" - - host + snippetId.scalaJsUrl(ScalaTarget.Js.sourceFilename) - } else source - ) + val sourceMap0 = sourceMap.copy( + sources = sourceMap.sources.map(source => + if (source.startsWith(ScalaTarget.Js.sourceUUID)) { + val host = + if (isProduction) "https://scastie.scala-lang.org" + else "http://localhost:9000" + + host + snippetId.scalaJsUrl(ScalaTarget.Js.sourceFilename) + } else source ) + ) Json.prettyPrint(Json.toJson(sourceMap0)) } @@ -121,9 +121,9 @@ class OutputExtractor(getScalaJsContent: () => Option[String], } private def extractProblems( - line: String, - lineOffset: Int, - isWorksheetMode: Boolean + line: String, + lineOffset: Int, + isWorksheetMode: Boolean ): Option[List[Problem]] = { val problems = extract[List[Problem]](line) @@ -143,7 +143,8 @@ class OutputExtractor(getScalaJsContent: () => Option[String], private def extractRuntimeError(line: String, lineOffset: Int): Option[RuntimeError] = { extract[RuntimeErrorWrap](line).flatMap { _.error.map { error => - val noStackTraceError = if (error.message.contains("No main class detected.")) error.copy(fullStack = "") else error + val noStackTraceError = + if (error.message.contains("No main class detected.")) error.copy(fullStack = "") else error val errorWithOffset = noStackTraceError.copy( line = noStackTraceError.line.map(lineNumber => (lineNumber + lineOffset) max 1) ) diff --git a/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtActor.scala b/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtActor.scala index 64e875b06..93a8fc6e5 100644 --- a/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtActor.scala +++ b/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtActor.scala @@ -1,22 +1,23 @@ package com.olegych.scastie.sbt +import scala.concurrent.duration._ + +import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, ActorSelection, ActorSystem, Props} import com.olegych.scastie.api._ import com.olegych.scastie.util._ -import akka.actor.{Actor, ActorContext, ActorLogging, ActorRef, ActorSelection, ActorSystem, Props} - -import scala.concurrent.duration._ case object SbtActorReady -class SbtActor(system: ActorSystem, - runTimeout: FiniteDuration, - sbtReloadTimeout: FiniteDuration, - isProduction: Boolean, - readyRef: Option[ActorRef], - override val reconnectInfo: Option[ReconnectInfo]) - extends Actor - with ActorLogging - with ActorReconnecting { +class SbtActor( + system: ActorSystem, + runTimeout: FiniteDuration, + sbtReloadTimeout: FiniteDuration, + isProduction: Boolean, + readyRef: Option[ActorRef], + override val reconnectInfo: Option[ReconnectInfo] +) extends Actor + with ActorLogging + with ActorReconnecting { def balancer(context: ActorContext, info: ReconnectInfo): ActorSelection = { import info._ @@ -47,21 +48,19 @@ class SbtActor(system: ActorSystem, super.postStop() } - private val formatActor = - context.actorOf(Props(new FormatActor()), name = "FormatActor") - - private val sbtRunner = - context.actorOf( - Props( - new SbtProcess( - runTimeout, - sbtReloadTimeout, - isProduction, - javaOptions = Seq("-Xms512m", "-Xmx1g") - ) - ), - name = "SbtRunner" - ) + private val formatActor = context.actorOf(Props(new FormatActor()), name = "FormatActor") + + private val sbtRunner = context.actorOf( + Props( + new SbtProcess( + runTimeout, + sbtReloadTimeout, + isProduction, + javaOptions = Seq("-Xms512m", "-Xmx1g") + ) + ), + name = "SbtRunner" + ) override def receive: Receive = reconnectBehavior orElse [Any, Unit] { case SbtPing => { @@ -92,4 +91,5 @@ class SbtActor(system: ActorSystem, } } } + } diff --git a/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtMain.scala b/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtMain.scala index 871972eb2..090ef0b80 100644 --- a/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtMain.scala +++ b/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtMain.scala @@ -1,18 +1,17 @@ package com.olegych.scastie.sbt -import com.olegych.scastie.util.ScastieFileUtil.writeRunningPid -import com.olegych.scastie.util.ReconnectInfo +import java.util.concurrent.TimeUnit +import scala.concurrent.duration._ +import scala.concurrent.Await import akka.actor.{ActorSystem, Props} +import com.olegych.scastie.util.ReconnectInfo +import com.olegych.scastie.util.ScastieFileUtil.writeRunningPid import com.typesafe.config.ConfigFactory - -import scala.concurrent.Await -import scala.concurrent.duration._ -import java.util.concurrent.TimeUnit - import org.slf4j.LoggerFactory object SbtMain { + def main(args: Array[String]): Unit = { val logger = LoggerFactory.getLogger(getClass) @@ -26,7 +25,7 @@ object SbtMain { val config = ConfigFactory.load().getConfig("com.olegych.scastie") val serverConfig = config.getConfig("web") - val sbtConfig = config.getConfig("sbt") + val sbtConfig = config.getConfig("sbt") val isProduction = sbtConfig.getBoolean("production") @@ -53,13 +52,12 @@ object SbtMain { ) } - val reconnectInfo = - ReconnectInfo( - serverHostname = serverConfig.getString("hostname"), - serverAkkaPort = serverConfig.getInt("akka-port"), - actorHostname = sbtConfig.getString("hostname"), - actorAkkaPort = sbtConfig.getInt("akka-port") - ) + val reconnectInfo = ReconnectInfo( + serverHostname = serverConfig.getString("hostname"), + serverAkkaPort = serverConfig.getInt("akka-port"), + actorHostname = sbtConfig.getString("hostname"), + actorAkkaPort = sbtConfig.getInt("akka-port") + ) logger.info(" runTimeout: {}", runTimeout) logger.info(" sbtReloadTimeout: {}", sbtReloadTimeout) @@ -87,4 +85,5 @@ object SbtMain { () } + } diff --git a/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtProcess.scala b/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtProcess.scala index 95774f696..a334cfa11 100644 --- a/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtProcess.scala +++ b/sbt-runner/src/main/scala/com.olegych.scastie.sbt/SbtProcess.scala @@ -2,43 +2,44 @@ package com.olegych.scastie.sbt import java.nio.file._ import java.time.Instant +import scala.concurrent.duration._ +import scala.util.Random import akka.actor.{ActorRef, Cancellable, FSM, Stash} import akka.pattern.ask import akka.util.Timeout import com.olegych.scastie.api._ import com.olegych.scastie.instrumentation.InstrumentedInputs -import com.olegych.scastie.util.ScastieFileUtil.{slurp, write} import com.olegych.scastie.util._ - -import scala.concurrent.duration._ -import scala.util.Random +import com.olegych.scastie.util.ScastieFileUtil.{slurp, write} object SbtProcess { sealed trait SbtState case object Initializing extends SbtState - case object Ready extends SbtState - case object Reloading extends SbtState - case object Running extends SbtState + case object Ready extends SbtState + case object Reloading extends SbtState + case object Running extends SbtState sealed trait Data case class SbtData(currentInputs: Inputs) extends Data + case class SbtRun( - snippetId: SnippetId, - inputs: Inputs, - isForcedProgramMode: Boolean, - progressActor: ActorRef, - snippetActor: ActorRef, - timeoutEvent: Option[Cancellable] + snippetId: SnippetId, + inputs: Inputs, + isForcedProgramMode: Boolean, + progressActor: ActorRef, + snippetActor: ActorRef, + timeoutEvent: Option[Cancellable] ) extends Data + case class SbtStateTimeout(duration: FiniteDuration, state: SbtState) { + def message: String = { - val stateMsg = - state match { - case Reloading => "updating build configuration" - case Running => "running code" - case _ => sys.error(s"unexpected timeout in state $state") - } + val stateMsg = state match { + case Reloading => "updating build configuration" + case Running => "running code" + case _ => sys.error(s"unexpected timeout in state $state") + } s"timed out after $duration when $stateMsg" } @@ -58,19 +59,22 @@ object SbtProcess { ) ) } + } + } -class SbtProcess(runTimeout: FiniteDuration, - reloadTimeout: FiniteDuration, - isProduction: Boolean, - javaOptions: Seq[String], - customSbtDir: Option[Path] = None) - extends FSM[SbtProcess.SbtState, SbtProcess.Data] - with Stash { +class SbtProcess( + runTimeout: FiniteDuration, + reloadTimeout: FiniteDuration, + isProduction: Boolean, + javaOptions: Seq[String], + customSbtDir: Option[Path] = None +) extends FSM[SbtProcess.SbtState, SbtProcess.Data] + with Stash { + import context.dispatcher import ProcessActor._ import SbtProcess._ - import context.dispatcher private var progressId = 0L @@ -80,15 +84,13 @@ class SbtProcess(runTimeout: FiniteDuration, run.progressActor ! p implicit val tm = Timeout(10.seconds) (run.snippetActor ? p) - .recover { - case e => - log.error(e, s"error while saving progress $p") + .recover { case e => + log.error(e, s"error while saving progress $p") } } - private val sbtDir: Path = - customSbtDir.getOrElse(Files.createTempDirectory("scastie")) - private val buildFile = sbtDir.resolve("build.sbt") + private val sbtDir: Path = customSbtDir.getOrElse(Files.createTempDirectory("scastie")) + private val buildFile = sbtDir.resolve("build.sbt") private val promptUniqueId = Random.alphanumeric.take(10).mkString private val projectDir = sbtDir.resolve("project") @@ -96,7 +98,7 @@ class SbtProcess(runTimeout: FiniteDuration, // log.info(s"sbtVersion: $sbtVersion") write(projectDir.resolve("build.properties"), s"sbt.version = ${com.olegych.scastie.buildinfo.BuildInfo.sbtVersion}") private val pluginFile = projectDir.resolve("plugins.sbt") - private val codeFile = sbtDir.resolve("src/main/scala/main.scala") + private val codeFile = sbtDir.resolve("src/main/scala/main.scala") Files.createDirectories(codeFile.getParent) private def scalaJsContent(): Option[String] = { @@ -122,21 +124,19 @@ class SbtProcess(runTimeout: FiniteDuration, ) private lazy val process = { - val sbtOpts = - (javaOptions ++ Seq( - "-Djline.terminal=jline.UnsupportedTerminal", - "-Dsbt.log.noformat=true", - "-Dsbt.banner=false", - )).mkString(" ") - - val props = - ProcessActor.props( - command = List("sbt"), - workingDir = sbtDir, - environment = Map( - "SBT_OPTS" -> sbtOpts - ) + val sbtOpts = (javaOptions ++ Seq( + "-Djline.terminal=jline.UnsupportedTerminal", + "-Dsbt.log.noformat=true", + "-Dsbt.banner=false" + )).mkString(" ") + + val props = ProcessActor.props( + command = List("sbt"), + workingDir = sbtDir, + environment = Map( + "SBT_OPTS" -> sbtOpts ) + ) context.actorOf(props, name = s"sbt-process-$promptUniqueId") } @@ -157,61 +157,59 @@ class SbtProcess(runTimeout: FiniteDuration, case _ -> Ready => println("-- Ready --") unstashAll() - case _ -> Initializing => - println("-- Initializing --") - case _ -> Reloading => - println("-- Reloading --") - case _ -> Running => - println("-- Running --") + case _ -> Initializing => println("-- Initializing --") + case _ -> Reloading => println("-- Reloading --") + case _ -> Running => println("-- Running --") } - when(Initializing) { - case Event(out: ProcessOutput, _) => - if (isPrompt(out.line)) { - goto(Ready) - } else { - stay() - } + when(Initializing) { case Event(out: ProcessOutput, _) => + if (isPrompt(out.line)) { + goto(Ready) + } else { + stay() + } } - when(Ready) { - case Event(task @ SbtTask(snippetId, taskInputs, ip, login, progressActor), SbtData(stateInputs)) => - println(s"Running: (login: $login, ip: $ip) \n ${taskInputs.code.take(30)}") - - val _sbtRun = SbtRun( - snippetId = snippetId, - inputs = taskInputs, - isForcedProgramMode = false, - progressActor = progressActor, - snippetActor = sender(), - timeoutEvent = None - ) - sendProgress(_sbtRun, SnippetProgress.default.copy(isDone = false, ts = Some(Instant.now.toEpochMilli), snippetId = Some(snippetId))) + when(Ready) { case Event(task @ SbtTask(snippetId, taskInputs, ip, login, progressActor), SbtData(stateInputs)) => + println(s"Running: (login: $login, ip: $ip) \n ${taskInputs.code.take(30)}") + + val _sbtRun = SbtRun( + snippetId = snippetId, + inputs = taskInputs, + isForcedProgramMode = false, + progressActor = progressActor, + snippetActor = sender(), + timeoutEvent = None + ) + sendProgress( + _sbtRun, + SnippetProgress.default.copy(isDone = false, ts = Some(Instant.now.toEpochMilli), snippetId = Some(snippetId)) + ) + + InstrumentedInputs(taskInputs) match { + case Right(instrumented) => + val sbtRun = _sbtRun.copy(inputs = instrumented.inputs, isForcedProgramMode = instrumented.isForcedProgramMode) + val isReloading = stateInputs.needsReload(sbtRun.inputs) + setInputs(sbtRun.inputs) + + instrumented.optionalParsingError.foreach { error => + sendProgress(sbtRun, error.toProgress(snippetId).copy(isDone = false)) + } - InstrumentedInputs(taskInputs) match { - case Right(instrumented) => - val sbtRun = _sbtRun.copy(inputs = instrumented.inputs, isForcedProgramMode = instrumented.isForcedProgramMode) - val isReloading = stateInputs.needsReload(sbtRun.inputs) - setInputs(sbtRun.inputs) - - instrumented.optionalParsingError.foreach { error => - sendProgress(sbtRun, error.toProgress(snippetId).copy(isDone = false)) - } - - if (isReloading) { - process ! Input("reload;compile/compileInputs") - gotoWithTimeout(sbtRun, Reloading, reloadTimeout) - } else { - gotoRunning(sbtRun) - } - - case Left(report) => - log.info(s"Instrumentation error: ${report.message}") - val sbtRun = _sbtRun - setInputs(sbtRun.inputs) - sendProgress(sbtRun, report.toProgress(snippetId)) - goto(Ready) - } + if (isReloading) { + process ! Input("reload;compile/compileInputs") + gotoWithTimeout(sbtRun, Reloading, reloadTimeout) + } else { + gotoRunning(sbtRun) + } + + case Left(report) => + log.info(s"Instrumentation error: ${report.message}") + val sbtRun = _sbtRun + setInputs(sbtRun.inputs) + sendProgress(sbtRun, report.toProgress(snippetId)) + goto(Ready) + } } val extractor = new OutputExtractor( @@ -221,45 +219,42 @@ class SbtProcess(runTimeout: FiniteDuration, promptUniqueId ) - when(Reloading) { - case Event(output: ProcessOutput, sbtRun: SbtRun) => - val progress = extractor.extractProgress(output, sbtRun, isReloading = true) - sendProgress(sbtRun, progress) + when(Reloading) { case Event(output: ProcessOutput, sbtRun: SbtRun) => + val progress = extractor.extractProgress(output, sbtRun, isReloading = true) + sendProgress(sbtRun, progress) - if (progress.isSbtError) { - throw new Exception("sbt error: " + output.line) - } + if (progress.isSbtError) { + throw new Exception("sbt error: " + output.line) + } - if (isPrompt(output.line)) { - gotoRunning(sbtRun) - } else { - stay() - } + if (isPrompt(output.line)) { + gotoRunning(sbtRun) + } else { + stay() + } } - when(Running) { - case Event(output: ProcessOutput, sbtRun: SbtRun) => - val progress = extractor.extractProgress(output, sbtRun, isReloading = false) - sendProgress(sbtRun, progress) + when(Running) { case Event(output: ProcessOutput, sbtRun: SbtRun) => + val progress = extractor.extractProgress(output, sbtRun, isReloading = false) + sendProgress(sbtRun, progress) - if (progress.isDone) { - sbtRun.timeoutEvent.foreach(_.cancel()) - goto(Ready).using(SbtData(sbtRun.inputs)) - } else { - stay() - } + if (progress.isDone) { + sbtRun.timeoutEvent.foreach(_.cancel()) + goto(Ready).using(SbtData(sbtRun.inputs)) + } else { + stay() + } } private def gotoWithTimeout(sbtRun: SbtRun, nextState: SbtState, duration: FiniteDuration): this.State = { sbtRun.timeoutEvent.foreach(_.cancel()) - val timeout = - context.system.scheduler.scheduleOnce( - duration, - self, - SbtStateTimeout(duration, nextState) - ) + val timeout = context.system.scheduler.scheduleOnce( + duration, + self, + SbtStateTimeout(duration, nextState) + ) goto(nextState).using(sbtRun.copy(timeoutEvent = Some(timeout))) } @@ -276,8 +271,7 @@ class SbtProcess(runTimeout: FiniteDuration, // Sbt files setup private def setInputs(inputs: Inputs): Unit = { - val prompt = - s"""shellPrompt := {_ => println(""); "$promptUniqueId" + "\\n "}""" + val prompt = s"""shellPrompt := {_ => println(""); "$promptUniqueId" + "\\n "}""" writeFile(pluginFile, inputs.sbtPluginsConfig + "\n") writeFile(buildFile, prompt + "\n" + inputs.sbtConfig) diff --git a/sbt-runner/src/test/scala/com.olegych.scastie.sbt/FormatActorTest.scala b/sbt-runner/src/test/scala/com.olegych.scastie.sbt/FormatActorTest.scala index 78e1e86a4..5b687d572 100644 --- a/sbt-runner/src/test/scala/com.olegych.scastie.sbt/FormatActorTest.scala +++ b/sbt-runner/src/test/scala/com.olegych.scastie.sbt/FormatActorTest.scala @@ -2,8 +2,8 @@ package com.olegych.scastie.sbt import com.olegych.scastie.api.ScalaTarget import com.olegych.scastie.sbt.FormatActor -import org.scalatest.Assertions._ import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.Assertions._ class FormatActorTest extends AnyFunSuite { test("format should accept scala 2 code") { @@ -19,7 +19,7 @@ class FormatActorTest extends AnyFunSuite { } test("format should accept scala 2 worksheets") { - val code = "val x:Int=41+1" + val code = "val x:Int=41+1" val output = "val x: Int = 41 + 1\n" assert(ScalaTarget.Jvm.default.hasWorksheetMode) @@ -39,7 +39,7 @@ class FormatActorTest extends AnyFunSuite { } test("format should accept scala 3 worksheets") { - val code = "val x:Int=41+1" + val code = "val x:Int=41+1" val output = "val x: Int = 41 + 1\n" assert(ScalaTarget.Scala3.default.hasWorksheetMode) diff --git a/sbt-runner/src/test/scala/com.olegych.scastie.sbt/SbtActorTest.scala b/sbt-runner/src/test/scala/com.olegych.scastie.sbt/SbtActorTest.scala index 422ae6bae..efd9b5625 100644 --- a/sbt-runner/src/test/scala/com.olegych.scastie.sbt/SbtActorTest.scala +++ b/sbt-runner/src/test/scala/com.olegych.scastie.sbt/SbtActorTest.scala @@ -1,16 +1,20 @@ package com.olegych.scastie.sbt +import scala.concurrent.duration._ + import akka.actor.{ActorRef, ActorSystem, Props} -import akka.testkit.TestActor.AutoPilot import akka.testkit.{ImplicitSender, TestKit, TestProbe} +import akka.testkit.TestActor.AutoPilot import com.olegych.scastie.api._ import com.olegych.scastie.util.SbtTask -import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.BeforeAndAfterAll -import scala.concurrent.duration._ - -class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitSender with AnyFunSuiteLike with BeforeAndAfterAll { +class SbtActorTest() + extends TestKit(ActorSystem("SbtActorTest")) + with ImplicitSender + with AnyFunSuiteLike + with BeforeAndAfterAll { setAutoPilot(new AutoPilot { def run(sender: ActorRef, msg: Any): AutoPilot = { sender ! s"reply to $msg" @@ -76,7 +80,7 @@ class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitS val message = "Hello" runCode( s"""object Main { def main(args: Array[String]): Unit = println("$message") }""", - allowFailure = true, + allowFailure = true ) { progress => if (progress.isDone) progress.isForcedProgramMode else false } @@ -115,46 +119,47 @@ class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitS } test("Scala.js support") { - val scalaJs = - Inputs.default.copy(code = "1 + 1", target = ScalaTarget.Js.default) + val scalaJs = Inputs.default.copy(code = "1 + 1", target = ScalaTarget.Js.default) run(scalaJs)(_.isDone) } test("Scala.js 3 support") { - val scalaJs = - Inputs.default.copy(code = "1 + 1", - target = ScalaTarget.Js.default.copy(scalaVersion = com.olegych.scastie.buildinfo.BuildInfo.latestLTS)) + val scalaJs = Inputs.default.copy( + code = "1 + 1", + target = ScalaTarget.Js.default.copy(scalaVersion = com.olegych.scastie.buildinfo.BuildInfo.latestLTS) + ) run(scalaJs)(_.isDone) } test("Scala 2.10 support") { - val scala = - Inputs.default.copy(code = "println(1 + 1)", target = ScalaTarget.Jvm(com.olegych.scastie.buildinfo.BuildInfo.latest210)) + val scala = Inputs.default + .copy(code = "println(1 + 1)", target = ScalaTarget.Jvm(com.olegych.scastie.buildinfo.BuildInfo.latest210)) run(scala)(assertUserOutput("2")) } test("Scala 2.11 support") { - val scala = - Inputs.default.copy(code = "println(1 + 1)", target = ScalaTarget.Jvm(com.olegych.scastie.buildinfo.BuildInfo.latest211)) + val scala = Inputs.default + .copy(code = "println(1 + 1)", target = ScalaTarget.Jvm(com.olegych.scastie.buildinfo.BuildInfo.latest211)) run(scala)(assertUserOutput("2")) } test("Scala 2.12 support") { - val scala = - Inputs.default.copy(code = "println(1 + 1)", target = ScalaTarget.Jvm(com.olegych.scastie.buildinfo.BuildInfo.latest212)) + val scala = Inputs.default + .copy(code = "println(1 + 1)", target = ScalaTarget.Jvm(com.olegych.scastie.buildinfo.BuildInfo.latest212)) run(scala)(assertUserOutput("2")) } test("avoid https://github.com/scala/bug/issues/8119") { - val scala = - Inputs.default.copy(code = "val n = 0; val m = List(1).par.foreach(_ => n); println(1)", - target = ScalaTarget.Jvm(com.olegych.scastie.buildinfo.BuildInfo.latest212)) + val scala = Inputs.default.copy( + code = "val n = 0; val m = List(1).par.foreach(_ => n); println(1)", + target = ScalaTarget.Jvm(com.olegych.scastie.buildinfo.BuildInfo.latest212) + ) run(scala)(assertUserOutput("1")) } test("Scala 2.13 support") { - val scala = - Inputs.default.copy(code = "println(1 + 1)", target = ScalaTarget.Jvm(com.olegych.scastie.buildinfo.BuildInfo.latest213)) + val scala = Inputs.default + .copy(code = "println(1 + 1)", target = ScalaTarget.Jvm(com.olegych.scastie.buildinfo.BuildInfo.latest213)) run(scala)(assertUserOutput("2")) } @@ -165,10 +170,11 @@ class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitS } test("no warnings on 2.12") { - val scala = - Inputs.default.copy(code = "println(1 + 1)", - sbtConfigExtra = """scalacOptions ++= List("-Xlint", "-Xfatal-warnings")""", - target = ScalaTarget.Jvm("2.12.10")) + val scala = Inputs.default.copy( + code = "println(1 + 1)", + sbtConfigExtra = """scalacOptions ++= List("-Xlint", "-Xfatal-warnings")""", + target = ScalaTarget.Jvm("2.12.10") + ) run(scala)(assertUserOutput("2")) } @@ -191,7 +197,7 @@ class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitS val message = "Hello, Scala 3 worksheet!" val dotty = Inputs.default.copy( code = s"""println("$message")""", - target = ScalaTarget.Scala3.default, + target = ScalaTarget.Scala3.default ) run(dotty)(assertUserOutput("Hello, Scala 3 worksheet!")) } @@ -200,7 +206,7 @@ class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitS val message = "Hello, Scala 3.0 worksheet!" val dotty = Inputs.default.copy( code = s"""println("$message")""", - target = ScalaTarget.Scala3("3.0.0"), + target = ScalaTarget.Scala3("3.0.0") ) run(dotty)(assertUserOutput("Hello, Scala 3.0 worksheet!")) } @@ -220,7 +226,7 @@ class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitS val dotty = Inputs.default.copy( code = s"""|println("Hello world!") |// test comment""".stripMargin, - target = ScalaTarget.Jvm.default, + target = ScalaTarget.Jvm.default ) run(dotty)(assertUserOutput("Hello world!")) } @@ -229,13 +235,15 @@ class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitS val dotty = Inputs.default.copy( code = s"""|println("Hello world!") |// test comment""".stripMargin, - target = ScalaTarget.Scala3.default, + target = ScalaTarget.Scala3.default ) run(dotty)(assertUserOutput("Hello world!")) } test("hide Playground from types") { - runCode("case class A(i:Int) extends AnyVal; A(1)")(_.instrumentations.headOption.exists(_.render == Value("A(1)", "A"))) + runCode("case class A(i:Int) extends AnyVal; A(1)")( + _.instrumentations.headOption.exists(_.render == Value("A(1)", "A")) + ) } test("#304 null pointer") { @@ -254,7 +262,7 @@ class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitS } def assertCompilationInfo( - infoAssert: Problem => Any + infoAssert: Problem => Any )(progress: SnippetProgress): Boolean = { val gotCompilationError = progress.compilationInfos.nonEmpty @@ -287,14 +295,17 @@ class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitS ) private var currentId = 0 + private def snippetId = { val t = currentId currentId += 1 SnippetId(t.toString, None) } + private var firstRun = true + private def run(inputs: Inputs, allowFailure: Boolean = false)(fish: SnippetProgress => Boolean): Unit = { - val ip = "my-ip" + val ip = "my-ip" val progressActor = TestProbe() sbtActor ! SbtTask(snippetId, inputs, ip, None, progressActor.ref) @@ -303,26 +314,25 @@ class SbtActorTest() extends TestKit(ActorSystem("SbtActorTest")) with ImplicitS if (firstRun) timeout + 10.second else timeout - progressActor.fishForMessage(totalTimeout + 100.seconds) { - case progress: SnippetProgress => - val fishResult = fish(progress) - // println(progress -> fishResult) - if ((progress.isFailure && !allowFailure) || (progress.isDone && !fishResult)) - throw new Exception(s"Fail to meet expectation at ${progress}") - else fishResult + progressActor.fishForMessage(totalTimeout + 100.seconds) { case progress: SnippetProgress => + val fishResult = fish(progress) + // println(progress -> fishResult) + if ((progress.isFailure && !allowFailure) || (progress.isDone && !fishResult)) + throw new Exception(s"Fail to meet expectation at ${progress}") + else fishResult } firstRun = false } private def runCode(code: String, target: ScalaTarget = ScalaTarget.Jvm.default, allowFailure: Boolean = false)( - fish: SnippetProgress => Boolean + fish: SnippetProgress => Boolean ): Unit = { run(Inputs.default.copy(code = code, target = target), allowFailure)(fish) } private def assertUserOutput( - message: String, - outputType: ProcessOutputType = ProcessOutputType.StdOut + message: String, + outputType: ProcessOutputType = ProcessOutputType.StdOut )(progress: SnippetProgress): Boolean = { val gotHelloMessage = progress.userOutput.exists(out => out.line == message && out.tpe == outputType) // if (!gotHelloMessage) assert(progress.userOutput.isEmpty) diff --git a/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/CompilerReporter.scala b/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/CompilerReporter.scala index 1b7c526b9..65a081249 100644 --- a/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/CompilerReporter.scala +++ b/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/CompilerReporter.scala @@ -1,19 +1,17 @@ package com.olegych.scastie.sbtscastie -import com.olegych.scastie.api +import java.util.Optional +import com.olegych.scastie.api import play.api.libs.json.Json - import sbt._ -import Keys._ +import xsbti.{Position, Problem, Reporter, Severity} import KeyRanks.DTask - +import Keys._ import System.{lineSeparator => nl} -import xsbti.{Reporter, Problem, Position, Severity} -import java.util.Optional - object CompilerReporter { + // compilerReporter is marked private in sbt private lazy val compilerReporter = TaskKey[xsbti.Reporter]( "compilerReporter", @@ -21,44 +19,43 @@ object CompilerReporter { DTask ) - val setting: sbt.Def.Setting[_] = - Compile / compile / compilerReporter := new xsbti.Reporter { - private val buffer = collection.mutable.ArrayBuffer.empty[Problem] - def reset(): Unit = buffer.clear() - def hasErrors: Boolean = buffer.exists(_.severity == Severity.Error) - def hasWarnings: Boolean = buffer.exists(_.severity == Severity.Warn) - - def printSummary(): Unit = { - def toApi(p: Problem): api.Problem = { - def toOption[T](m: Optional[T]): Option[T] = { - if (!m.isPresent) None - else Some(m.get) - } - val severity = - p.severity match { - case xsbti.Severity.Info => api.Info - case xsbti.Severity.Warn => api.Warning - case xsbti.Severity.Error => api.Error - } - api.Problem(severity, toOption(p.position.line).map(_.toInt), p.message) + val setting: sbt.Def.Setting[_] = Compile / compile / compilerReporter := new xsbti.Reporter { + private val buffer = collection.mutable.ArrayBuffer.empty[Problem] + def reset(): Unit = buffer.clear() + def hasErrors: Boolean = buffer.exists(_.severity == Severity.Error) + def hasWarnings: Boolean = buffer.exists(_.severity == Severity.Warn) + + def printSummary(): Unit = { + def toApi(p: Problem): api.Problem = { + def toOption[T](m: Optional[T]): Option[T] = { + if (!m.isPresent) None + else Some(m.get) } - if (problems.nonEmpty) { - val apiProblems = problems.map(toApi) - println(Json.stringify(Json.toJson(apiProblems))) + val severity = p.severity match { + case xsbti.Severity.Info => api.Info + case xsbti.Severity.Warn => api.Warning + case xsbti.Severity.Error => api.Error } + api.Problem(severity, toOption(p.position.line).map(_.toInt), p.message) + } + if (problems.nonEmpty) { + val apiProblems = problems.map(toApi) + println(Json.stringify(Json.toJson(apiProblems))) } - def problems: Array[Problem] = buffer.toArray + } + def problems: Array[Problem] = buffer.toArray // def log(pos: Position, msg: String, sev: Severity): Unit = { - def log(problem: Problem): Unit = { - object MyProblem extends Problem { - def category: String = "foo" - def severity: Severity = problem.severity() - def message: String = problem.message() - def position: Position = problem.position() - override def toString = s"$position:$severity: $message" - } - buffer.append(MyProblem) + def log(problem: Problem): Unit = { + object MyProblem extends Problem { + def category: String = "foo" + def severity: Severity = problem.severity() + def message: String = problem.message() + def position: Position = problem.position() + override def toString = s"$position:$severity: $message" } - def comment(pos: xsbti.Position, msg: String): Unit = () + buffer.append(MyProblem) } + def comment(pos: xsbti.Position, msg: String): Unit = () + } + } diff --git a/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/RuntimeErrorLogger.scala b/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/RuntimeErrorLogger.scala index 46e15dd83..e961ebeb2 100644 --- a/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/RuntimeErrorLogger.scala +++ b/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/RuntimeErrorLogger.scala @@ -1,19 +1,20 @@ package sbt.internal.util.com.olegych.scastie.sbtscastie +import java.io.{OutputStream, PrintWriter} +import java.nio.channels.ClosedChannelException +import java.util.concurrent.atomic.AtomicReference + import com.olegych.scastie.api._ import org.apache.logging.log4j.core.{Appender => XAppender, LogEvent => XLogEvent} import org.apache.logging.log4j.message.ObjectMessage import play.api.libs.json.Json -import sbt.Keys._ import sbt._ -import sbt.internal.util.ConsoleAppender.Properties import sbt.internal.util.{ConsoleAppender, Log4JConsoleAppender, ObjectEvent, TraceEvent} - -import java.io.{OutputStream, PrintWriter} -import java.nio.channels.ClosedChannelException -import java.util.concurrent.atomic.AtomicReference +import sbt.internal.util.ConsoleAppender.Properties +import sbt.Keys._ object RuntimeErrorLogger { + private val scastieOut = new PrintWriter(new OutputStream { def out(in: String): Unit = { println( @@ -24,37 +25,37 @@ object RuntimeErrorLogger { ) ) } - override def write(b: Int): Unit = () - override def write(b: Array[Byte]): Unit = out(new String(b)) + override def write(b: Int): Unit = () + override def write(b: Array[Byte]): Unit = out(new String(b)) override def write(b: Array[Byte], off: Int, len: Int): Unit = out(new String(b, off, len)) - override def close(): Unit = () - override def flush(): Unit = () + override def close(): Unit = () + override def flush(): Unit = () }) private def findThrowable(event: XLogEvent) = { - //daaamn + // daaamn Option(event.getThrown).orElse { for { - e <- Option(event.getMessage).collect { - case e: ObjectMessage => e + e <- Option(event.getMessage).collect { case e: ObjectMessage => + e } - e <- Option(e.getParameter).collect { - case e: ObjectEvent[_] => e + e <- Option(e.getParameter).collect { case e: ObjectEvent[_] => + e } - e <- Option(e.message).collect { - case e: TraceEvent => e + e <- Option(e.message).collect { case e: TraceEvent => + e } - //since worksheet wraps the code in object we unwrap it to display clearer message + // since worksheet wraps the code in object we unwrap it to display clearer message e <- Option(e.message).collect { case e: ExceptionInInitializerError if e.getCause != null && e.getCause.getStackTrace.headOption.exists { e => e.getClassName == Instrumentation.instrumentedObject + "$" && e.getMethodName == "" - } => - e.getCause + } => e.getCause case e => e } } yield e } } + private def logThrowable(throwable: Throwable): Unit = { val error = RuntimeErrorWrap(RuntimeError.fromThrowable(throwable)) println(Json.stringify(Json.toJson(error))) @@ -62,35 +63,39 @@ object RuntimeErrorLogger { val settings: Seq[sbt.Def.Setting[_]] = Seq( showSuccess := false, - useLog4J := true, - logManager := sbt.internal.LogManager.withLoggers( - (_, _) => - new ConsoleAppender(ConsoleAppender.generateName, Properties.from(ConsoleOut.printWriterOut(scastieOut), true, false), _ => None) { - override def trace(t: => Throwable, traceLevel: Int): Unit = logThrowable(t) - private[this] val log4j = new AtomicReference[XAppender](null) - private[sbt] override lazy val toLog4J = log4j.get match { - case null => - log4j.synchronized { - log4j.get match { - case null => - val l = new Log4JConsoleAppender( - name, - properties, - suppressedMessage, { event => - val level = ConsoleAppender.toLevel(event.getLevel) - val message = event.getMessage - findThrowable(event).foreach(logThrowable) - try appendMessage(level, message) - catch { case _: ClosedChannelException => } - } - ) - log4j.set(l) - l - case l => l - } + useLog4J := true, + logManager := sbt.internal.LogManager.withLoggers((_, _) => + new ConsoleAppender( + ConsoleAppender.generateName, + Properties.from(ConsoleOut.printWriterOut(scastieOut), true, false), + _ => None + ) { + override def trace(t: => Throwable, traceLevel: Int): Unit = logThrowable(t) + private[this] val log4j = new AtomicReference[XAppender](null) + private[sbt] override lazy val toLog4J = log4j.get match { + case null => log4j.synchronized { + log4j.get match { + case null => + val l = new Log4JConsoleAppender( + name, + properties, + suppressedMessage, + { event => + val level = ConsoleAppender.toLevel(event.getLevel) + val message = event.getMessage + findThrowable(event).foreach(logThrowable) + try appendMessage(level, message) + catch { case _: ClosedChannelException => } + } + ) + log4j.set(l) + l + case l => l } - } + } + } } - ), + ) ) + } diff --git a/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/SbtScastiePlugin.scala b/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/SbtScastiePlugin.scala index 62221b6dc..b786bcb40 100644 --- a/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/SbtScastiePlugin.scala +++ b/sbt-scastie/src/main/scala/com.olegych.scastie.sbtscastie/SbtScastiePlugin.scala @@ -1,32 +1,31 @@ package com.olegych.scastie package sbtscastie -import sbt.Keys.* +import scala.util.{Failure, Success, Try} + import sbt.* import sbt.internal.inc.AnalyzingCompiler - -import scala.util.{Success, Try, Failure} +import sbt.Keys.* object SbtScastiePlugin extends AutoPlugin { override def requires = sbt.plugins.JvmPlugin - override def trigger = allRequirements + override def trigger = allRequirements override lazy val projectSettings: Seq[sbt.Def.Setting[_]] = (CompilerReporter.setting +: sbt.internal.util.com.olegych.scastie.sbtscastie.RuntimeErrorLogger.settings) ++ Seq( - //workaround https://github.com/sbt/sbt/issues/5482 + // workaround https://github.com/sbt/sbt/issues/5482 Global / nio.Keys.onChangedBuildSource := nio.Keys.IgnoreSourceChanges, - turbo := true, - useSuperShell := false, - autoStartServer := false, + turbo := true, + useSuperShell := false, + autoStartServer := false, compilers := { val r = compilers.value - //compile bridge to init everything on reload + // compile bridge to init everything on reload r.scalac() match { - case c: AnalyzingCompiler => - c.provider.fetchCompiledBridge(c.scalaInstance, streams.value.log) - case _ => () + case c: AnalyzingCompiler => c.provider.fetchCompiledBridge(c.scalaInstance, streams.value.log) + case _ => () } r }, @@ -49,10 +48,13 @@ object SbtScastiePlugin extends AutoPlugin { resolvers := { Seq[Resolver]( Resolver - .url("my-ivy-proxy-releases", url("http://scala-webapps.epfl.ch:8081/artifactory/scastie-ivy/"))(Resolver.ivyStylePatterns) + .url("my-ivy-proxy-releases", url("http://scala-webapps.epfl.ch:8081/artifactory/scastie-ivy/"))( + Resolver.ivyStylePatterns + ) .withAllowInsecureProtocol(true), - "my-maven-proxy-releases" at "http://scala-webapps.epfl.ch:8081/artifactory/scastie-maven/" withAllowInsecureProtocol (true), + "my-maven-proxy-releases" at "http://scala-webapps.epfl.ch:8081/artifactory/scastie-maven/" withAllowInsecureProtocol (true) ) ++ resolvers.value - }, + } ) + } diff --git a/sbt-scastie/src/main/scala/sbt/ScastieTrapExit.scala b/sbt-scastie/src/main/scala/sbt/ScastieTrapExit.scala index bdd61508a..c9f3c1f98 100644 --- a/sbt-scastie/src/main/scala/sbt/ScastieTrapExit.scala +++ b/sbt-scastie/src/main/scala/sbt/ScastieTrapExit.scala @@ -7,56 +7,52 @@ package sbt -import scala.annotation.nowarn -import scala.reflect.Manifest -import scala.collection.concurrent.TrieMap import java.lang.ref.WeakReference -import Thread.currentThread +import java.lang.Integer.{toHexString => hex} import java.security.Permission import java.util.concurrent.{ConcurrentHashMap => CMap} -import java.lang.Integer.{toHexString => hex} import java.util.function.Supplier +import scala.annotation.nowarn +import scala.collection.concurrent.TrieMap +import scala.reflect.Manifest import sbt.util.InterfaceUtil import ScastieTrapExit._ +import Thread.currentThread /** - * Provides an approximation to isolated execution within a single JVM. - * System.exit calls are trapped to prevent the JVM from terminating. This is useful for executing - * user code that may call System.exit, but actually exiting is undesirable. - * - * Exit is simulated by disposing all top-level windows and interrupting user-started threads. - * Threads are not stopped and shutdown hooks are not called. It is - * therefore inappropriate to use this with code that requires shutdown hooks, creates threads that - * do not terminate, or if concurrent AWT applications are run. - * This category of code should only be called by forking a new JVM. - */ + * Provides an approximation to isolated execution within a single JVM. System.exit calls are trapped to prevent the + * JVM from terminating. This is useful for executing user code that may call System.exit, but actually exiting is + * undesirable. + * + * Exit is simulated by disposing all top-level windows and interrupting user-started threads. Threads are not stopped + * and shutdown hooks are not called. It is therefore inappropriate to use this with code that requires shutdown hooks, + * creates threads that do not terminate, or if concurrent AWT applications are run. This category of code should only + * be called by forking a new JVM. + */ @nowarn object ScastieTrapExit { /** - * Run `execute` in a managed context, using `log` for debugging messages. - * `installManager` must be called before calling this method. - */ - def apply(execute: => Unit, log: Logger): Int = - System.getSecurityManager match { - case m: ScastieTrapExit => m.runManaged(InterfaceUtil.toSupplier(execute), log) - case _ => runUnmanaged(execute, log) - } + * Run `execute` in a managed context, using `log` for debugging messages. `installManager` must be called before + * calling this method. + */ + def apply(execute: => Unit, log: Logger): Int = System.getSecurityManager match { + case m: ScastieTrapExit => m.runManaged(InterfaceUtil.toSupplier(execute), log) + case _ => runUnmanaged(execute, log) + } /** - * Installs the SecurityManager that implements the isolation and returns the previously installed SecurityManager, which may be null. - * This method must be called before using `apply`. - */ - def installManager(): SecurityManager = - System.getSecurityManager match { - case m: ScastieTrapExit => m - case m => System.setSecurityManager(new ScastieTrapExit(m)); m - } + * Installs the SecurityManager that implements the isolation and returns the previously installed SecurityManager, + * which may be null. This method must be called before using `apply`. + */ + def installManager(): SecurityManager = System.getSecurityManager match { + case m: ScastieTrapExit => m + case m => System.setSecurityManager(new ScastieTrapExit(m)); m + } /** Uninstalls the isolation SecurityManager and restores the old security manager. */ - def uninstallManager(previous: SecurityManager): Unit = - System.setSecurityManager(previous) + def uninstallManager(previous: SecurityManager): Unit = System.setSecurityManager(previous) private[this] def runUnmanaged(execute: => Unit, log: Logger): Int = { log.warn("Managed execution not possible: security manager not installed.") @@ -72,11 +68,10 @@ object ScastieTrapExit { private type ThreadID = String - /** `true` if the thread `t` is in the TERMINATED state.x*/ + /** `true` if the thread `t` is in the TERMINATED state.x */ private def isDone(t: Thread): Boolean = t.getState == Thread.State.TERMINATED - private def computeID(g: ThreadGroup): ThreadID = - s"g:${hex(System.identityHashCode(g))}:${g.getName}" + private def computeID(g: ThreadGroup): ThreadID = s"g:${hex(System.identityHashCode(g))}:${g.getName}" /** Computes an identifier for a Thread that has a high probability of being unique within a single JVM execution. */ private def computeID(t: Thread): ThreadID = @@ -85,7 +80,9 @@ object ScastieTrapExit { // Apple AWT: +[ThreadUtilities getJNIEnvUncached] attempting to attach current thread after JNFObtainEnv() failed s"${hex(System.identityHashCode(t))}" - /** Waits for the given `thread` to terminate. However, if the thread state is NEW, this method returns immediately. */ + /** + * Waits for the given `thread` to terminate. However, if the thread state is NEW, this method returns immediately. + */ private def waitOnThread(thread: Thread, log: Logger): Unit = { log.debug("Waiting for thread " + thread.getName + " to terminate.") thread.join @@ -99,8 +96,11 @@ object ScastieTrapExit { thread.interrupt log.debug("\tInterrupted " + thread.getName) } + // an uncaught exception handler that swallows InterruptedExceptions and otherwise defers to originalHandler - private final class TrapInterrupt(originalHandler: Thread.UncaughtExceptionHandler) extends Thread.UncaughtExceptionHandler { + private final class TrapInterrupt(originalHandler: Thread.UncaughtExceptionHandler) + extends Thread.UncaughtExceptionHandler { + def uncaughtException(thread: Thread, e: Throwable): Unit = { withCause[InterruptedException, Unit](e) { interrupted => () @@ -109,47 +109,50 @@ object ScastieTrapExit { } thread.setUncaughtExceptionHandler(originalHandler) } + } /** - * Recurses into the causes of the given exception looking for a cause of type CauseType. If one is found, `withType` is called with that cause. - * If not, `notType` is called with the root cause. - */ + * Recurses into the causes of the given exception looking for a cause of type CauseType. If one is found, `withType` + * is called with that cause. If not, `notType` is called with the root cause. + */ private def withCause[CauseType <: Throwable, T]( - e: Throwable - )(withType: CauseType => T)(notType: Throwable => T)(implicit mf: Manifest[CauseType]): T = { + e: Throwable + )(withType: CauseType => T)(notType: Throwable => T)( + implicit mf: Manifest[CauseType] + ): T = { val clazz = mf.runtimeClass - if (clazz.isInstance(e)) - withType(e.asInstanceOf[CauseType]) + if (clazz.isInstance(e)) withType(e.asInstanceOf[CauseType]) else { val cause = e.getCause - if (cause == null) - notType(e) - else - withCause(cause)(withType)(notType)(mf) + if (cause == null) notType(e) + else withCause(cause)(withType)(notType)(mf) } } } /** - * Simulates isolation via a SecurityManager. - * Multiple applications are supported by tracking Thread constructions via `checkAccess`. - * The Thread that constructed each Thread is used to map a new Thread to an application. - * This is not reliable on all jvms, so ThreadGroup creations are also tracked via - * `checkAccess` and traversed on demand to collect threads. - * This association of Threads with an application allows properly waiting for - * non-daemon threads to terminate or to interrupt the correct threads when terminating. - * It also allows disposing AWT windows if the application created any. - * Only one AWT application is supported at a time, however. - */ + * Simulates isolation via a SecurityManager. Multiple applications are supported by tracking Thread constructions via + * `checkAccess`. The Thread that constructed each Thread is used to map a new Thread to an application. This is not + * reliable on all jvms, so ThreadGroup creations are also tracked via `checkAccess` and traversed on demand to collect + * threads. This association of Threads with an application allows properly waiting for non-daemon threads to terminate + * or to interrupt the correct threads when terminating. It also allows disposing AWT windows if the application + * created any. Only one AWT application is supported at a time, however. + */ @nowarn private final class ScastieTrapExit(delegateManager: SecurityManager) extends SecurityManager { - /** Tracks the number of running applications in order to short-cut SecurityManager checks when no applications are active.*/ + /** + * Tracks the number of running applications in order to short-cut SecurityManager checks when no applications are + * active. + */ private[this] val running = new java.util.concurrent.atomic.AtomicInteger - /** Maps a thread or thread group to its originating application. The thread is represented by a unique identifier to avoid leaks. */ + /** + * Maps a thread or thread group to its originating application. The thread is represented by a unique identifier to + * avoid leaks. + */ private[this] val threadToApp = new CMap[ThreadID, App] /** Executes `f` in a managed context. */ @@ -160,9 +163,10 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se running.decrementAndGet(); () } } + private[this] def runManaged0(f: Supplier[Unit], xlog: xsbti.Logger): Int = { - val log: Logger = xlog - val app = new App(f, log) + val log: Logger = xlog + val app = new App(f, log) val executionThread = app.mainThread try { executionThread.start() // thread actually evaluating `f` @@ -182,9 +186,9 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se } /** - * Wait for all non-daemon threads for `app` to exit, for an exception to be thrown in the main thread, - * or for `System.exit` to be called in a thread started by `app`. - */ + * Wait for all non-daemon threads for `app` to exit, for an exception to be thrown in the main thread, or for + * `System.exit` to be called in a thread started by `app`. + */ private[this] def finish(app: App, log: Logger): Int = { log.debug("Waiting for threads to exit or System.exit to be called.") waitForExit(app) @@ -207,28 +211,25 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se } } // processThreads takes a snapshot of the threads at a given moment, so if there were only daemons, the application should shut down - if (!daemonsOnly) - waitForExit(app) + if (!daemonsOnly) waitForExit(app) } /** Gives managed applications a unique ID to use in the IDs of the main thread and thread group. */ - private[this] val nextAppID = new java.util.concurrent.atomic.AtomicLong + private[this] val nextAppID = new java.util.concurrent.atomic.AtomicLong private def nextID(): String = nextAppID.getAndIncrement.toHexString /** - * Represents an isolated application as simulated by [[ScastieTrapExit]]. - * `execute` is the application code to evaluate. - * `log` is used for debug logging. - */ + * Represents an isolated application as simulated by [[ScastieTrapExit]]. `execute` is the application code to + * evaluate. `log` is used for debug logging. + */ private final class App(val execute: Supplier[Unit], val log: Logger) extends Runnable { /** - * Tracks threads and groups created by this application. - * To avoid leaks, keys are a unique identifier and values are held via WeakReference. - * A TrieMap supports the necessary concurrent updates and snapshots. - */ + * Tracks threads and groups created by this application. To avoid leaks, keys are a unique identifier and values + * are held via WeakReference. A TrieMap supports the necessary concurrent updates and snapshots. + */ private[this] val threads = new TrieMap[ThreadID, WeakReference[Thread]] - private[this] val groups = new TrieMap[ThreadID, WeakReference[ThreadGroup]] + private[this] val groups = new TrieMap[ThreadID, WeakReference[ThreadGroup]] /** Tracks whether AWT has ever been used in this jvm execution. */ @volatile @@ -239,15 +240,15 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se /** The ThreadGroup to use to try to track created threads. */ val mainGroup: ThreadGroup = new ThreadGroup("run-main-group-" + id) { - private[this] val handler = new LoggingExceptionHandler(log, None) - override def uncaughtException(t: Thread, e: Throwable): Unit = - handler.uncaughtException(t, e) + private[this] val handler = new LoggingExceptionHandler(log, None) + override def uncaughtException(t: Thread, e: Throwable): Unit = handler.uncaughtException(t, e) } + val mainThread = new Thread(mainGroup, this, "run-main-" + id) /** Saves the ids of the creating thread and thread group to avoid tracking them as coming from this application. */ val creatorThreadID = computeID(currentThread) - val creatorGroup = currentThread.getThreadGroup + val creatorGroup = currentThread.getThreadGroup register(mainThread) register(mainGroup) @@ -258,26 +259,25 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se try execute.get() catch { case x: Throwable => - exitCode.set(1) //exceptions in the main thread cause the exit code to be 1 + exitCode.set(1) // exceptions in the main thread cause the exit code to be 1 throw x } } - /** Records a new group both in the global [[ScastieTrapExit]] manager and for this [[App]].*/ - def register(g: ThreadGroup): Unit = - if (g != null && g != creatorGroup && !isSystemGroup(g)) { - val groupID = computeID(g) - val old = groups.putIfAbsent(groupID, new WeakReference(g)) - if (old.isEmpty) { // wasn't registered - threadToApp.put(groupID, this) - () - } + /** Records a new group both in the global [[ScastieTrapExit]] manager and for this [[App]]. */ + def register(g: ThreadGroup): Unit = if (g != null && g != creatorGroup && !isSystemGroup(g)) { + val groupID = computeID(g) + val old = groups.putIfAbsent(groupID, new WeakReference(g)) + if (old.isEmpty) { // wasn't registered + threadToApp.put(groupID, this) + () } + } /** - * Records a new thread both in the global [[ScastieTrapExit]] manager and for this [[App]]. - * Its uncaught exception handler is configured to log exceptions through `log`. - */ + * Records a new thread both in the global [[ScastieTrapExit]] manager and for this [[App]]. Its uncaught exception + * handler is configured to log exceptions through `log`. + */ def register(t: Thread): Unit = { val threadID = computeID(t) if (!isDone(t) && threadID != creatorThreadID) { @@ -285,8 +285,7 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se if (old.isEmpty) { // wasn't registered threadToApp.put(threadID, this) setExceptionHandler(t) - if (!awtUsed && isEventQueue(t)) - awtUsed = true + if (!awtUsed && isEventQueue(t)) awtUsed = true } } } @@ -314,11 +313,11 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se cleanup(threads) cleanup(groups) } + private[this] def cleanup(resources: TrieMap[ThreadID, _]): Unit = { val snap = resources.readOnlySnapshot resources.clear() - for ((id, _) <- snap) - unregister(id) + for ((id, _) <- snap) unregister(id) } // only want to operate on unterminated threads @@ -332,19 +331,16 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se val snap = threads.readOnlySnapshot for ((id, tref) <- snap) { val t = tref.get - if ((t eq null) || isDone(t)) - unregister(id) + if ((t eq null) || isDone(t)) unregister(id) else { f(t) - if (isDone(t)) - unregister(id) + if (isDone(t)) unregister(id) } } } // registers Threads from the tracked ThreadGroups - private[this] def addUntrackedThreads(): Unit = - groupThreadsSnapshot foreach register + private[this] def addUntrackedThreads(): Unit = groupThreadsSnapshot foreach register private[this] def groupThreadsSnapshot: Seq[Thread] = { val snap = groups.readOnlySnapshot.values.map(_.get).filter(_ != null) @@ -355,8 +351,8 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se // the thread groups are accumulated in `accum` and then the threads in each are collected all at // once while they are all locked. This is the closest thing to a snapshot that can be accomplished. private[this] def threadsInGroups( - toProcess: List[ThreadGroup], - accum: List[ThreadGroup] + toProcess: List[ThreadGroup], + accum: List[ThreadGroup] ): List[Thread] = toProcess match { case group :: tail => // ThreadGroup implementation synchronizes on its methods, so by synchronizing here, we can workaround its quirks somewhat @@ -369,35 +365,34 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se // gets the immediate child ThreadGroups of `group` private[this] def threadGroups(group: ThreadGroup): List[ThreadGroup] = { - val upperBound = group.activeGroupCount - val groups = new Array[ThreadGroup](upperBound) + val upperBound = group.activeGroupCount + val groups = new Array[ThreadGroup](upperBound) val childrenCount = group.enumerate(groups, false) groups.take(childrenCount).toList } // gets the immediate child Threads of `group` private[this] def threads(group: ThreadGroup): List[Thread] = { - val upperBound = group.activeCount - val threads = new Array[Thread](upperBound) + val upperBound = group.activeCount + val threads = new Array[Thread](upperBound) val childrenCount = group.enumerate(threads, false) threads.take(childrenCount).toList } + } private[this] def stopAllThreads(app: App): Unit = { // only try to dispose frames if we think the App used AWT // otherwise, we initialize AWT as a side effect of asking for the frames // also, we only assume one AWT application at a time - if (app.awtUsed) - disposeAllFrames(app.log) + if (app.awtUsed) disposeAllFrames(app.log) interruptAllThreads(app) } - private[this] def interruptAllThreads(app: App): Unit = - app processThreads { t => - if (!isSystemThread(t)) safeInterrupt(t, app.log) - else app.log.debug(s"Not interrupting system thread $t") - } + private[this] def interruptAllThreads(app: App): Unit = app processThreads { t => + if (!isSystemThread(t)) safeInterrupt(t, app.log) + else app.log.debug(s"Not interrupting system thread $t") + } /** Gets the managed application associated with Thread `t` */ private[this] def getApp(t: Thread): Option[App] = @@ -408,9 +403,9 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se Option(group).flatMap(g => Option(threadToApp.get(computeID(g)))) /** - * Handles a valid call to `System.exit` by setting the exit code and - * interrupting remaining threads for the application associated with `t`, if one exists. - */ + * Handles a valid call to `System.exit` by setting the exit code and interrupting remaining threads for the + * application associated with `t`, if one exists. + */ private[this] def exitApp(t: Thread, status: Int): Unit = getApp(t) match { case None => System.err.println(s"Could not exit($status): no application associated with $t") case Some(a) => @@ -418,9 +413,9 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se stopAllThreads(a) } - /** SecurityManager hook to trap calls to `System.exit` to avoid shutting down the whole JVM.*/ + /** SecurityManager hook to trap calls to `System.exit` to avoid shutting down the whole JVM. */ override def checkExit(status: Int): Unit = if (active) { - val t = currentThread + val t = currentThread val stack = t.getStackTrace if (stack == null || stack.exists(isRealExit)) { exitApp(t, status) @@ -428,33 +423,32 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se } } - /** This ensures that only actual calls to exit are trapped and not just calls to check if exit is allowed.*/ + /** This ensures that only actual calls to exit are trapped and not just calls to check if exit is allowed. */ private def isRealExit(element: StackTraceElement): Boolean = element.getClassName == "java.lang.Runtime" && element.getMethodName == "exit" // These are overridden to do nothing because there is a substantial filesystem performance penalty // when there is a SecurityManager defined. The default implementations of these construct a // FilePermission, and its initialization involves canonicalization, which is expensive. - override def checkRead(file: String): Unit = () + override def checkRead(file: String): Unit = () override def checkRead(file: String, context: AnyRef): Unit = () - override def checkWrite(file: String): Unit = () - override def checkDelete(file: String): Unit = () - override def checkExec(cmd: String): Unit = () + override def checkWrite(file: String): Unit = () + override def checkDelete(file: String): Unit = () + override def checkExec(cmd: String): Unit = () override def checkPermission(perm: Permission): Unit = { - if (delegateManager ne null) - delegateManager.checkPermission(perm) + if (delegateManager ne null) delegateManager.checkPermission(perm) } + override def checkPermission(perm: Permission, context: AnyRef): Unit = { - if (delegateManager ne null) - delegateManager.checkPermission(perm, context) + if (delegateManager ne null) delegateManager.checkPermission(perm, context) } /** - * SecurityManager hook that is abused to record every created Thread and associate it with a managed application. - * This is not reliably called on different jvm implementations. On openjdk and similar jvms, the Thread constructor - * calls setPriority, which triggers this SecurityManager check. For Java 6 on OSX, this is not called, however. - */ + * SecurityManager hook that is abused to record every created Thread and associate it with a managed application. + * This is not reliably called on different jvm implementations. On openjdk and similar jvms, the Thread constructor + * calls setPriority, which triggers this SecurityManager check. For Java 6 on OSX, this is not called, however. + */ override def checkAccess(t: Thread): Unit = { if (active) { val group = t.getThreadGroup @@ -464,14 +458,13 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se app.register(currentThread) } } - if (delegateManager ne null) - delegateManager.checkAccess(t) + if (delegateManager ne null) delegateManager.checkAccess(t) } /** - * This is specified to be called in every Thread's constructor and every time a ThreadGroup is created. - * This allows us to reliably track every ThreadGroup that is created and map it back to the constructing application. - */ + * This is specified to be called in every Thread's constructor and every time a ThreadGroup is created. This allows + * us to reliably track every ThreadGroup that is created and map it back to the constructing application. + */ override def checkAccess(tg: ThreadGroup): Unit = { if (active && !isSystemGroup(tg)) { noteAccess(tg) { app => @@ -480,15 +473,13 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se } } - if (delegateManager ne null) - delegateManager.checkAccess(tg) + if (delegateManager ne null) delegateManager.checkAccess(tg) } private[this] def noteAccess(group: ThreadGroup)(f: App => Unit): Unit = getApp(currentThread) orElse getApp(group) foreach f - private[this] def isSystemGroup(group: ThreadGroup): Boolean = - (group != null) && (group.getName == "system") + private[this] def isSystemGroup(group: ThreadGroup): Boolean = (group != null) && (group.getName == "system") /** `true` if there is at least one application currently being managed. */ private[this] def active = running.get > 0 @@ -497,45 +488,46 @@ private final class ScastieTrapExit(delegateManager: SecurityManager) extends Se val allFrames = java.awt.Frame.getFrames if (allFrames.nonEmpty) { log.debug(s"Disposing ${allFrames.length} top-level windows...") - allFrames.foreach(_.dispose) // dispose all top-level windows, which will cause the AWT-EventQueue-* threads to exit + allFrames.foreach( + _.dispose + ) // dispose all top-level windows, which will cause the AWT-EventQueue-* threads to exit val waitSeconds = 2 log.debug(s"Waiting $waitSeconds s to let AWT thread exit.") Thread.sleep(waitSeconds * 1000L) // AWT Thread doesn't exit immediately, so wait to interrupt it } } - /** Returns true if the given thread is in the 'system' thread group or is an AWT thread other than AWT-EventQueue.*/ + /** Returns true if the given thread is in the 'system' thread group or is an AWT thread other than AWT-EventQueue. */ private def isSystemThread(t: Thread) = - if (t.getName.startsWith("AWT-")) - !isEventQueue(t) - else - isSystemGroup(t.getThreadGroup) + if (t.getName.startsWith("AWT-")) !isEventQueue(t) + else isSystemGroup(t.getThreadGroup) /** - * An App is identified as using AWT if it gets associated with the event queue thread. - * The event queue thread is not treated as a system thread. - */ + * An App is identified as using AWT if it gets associated with the event queue thread. The event queue thread is not + * treated as a system thread. + */ private[this] def isEventQueue(t: Thread): Boolean = t.getName.startsWith("AWT-EventQueue") } -/** A thread-safe, write-once, optional cell for tracking an application's exit code.*/ +/** A thread-safe, write-once, optional cell for tracking an application's exit code. */ private final class ExitCode { private var code: Option[Int] = None - def set(c: Int): Unit = synchronized { code = code orElse Some(c) } - def value: Option[Int] = synchronized { code } + def set(c: Int): Unit = synchronized { code = code orElse Some(c) } + def value: Option[Int] = synchronized { code } } /** - * The default uncaught exception handler for managed executions. - * It logs the thread and the exception. - */ + * The default uncaught exception handler for managed executions. It logs the thread and the exception. + */ private final class LoggingExceptionHandler( - log: Logger, - delegate: Option[Thread.UncaughtExceptionHandler] + log: Logger, + delegate: Option[Thread.UncaughtExceptionHandler] ) extends Thread.UncaughtExceptionHandler { + def uncaughtException(t: Thread, e: Throwable): Unit = { log.error("(" + t.getName + ") " + e.toString) log.trace(e) delegate.foreach(_.uncaughtException(t, e)) } + } diff --git a/server/src/main/scala/com.olegych.scastie.web/PlayJsonSupport.scala b/server/src/main/scala/com.olegych.scastie.web/PlayJsonSupport.scala index dc091956a..efc849679 100644 --- a/server/src/main/scala/com.olegych.scastie.web/PlayJsonSupport.scala +++ b/server/src/main/scala/com.olegych.scastie.web/PlayJsonSupport.scala @@ -16,6 +16,8 @@ package com.olegych.scastie.web +import scala.collection.immutable.Seq + import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller} import akka.http.scaladsl.model.ContentTypeRange import akka.http.scaladsl.model.MediaTypes.`application/json` @@ -23,66 +25,65 @@ import akka.http.scaladsl.server.{RejectionError, ValidationRejection} import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} import akka.util.ByteString import play.api.libs.json.{JsError, JsValue, Json, Reads, Writes} -import scala.collection.immutable.Seq /** - * Automatic to and from JSON marshalling/unmarshalling using an in-scope *play-json* protocol. - */ + * Automatic to and from JSON marshalling/unmarshalling using an in-scope *play-json* protocol. + */ object PlayJsonSupport extends PlayJsonSupport { final case class PlayJsonError(error: JsError) extends RuntimeException { - override def getMessage: String = - JsError.toJson(error).toString() + override def getMessage: String = JsError.toJson(error).toString() } + } /** - * Automatic to and from JSON marshalling/unmarshalling using an in-scope *play-json* protocol. - */ + * Automatic to and from JSON marshalling/unmarshalling using an in-scope *play-json* protocol. + */ trait PlayJsonSupport { import PlayJsonSupport._ - def unmarshallerContentTypes: Seq[ContentTypeRange] = - List(`application/json`) + def unmarshallerContentTypes: Seq[ContentTypeRange] = List(`application/json`) - private val jsonStringUnmarshaller = - Unmarshaller.byteStringUnmarshaller - .forContentTypes(unmarshallerContentTypes: _*) - .mapWithCharset { - case (ByteString.empty, _) => throw Unmarshaller.NoContentException - case (data, charset) => data.decodeString(charset.nioCharset.name) - } + private val jsonStringUnmarshaller = Unmarshaller.byteStringUnmarshaller + .forContentTypes(unmarshallerContentTypes: _*) + .mapWithCharset { + case (ByteString.empty, _) => throw Unmarshaller.NoContentException + case (data, charset) => data.decodeString(charset.nioCharset.name) + } - private val jsonStringMarshaller = - Marshaller.stringMarshaller(`application/json`) + private val jsonStringMarshaller = Marshaller.stringMarshaller(`application/json`) /** - * HTTP entity => `A` - * - * @tparam A type to decode - * @return unmarshaller for `A` - */ + * HTTP entity => `A` + * + * @tparam A + * type to decode + * @return + * unmarshaller for `A` + */ implicit def unmarshaller[A: Reads]: FromEntityUnmarshaller[A] = { - def read(json: JsValue) = - implicitly[Reads[A]] - .reads(json) - .recoverTotal { e => - throw RejectionError( - ValidationRejection(JsError.toJson(e).toString, Some(PlayJsonError(e))) - ) - } + def read(json: JsValue) = implicitly[Reads[A]] + .reads(json) + .recoverTotal { e => + throw RejectionError( + ValidationRejection(JsError.toJson(e).toString, Some(PlayJsonError(e))) + ) + } jsonStringUnmarshaller.map(data => read(Json.parse(data))) } /** - * `A` => HTTP entity - * - * @tparam A type to encode - * @return marshaller for any `A` value - */ + * `A` => HTTP entity + * + * @tparam A + * type to encode + * @return + * marshaller for any `A` value + */ implicit def marshaller[A]( - implicit writes: Writes[A], - printer: JsValue => String = Json.prettyPrint - ): ToEntityMarshaller[A] = - jsonStringMarshaller.compose(printer).compose(writes.writes) + implicit writes: Writes[A], + printer: JsValue => String = Json.prettyPrint + ): ToEntityMarshaller[A] = jsonStringMarshaller.compose(printer).compose(writes.writes) + } diff --git a/server/src/main/scala/com.olegych.scastie.web/RestApiServer.scala b/server/src/main/scala/com.olegych.scastie.web/RestApiServer.scala index cb83593d2..80a1e8e99 100644 --- a/server/src/main/scala/com.olegych.scastie.web/RestApiServer.scala +++ b/server/src/main/scala/com.olegych.scastie.web/RestApiServer.scala @@ -1,29 +1,28 @@ package com.olegych.scastie package web -import api._ -import balancer._ +import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.duration.DurationInt -import akka.pattern.ask import akka.actor.ActorRef -import akka.util.Timeout import akka.http.scaladsl.model.RemoteAddress - -import scala.concurrent.{Future, ExecutionContext} -import scala.concurrent.duration.DurationInt +import akka.pattern.ask +import akka.util.Timeout +import api._ +import balancer._ import com.olegych.scastie.storage.PolicyAcceptance class RestApiServer( - dispatchActor: ActorRef, - ip: RemoteAddress, - maybeUser: Option[User] -)(implicit executionContext: ExecutionContext) - extends RestApi { + dispatchActor: ActorRef, + ip: RemoteAddress, + maybeUser: Option[User] +)( + implicit executionContext: ExecutionContext +) extends RestApi { implicit val timeout: Timeout = Timeout(20.seconds) - private def wrap(inputs: Inputs): InputsWithIpAndUser = - InputsWithIpAndUser(inputs, UserTrace(ip.toString, maybeUser)) + private def wrap(inputs: Inputs): InputsWithIpAndUser = InputsWithIpAndUser(inputs, UserTrace(ip.toString, maybeUser)) def run(inputs: Inputs): Future[SnippetId] = { dispatchActor @@ -90,8 +89,7 @@ class RestApiServer( def fetchUserSnippets(): Future[List[SnippetSummary]] = { maybeUser match { - case Some(user) => - dispatchActor + case Some(user) => dispatchActor .ask(FetchUserSnippets(user)) .mapTo[List[SnippetSummary]] case _ => Future.successful(Nil) @@ -101,8 +99,7 @@ class RestApiServer( @deprecated("Scheduled for removal", "2023-04-30") def getPrivacyPolicy(): Future[Boolean] = { maybeUser match { - case Some(user) => - dispatchActor + case Some(user) => dispatchActor .ask(GetPrivacyPolicy(user)) .mapTo[Boolean] case _ => Future.successful(true) @@ -112,8 +109,7 @@ class RestApiServer( @deprecated("Scheduled for removal", "2023-04-30") def acceptPrivacyPolicy(): Future[Boolean] = { maybeUser match { - case Some(user) => - dispatchActor + case Some(user) => dispatchActor .ask(SetPrivacyPolicy(user, true)) .mapTo[Boolean] case _ => Future.successful(true) @@ -123,8 +119,7 @@ class RestApiServer( @deprecated("Scheduled for removal", "2023-04-30") def removeUserFromPolicyStatus(): Future[Boolean] = { maybeUser match { - case Some(user) => - dispatchActor + case Some(user) => dispatchActor .ask(RemovePrivacyPolicy(user)) .mapTo[Boolean] case _ => Future.successful(true) @@ -134,11 +129,11 @@ class RestApiServer( @deprecated("Scheduled for removal", "2023-04-30") def removeAllUserSnippets(): Future[Boolean] = { maybeUser match { - case Some(user) => - dispatchActor + case Some(user) => dispatchActor .ask(RemoveAllUserSnippets(user)) .mapTo[Boolean] case _ => Future.successful(true) } } + } diff --git a/server/src/main/scala/com.olegych.scastie.web/ServerMain.scala b/server/src/main/scala/com.olegych.scastie.web/ServerMain.scala index d13b7c273..3b15085e3 100644 --- a/server/src/main/scala/com.olegych.scastie.web/ServerMain.scala +++ b/server/src/main/scala/com.olegych.scastie.web/ServerMain.scala @@ -1,5 +1,10 @@ package com.olegych.scastie.web +import scala.concurrent.duration._ +import scala.concurrent.Await +import scala.util.Failure +import scala.util.Success + import akka.actor.ActorSystem import akka.actor.Props import akka.http.scaladsl._ @@ -11,12 +16,6 @@ import com.olegych.scastie.web.oauth2._ import com.olegych.scastie.web.routes._ import com.typesafe.config.ConfigFactory import com.typesafe.scalalogging.Logger - -import scala.concurrent.Await -import scala.concurrent.duration._ -import scala.util.Failure -import scala.util.Success - import server.Directives._ object ServerMain { @@ -77,8 +76,7 @@ object ServerMain { val scalaLangRoutes = new ScalaLangRoutes(dispatchActor, userDirectives).routes val frontPageRoutes = new FrontPageRoutes(dispatchActor, production, hostname).routes - val routes = - oauthRoutes ~ + val routes = oauthRoutes ~ cors() { pathPrefix("api") { apiRoutes ~ diff --git a/server/src/main/scala/com.olegych.scastie.web/oauth2/Github.scala b/server/src/main/scala/com.olegych.scastie.web/oauth2/Github.scala index 6c438b26e..fa5d45a04 100644 --- a/server/src/main/scala/com.olegych.scastie.web/oauth2/Github.scala +++ b/server/src/main/scala/com.olegych.scastie.web/oauth2/Github.scala @@ -1,33 +1,35 @@ package com.olegych.scastie.web.oauth2 +import scala.concurrent.Future + import akka.actor.ActorSystem import akka.http.scaladsl._ -import akka.http.scaladsl.model.HttpMethods.POST -import akka.http.scaladsl.model.Uri._ import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.model.HttpMethods.POST +import akka.http.scaladsl.model.Uri._ import akka.http.scaladsl.unmarshalling.Unmarshal import com.olegych.scastie.api.User import com.olegych.scastie.web.PlayJsonSupport import com.typesafe.config.ConfigFactory -import scala.concurrent.Future - case class AccessToken(access_token: String) -class Github(implicit system: ActorSystem) extends PlayJsonSupport { +class Github( + implicit system: ActorSystem +) extends PlayJsonSupport { import play.api.libs.json._ import system.dispatcher - implicit val formatUser: OFormat[User] = Json.format[User] + implicit val formatUser: OFormat[User] = Json.format[User] implicit val readAccessToken: Reads[AccessToken] = Json.reads[AccessToken] - private val config = - ConfigFactory.load().getConfig("com.olegych.scastie.web.oauth2") - val clientId: String = config.getString("client-id") + private val config = ConfigFactory.load().getConfig("com.olegych.scastie.web.oauth2") + val clientId: String = config.getString("client-id") private val clientSecret = config.getString("client-secret") - private val redirectUri = config.getString("uri") + "/callback" + private val redirectUri = config.getString("uri") + "/callback" def getUserWithToken(token: String): Future[User] = info(token) + def getUserWithOauth2(code: String): Future[User] = { def access = { Http() @@ -36,18 +38,16 @@ class Github(implicit system: ActorSystem) extends PlayJsonSupport { method = POST, uri = Uri("https://github.com/login/oauth/access_token").withQuery( Query( - "client_id" -> clientId, + "client_id" -> clientId, "client_secret" -> clientSecret, - "code" -> code, - "redirect_uri" -> redirectUri + "code" -> code, + "redirect_uri" -> redirectUri ) ), headers = List(Accept(MediaTypes.`application/json`)) ) ) - .flatMap( - response => Unmarshal(response).to[AccessToken].map(_.access_token) - ) + .flatMap(response => Unmarshal(response).to[AccessToken].map(_.access_token)) } access.flatMap(info) @@ -65,4 +65,5 @@ class Github(implicit system: ActorSystem) extends PlayJsonSupport { .singleRequest(fetchGithub(Path.Empty / "user")) .flatMap(response => Unmarshal(response).to[User]) } + } diff --git a/server/src/main/scala/com.olegych.scastie.web/oauth2/GithubUserSession.scala b/server/src/main/scala/com.olegych.scastie.web/oauth2/GithubUserSession.scala index 1e76612d5..6ed2e6c8a 100644 --- a/server/src/main/scala/com.olegych.scastie.web/oauth2/GithubUserSession.scala +++ b/server/src/main/scala/com.olegych.scastie.web/oauth2/GithubUserSession.scala @@ -3,6 +3,10 @@ package com.olegych.scastie.web.oauth2 import java.lang.System.{lineSeparator => nl} import java.nio.file._ import java.util.UUID +import scala.collection.concurrent.TrieMap +import scala.jdk.CollectionConverters._ +import scala.util.control.NonFatal +import scala.util.Try import akka.actor.ActorSystem import com.olegych.scastie.api.User @@ -11,23 +15,14 @@ import com.typesafe.config.ConfigFactory import com.typesafe.scalalogging.Logger import play.api.libs.json.Json -import scala.collection.concurrent.TrieMap -import scala.jdk.CollectionConverters._ -import scala.util.Try -import scala.util.control.NonFatal - class GithubUserSession(system: ActorSystem) { val logger = Logger("GithubUserSession") - private val configuration = - ConfigFactory.load().getConfig("com.olegych.scastie.web") - private val usersFile = - Paths.get(configuration.getString("oauth2.users-file")) - private val usersSessions = - Paths.get(configuration.getString("oauth2.sessions-file")) + private val configuration = ConfigFactory.load().getConfig("com.olegych.scastie.web") + private val usersFile = Paths.get(configuration.getString("oauth2.users-file")) + private val usersSessions = Paths.get(configuration.getString("oauth2.sessions-file")) - private val sessionConfig = - SessionConfig.default(configuration.getString("session-secret")) + private val sessionConfig = SessionConfig.default(configuration.getString("session-secret")) private lazy val users = { val trie = TrieMap[UUID, User]() @@ -35,12 +30,12 @@ class GithubUserSession(system: ActorSystem) { trie } - implicit def serializer: SessionSerializer[UUID, String] = - new SingleValueSessionSerializer( - _.toString(), - (id: String) => Try { UUID.fromString(id) } - ) - implicit val sessionManager = new SessionManager[UUID](sessionConfig) + implicit def serializer: SessionSerializer[UUID, String] = new SingleValueSessionSerializer( + _.toString(), + (id: String) => Try { UUID.fromString(id) } + ) + + implicit val sessionManager = new SessionManager[UUID](sessionConfig) implicit val refreshTokenStorage = new ActorRefreshTokenStorage(system) private def readSessionsFile(): Vector[(UUID, User)] = { @@ -64,7 +59,7 @@ class GithubUserSession(system: ActorSystem) { private def appendSessionsFile(uuid: UUID, user: User): Unit = synchronized { val pair = uuid -> user users += pair - val sessions = readSessionsFile() + val sessions = readSessionsFile() val sessions0 = sessions :+ pair if (Files.exists(usersSessions)) { @@ -98,6 +93,5 @@ class GithubUserSession(system: ActorSystem) { } } - def getUser(id: Option[UUID]): Option[User] = - id.flatMap(users.get) + def getUser(id: Option[UUID]): Option[User] = id.flatMap(users.get) } diff --git a/server/src/main/scala/com.olegych.scastie.web/oauth2/InMemoryRefreshTokenStorage.scala b/server/src/main/scala/com.olegych.scastie.web/oauth2/InMemoryRefreshTokenStorage.scala index 18212e147..6fafaf440 100644 --- a/server/src/main/scala/com.olegych.scastie.web/oauth2/InMemoryRefreshTokenStorage.scala +++ b/server/src/main/scala/com.olegych.scastie.web/oauth2/InMemoryRefreshTokenStorage.scala @@ -1,22 +1,21 @@ package com.olegych.scastie.web.oauth2 -import com.softwaremill.session.{RefreshTokenData, RefreshTokenStorage, RefreshTokenLookupResult} +import java.util.UUID +import scala.collection.mutable +import scala.concurrent.duration._ +import scala.concurrent.Future + import akka.actor.{Actor, ActorSystem, Props} import akka.pattern.ask import akka.util.Timeout - -import scala.concurrent.Future -import scala.concurrent.duration._ -import scala.collection.mutable - -import java.util.UUID +import com.softwaremill.session.{RefreshTokenData, RefreshTokenLookupResult, RefreshTokenStorage} private[oauth2] case class SessionStorage(session: UUID, tokenHash: String, expires: Long) class ActorRefreshTokenStorage(system: ActorSystem) extends RefreshTokenStorage[UUID] { import system.dispatcher implicit private val timeout = Timeout(10.seconds) - private val impl = system.actorOf(Props(new ActorRefreshTokenStorageImpl())) + private val impl = system.actorOf(Props(new ActorRefreshTokenStorageImpl())) def lookup(selector: String): Future[Option[RefreshTokenLookupResult[UUID]]] = (impl ? Lookup(selector)).mapTo[Option[RefreshTokenLookupResult[UUID]]] @@ -25,16 +24,19 @@ class ActorRefreshTokenStorage(system: ActorSystem) extends RefreshTokenStorage[ impl ! Store(data) Future.successful(()) } + def remove(selector: String): Future[Unit] = { impl ! Remove(selector) Future.successful(()) } + def schedule[S](after: Duration)(op: => Future[S]): Unit = { after match { case finite: FiniteDuration => system.scheduler.scheduleOnce(finite)(op) case _: Duration.Infinite => () } } + } private[oauth2] case class Lookup(selector: String) @@ -43,18 +45,15 @@ private[oauth2] case class Remove(selector: String) class ActorRefreshTokenStorageImpl() extends Actor { private val storage = mutable.Map[String, SessionStorage]() + override def receive: Receive = { case Lookup(selector) => - val lookupResult = - storage - .get(selector) - .map( - s => RefreshTokenLookupResult(s.tokenHash, s.expires, () => s.session) - ) + val lookupResult = storage + .get(selector) + .map(s => RefreshTokenLookupResult(s.tokenHash, s.expires, () => s.session)) sender() ! lookupResult - case Store(data) => - storage.put(data.selector, SessionStorage(data.forSession, data.tokenHash, data.expires)) - case Remove(selector) => - storage.remove(selector) + case Store(data) => storage.put(data.selector, SessionStorage(data.forSession, data.tokenHash, data.expires)) + case Remove(selector) => storage.remove(selector) } + } diff --git a/server/src/main/scala/com.olegych.scastie.web/oauth2/UserDirectives.scala b/server/src/main/scala/com.olegych.scastie.web/oauth2/UserDirectives.scala index bf7bfe263..5d8748163 100644 --- a/server/src/main/scala/com.olegych.scastie.web/oauth2/UserDirectives.scala +++ b/server/src/main/scala/com.olegych.scastie.web/oauth2/UserDirectives.scala @@ -1,21 +1,20 @@ package com.olegych.scastie.web.oauth2 -import com.olegych.scastie.api.User +import scala.concurrent.ExecutionContext import akka.http.scaladsl._ -import server._ - +import com.olegych.scastie.api.User import com.softwaremill.session._ +import server._ import SessionDirectives._ import SessionOptions._ -import scala.concurrent.ExecutionContext - class UserDirectives( - session: GithubUserSession -)(implicit val executionContext: ExecutionContext) { + session: GithubUserSession +)( + implicit val executionContext: ExecutionContext +) { import session._ - def optionalLogin: Directive1[Option[User]] = - optionalSession(refreshable, usingCookies).map(getUser) + def optionalLogin: Directive1[Option[User]] = optionalSession(refreshable, usingCookies).map(getUser) } diff --git a/server/src/main/scala/com.olegych.scastie.web/routes/ApiRoutes.scala b/server/src/main/scala/com.olegych.scastie.web/routes/ApiRoutes.scala index 4709d7685..8e987b473 100644 --- a/server/src/main/scala/com.olegych.scastie.web/routes/ApiRoutes.scala +++ b/server/src/main/scala/com.olegych.scastie.web/routes/ApiRoutes.scala @@ -2,95 +2,81 @@ package com.olegych.scastie.web.routes import akka.actor.{ActorRef, ActorSystem} import akka.http.scaladsl.coding.Coders.Gzip -import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{Directive1, Route} +import akka.http.scaladsl.server.Directives._ import com.olegych.scastie.api._ import com.olegych.scastie.web._ import com.olegych.scastie.web.oauth2._ class ApiRoutes( - dispatchActor: ActorRef, - userDirectives: UserDirectives -)(implicit system: ActorSystem) - extends PlayJsonSupport { + dispatchActor: ActorRef, + userDirectives: UserDirectives +)( + implicit system: ActorSystem +) extends PlayJsonSupport { import system.dispatcher import userDirectives.optionalLogin - val withRestApiServer: Directive1[RestApiServer] = - (extractClientIP & optionalLogin).tmap { - case (remoteAddress, user) => - new RestApiServer(dispatchActor, remoteAddress, user) - } + val withRestApiServer: Directive1[RestApiServer] = (extractClientIP & optionalLogin).tmap { + case (remoteAddress, user) => new RestApiServer(dispatchActor, remoteAddress, user) + } - val routes: Route = - withRestApiServer( - server => + val routes: Route = withRestApiServer(server => + concat( + post( concat( - post( - concat( - path("run")( - entity(as[Inputs])(inputs => complete(server.run(inputs))) - ), - path("save")( - entity(as[Inputs])(inputs => complete(server.save(inputs))) - ), - path("update")( - entity(as[EditInputs])( - editInputs => complete(server.update(editInputs)) - ) - ), - path("fork")( - entity(as[EditInputs])( - editInputs => complete(server.fork(editInputs)) - ) - ), - path("delete")( - entity(as[SnippetId])( - snippetId => complete(server.delete(snippetId)) - ) - ), - path("format")( - entity(as[FormatRequest])( - request => complete(server.format(request)) - ) - ) - ) + path("run")( + entity(as[Inputs])(inputs => complete(server.run(inputs))) ), - encodeResponseWith(Gzip)( - get( - concat( - snippetIdStart("snippets")( - sid => complete(server.fetch(sid)) - ), - path("old-snippets" / IntNumber)( - id => complete(server.fetchOld(id)) - ), - path("user" / "settings")( - complete(server.fetchUser()) - ), - path("user" / "snippets")( - complete(server.fetchUserSnippets()) - ) - ) - ) + path("save")( + entity(as[Inputs])(inputs => complete(server.save(inputs))) + ), + path("update")( + entity(as[EditInputs])(editInputs => complete(server.update(editInputs))) ), - post( + path("fork")( + entity(as[EditInputs])(editInputs => complete(server.fork(editInputs))) + ), + path("delete")( + entity(as[SnippetId])(snippetId => complete(server.delete(snippetId))) + ), + path("format")( + entity(as[FormatRequest])(request => complete(server.format(request))) + ) + ) + ), + encodeResponseWith(Gzip)( + get( concat( - path("user" / "privacyPolicyStatus")( - complete(server.getPrivacyPolicy()) - ), - path("user" / "acceptPrivacyPolicy")( - complete(server.acceptPrivacyPolicy()) - ), - path("user" / "removeUserFromPolicyStatus")( - complete(server.removeUserFromPolicyStatus()) - ), - path("user" / "removeAllUserSnippets")( - complete(server.removeAllUserSnippets()) + snippetIdStart("snippets")(sid => complete(server.fetch(sid))), + path("old-snippets" / IntNumber)(id => complete(server.fetchOld(id))), + path("user" / "settings")( + complete(server.fetchUser()) ), + path("user" / "snippets")( + complete(server.fetchUserSnippets()) + ) + ) + ) + ), + post( + concat( + path("user" / "privacyPolicyStatus")( + complete(server.getPrivacyPolicy()) + ), + path("user" / "acceptPrivacyPolicy")( + complete(server.acceptPrivacyPolicy()) + ), + path("user" / "removeUserFromPolicyStatus")( + complete(server.removeUserFromPolicyStatus()) + ), + path("user" / "removeAllUserSnippets")( + complete(server.removeAllUserSnippets()) ) ) ) ) + ) + } diff --git a/server/src/main/scala/com.olegych.scastie.web/routes/DownloadRoutes.scala b/server/src/main/scala/com.olegych.scastie.web/routes/DownloadRoutes.scala index e36216679..08bedcae0 100644 --- a/server/src/main/scala/com.olegych.scastie.web/routes/DownloadRoutes.scala +++ b/server/src/main/scala/com.olegych.scastie.web/routes/DownloadRoutes.scala @@ -1,33 +1,27 @@ package com.olegych.scastie.web.routes -import com.olegych.scastie.balancer.DownloadSnippet +import java.nio.file.Path +import scala.concurrent.duration.DurationInt +import akka.actor.ActorRef import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route - -import akka.actor.ActorRef import akka.pattern.ask - -import java.nio.file.Path - import akka.util.Timeout -import scala.concurrent.duration.DurationInt +import com.olegych.scastie.balancer.DownloadSnippet class DownloadRoutes(dispatchActor: ActorRef) { implicit val timeout = Timeout(5.seconds) - val routes: Route = - get { - snippetIdStart("download")( - sid => - onSuccess((dispatchActor ? DownloadSnippet(sid)).mapTo[Option[Path]]) { - case Some(path) => - getFromFile(path.toFile) - case None => - throw new Exception( - s"Can't serve project ${sid.base64UUID} to user ${sid.user.getOrElse("anon")}" - ) - } - ) - } + val routes: Route = get { + snippetIdStart("download")(sid => + onSuccess((dispatchActor ? DownloadSnippet(sid)).mapTo[Option[Path]]) { + case Some(path) => getFromFile(path.toFile) + case None => throw new Exception( + s"Can't serve project ${sid.base64UUID} to user ${sid.user.getOrElse("anon")}" + ) + } + ) + } + } diff --git a/server/src/main/scala/com.olegych.scastie.web/routes/FrontPageRoutes.scala b/server/src/main/scala/com.olegych.scastie.web/routes/FrontPageRoutes.scala index 9dc69654b..1b56736ff 100644 --- a/server/src/main/scala/com.olegych.scastie.web/routes/FrontPageRoutes.scala +++ b/server/src/main/scala/com.olegych.scastie.web/routes/FrontPageRoutes.scala @@ -1,17 +1,21 @@ package com.olegych.scastie.web.routes +import scala.concurrent.duration.DurationInt +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + import akka.actor.ActorRef import akka.http.scaladsl.coding.Coders.Gzip import akka.http.scaladsl.coding.Coders.NoCoding -import akka.http.scaladsl.model.HttpEntity import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers.{`Cache-Control`, CacheDirectives} +import akka.http.scaladsl.model.HttpEntity import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.RouteResult import akka.pattern.ask -import akka.stream.Materializer import akka.stream.scaladsl.StreamConverters +import akka.stream.Materializer import akka.util.ByteString import akka.util.Timeout import com.olegych.scastie.api.FetchResult @@ -21,27 +25,29 @@ import com.olegych.scastie.balancer.FetchSnippet import com.olegych.scastie.util.Base64UUID import org.apache.commons.text.StringEscapeUtils -import scala.concurrent.ExecutionContext -import scala.concurrent.Future -import scala.concurrent.duration.DurationInt - -class FrontPageRoutes(dispatchActor: ActorRef, production: Boolean, hostname: String)(implicit ec: ExecutionContext, mat: Materializer) { +class FrontPageRoutes(dispatchActor: ActorRef, production: Boolean, hostname: String)( + implicit ec: ExecutionContext, + mat: Materializer +) { implicit val timeout: Timeout = Timeout(20.seconds) + private val placeholders = List( - "Scastie can run any Scala program with any library in your browser. You don’t need to download or install anything.", + "Scastie can run any Scala program with any library in your browser. You don’t need to download or install anything." ) private val indexResource = "public/index.html" - private val indexResourceContent = Future.traverse(Option(getClass.getClassLoader.getResource(indexResource)).toList) { url => - StreamConverters.fromInputStream(() => url.openStream()).runFold("")(_ + _.utf8String) - } + + private val indexResourceContent = + Future.traverse(Option(getClass.getClassLoader.getResource(indexResource)).toList) { url => + StreamConverters.fromInputStream(() => url.openStream()).runFold("")(_ + _.utf8String) + } + private val index = getFromResource(indexResource) private def embeddedResource(snippetId: SnippetId, theme: Option[String]): String = { val user = snippetId.user match { - case Some(SnippetUserPart(login, update)) => - s"user: '$login', update: $update," - case None => "" + case Some(SnippetUserPart(login, update)) => s"user: '$login', update: $update," + case None => "" } val themePart = theme match { @@ -78,26 +84,33 @@ class FrontPageRoutes(dispatchActor: ActorRef, production: Boolean, hostname: St respondWithHeader(`Cache-Control`(CacheDirectives.`no-cache`))( concat( path("embedded.js")( - getFromResource("public/embedded/embedded.js", ContentType(MediaTypes.`application/javascript`, HttpCharsets.`UTF-8`)) + getFromResource( + "public/embedded/embedded.js", + ContentType(MediaTypes.`application/javascript`, HttpCharsets.`UTF-8`) + ) ), path("public" / "embedded.css")( getFromResource("public/embedded/style.css", ContentType(MediaTypes.`text/css`, HttpCharsets.`UTF-8`)) ), path("public" / "tree-sitter.wasm")( - getFromResource("public/tree-sitter.wasm", ContentType(MediaType.applicationBinary("wasm", MediaType.Compressible, "wasm"))) + getFromResource( + "public/tree-sitter.wasm", + ContentType(MediaType.applicationBinary("wasm", MediaType.Compressible, "wasm")) + ) ), path("public" / "tree-sitter-scala.wasm")( - getFromResource("public/tree-sitter-scala.wasm", ContentType(MediaType.applicationBinary("wasm", MediaType.Compressible, "wasm"))) + getFromResource( + "public/tree-sitter-scala.wasm", + ContentType(MediaType.applicationBinary("wasm", MediaType.Compressible, "wasm")) + ) ), path("public" / "highlights.scm")( getFromResource("public/highlights.scm", ContentType(MediaTypes.`text/css`, HttpCharsets.`UTF-8`)) - ), + ) ) ), respondWithHeader(`Cache-Control`(CacheDirectives.immutableDirective))( - path("public" / Remaining)( - path => getFromResource("public/" + path) - ), + path("public" / Remaining)(path => getFromResource("public/" + path)) ), pathSingleSlash(index), snippetId { snippetId => ctx => @@ -105,30 +118,35 @@ class FrontPageRoutes(dispatchActor: ActorRef, production: Boolean, hostname: St s <- dispatchActor.ask(FetchSnippet(snippetId)).mapTo[Option[FetchResult]] c <- indexResourceContent r <- index(ctx) - } yield - (r, c, s) match { - case (r: RouteResult.Complete, List(c), Some(s)) if r.response.status.intValue() == 200 => - val code = StringEscapeUtils.escapeHtml4(s.inputs.code) - r.copy( - response = r.response.withEntity( - HttpEntity.Strict( - r.response.entity.contentType, - ByteString.fromString(placeholders.foldLeft(c)(_.replace(_, code))), - ) + } yield (r, c, s) match { + case (r: RouteResult.Complete, List(c), Some(s)) if r.response.status.intValue() == 200 => + val code = StringEscapeUtils.escapeHtml4(s.inputs.code) + r.copy( + response = r.response.withEntity( + HttpEntity.Strict( + r.response.entity.contentType, + ByteString.fromString(placeholders.foldLeft(c)(_.replace(_, code))) ) ) - case _ => r - } + ) + case _ => r + } }, parameter("theme".?) { theme => snippetIdExtension(".js") { sid => complete { - HttpResponse(entity = HttpEntity(ContentType(MediaTypes.`application/javascript`, HttpCharsets.`UTF-8`), embeddedResource(sid, theme))) + HttpResponse(entity = + HttpEntity( + ContentType(MediaTypes.`application/javascript`, HttpCharsets.`UTF-8`), + embeddedResource(sid, theme) + ) + ) } } }, - index, + index ) ) ) + } diff --git a/server/src/main/scala/com.olegych.scastie.web/routes/OAuth2Routes.scala b/server/src/main/scala/com.olegych.scastie.web/routes/OAuth2Routes.scala index 755c91c75..f8dda92c9 100644 --- a/server/src/main/scala/com.olegych.scastie.web/routes/OAuth2Routes.scala +++ b/server/src/main/scala/com.olegych.scastie.web/routes/OAuth2Routes.scala @@ -2,85 +2,82 @@ package com.olegych.scastie package web package routes -import oauth2._ - -import com.softwaremill.session.SessionDirectives._ -import com.softwaremill.session.SessionOptions._ -import com.softwaremill.session.CsrfDirectives._ -import com.softwaremill.session.CsrfOptions._ +import scala.concurrent.ExecutionContext import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.Uri.Query -import akka.http.scaladsl.model.StatusCodes.TemporaryRedirect import akka.http.scaladsl.model.headers.Referer +import akka.http.scaladsl.model.StatusCodes.TemporaryRedirect +import akka.http.scaladsl.model.Uri.Query import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route - -import scala.concurrent.ExecutionContext +import com.softwaremill.session.CsrfDirectives._ +import com.softwaremill.session.CsrfOptions._ +import com.softwaremill.session.SessionDirectives._ +import com.softwaremill.session.SessionOptions._ +import oauth2._ class OAuth2Routes(github: Github, session: GithubUserSession)( - implicit val executionContext: ExecutionContext + implicit val executionContext: ExecutionContext ) { import session._ - val routes: Route = - get( - concat( - path("login") { - parameter("home".?)( - home => - optionalHeaderValueByType[Referer](()) { referrer => - redirect( - Uri("https://github.com/login/oauth/authorize").withQuery( - Query( - "client_id" -> github.clientId, - "state" -> { - val homeUri = "/" - if (home.isDefined) homeUri - else referrer.map(_.value).getOrElse(homeUri) - } - ) - ), - TemporaryRedirect + val routes: Route = get( + concat( + path("login") { + parameter("home".?)(home => + optionalHeaderValueByType[Referer](()) { referrer => + redirect( + Uri("https://github.com/login/oauth/authorize").withQuery( + Query( + "client_id" -> github.clientId, + "state" -> { + val homeUri = "/" + if (home.isDefined) homeUri + else referrer.map(_.value).getOrElse(homeUri) + } ) - } - ) - }, - path("logout") { - headerValueByType[Referer](()) { referrer => - requiredSession(refreshable, usingCookies) { _ => - invalidateSession(refreshable, usingCookies) { ctx => - ctx.complete( - HttpResponse( - status = TemporaryRedirect, - headers = headers.Location(Uri(referrer.value)) :: Nil, - entity = HttpEntity.Empty - ) + ), + TemporaryRedirect + ) + } + ) + }, + path("logout") { + headerValueByType[Referer](()) { referrer => + requiredSession(refreshable, usingCookies) { _ => + invalidateSession(refreshable, usingCookies) { ctx => + ctx.complete( + HttpResponse( + status = TemporaryRedirect, + headers = headers.Location(Uri(referrer.value)) :: Nil, + entity = HttpEntity.Empty ) - } + ) } } - }, - pathPrefix("callback") { - pathEnd { - parameters("code", "state".?) { (code, state) => - onSuccess(github.getUserWithOauth2(code)) { user => - setSession(refreshable, usingCookies, session.addUser(user)) { - setNewCsrfToken(checkHeader) { ctx => - ctx.complete( - HttpResponse( - status = TemporaryRedirect, - headers = headers - .Location(Uri(state.getOrElse("/"))) :: Nil, - entity = HttpEntity.Empty - ) + } + }, + pathPrefix("callback") { + pathEnd { + parameters("code", "state".?) { (code, state) => + onSuccess(github.getUserWithOauth2(code)) { user => + setSession(refreshable, usingCookies, session.addUser(user)) { + setNewCsrfToken(checkHeader) { ctx => + ctx.complete( + HttpResponse( + status = TemporaryRedirect, + headers = headers + .Location(Uri(state.getOrElse("/"))) :: Nil, + entity = HttpEntity.Empty ) - } + ) } } } } } - ) + } ) + ) + } diff --git a/server/src/main/scala/com.olegych.scastie.web/routes/ProgressRoutes.scala b/server/src/main/scala/com.olegych.scastie.web/routes/ProgressRoutes.scala index 742dbf75e..c0096fbde 100644 --- a/server/src/main/scala/com.olegych.scastie.web/routes/ProgressRoutes.scala +++ b/server/src/main/scala/com.olegych.scastie.web/routes/ProgressRoutes.scala @@ -1,6 +1,8 @@ package com.olegych.scastie.web.routes -import akka.NotUsed +import scala.concurrent.duration.DurationInt +import scala.concurrent.Future + import akka.actor.ActorRef import akka.http.scaladsl.coding.Coders.Gzip import akka.http.scaladsl.marshalling.sse.EventStreamMarshalling._ @@ -11,14 +13,13 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import akka.pattern.ask import akka.stream.scaladsl._ +import akka.NotUsed import com.olegych.scastie.api._ import com.olegych.scastie.balancer._ import play.api.libs.json.Json -import scala.concurrent.Future -import scala.concurrent.duration.DurationInt - class ProgressRoutes(progressActor: ActorRef) { + val routes: Route = encodeResponseWith(Gzip)( concat( snippetIdStart("progress-sse") { sid => @@ -28,14 +29,12 @@ class ProgressRoutes(progressActor: ActorRef) { } } }, - snippetIdStart("progress-ws")( - sid => handleWebSocketMessages(webSocket(sid)) - ) + snippetIdStart("progress-ws")(sid => handleWebSocketMessages(webSocket(sid))) ) ) private def progressSource( - snippetId: SnippetId + snippetId: SnippetId ): Source[SnippetProgress, NotUsed] = { Source .fromFuture((progressActor ? SubscribeProgress(snippetId))(1.second).mapTo[Source[SnippetProgress, NotUsed]]) @@ -44,7 +43,7 @@ class ProgressRoutes(progressActor: ActorRef) { private def webSocket(snippetId: SnippetId): Flow[ws.Message, ws.Message, _] = { def flow: Flow[String, SnippetProgress, NotUsed] = { - val in = Flow[String].to(Sink.ignore) + val in = Flow[String].to(Sink.ignore) val out = progressSource(snippetId) Flow.fromSinkAndSource(in, out) } @@ -55,8 +54,7 @@ class ProgressRoutes(progressActor: ActorRef) { case e => Future.failed(new Exception(e.toString)) } .via(flow) - .map( - progress => ws.TextMessage.Strict(Json.stringify(Json.toJson(progress))) - ) + .map(progress => ws.TextMessage.Strict(Json.stringify(Json.toJson(progress)))) } + } diff --git a/server/src/main/scala/com.olegych.scastie.web/routes/ScalaJsRoutes.scala b/server/src/main/scala/com.olegych.scastie.web/routes/ScalaJsRoutes.scala index b69f5da16..949ee084b 100644 --- a/server/src/main/scala/com.olegych.scastie.web/routes/ScalaJsRoutes.scala +++ b/server/src/main/scala/com.olegych.scastie.web/routes/ScalaJsRoutes.scala @@ -1,50 +1,47 @@ package com.olegych.scastie.web.routes -import com.olegych.scastie.api._ - -import akka.util.Timeout +import scala.concurrent.duration.DurationInt -import akka.pattern.ask import akka.actor.{ActorRef, ActorSystem} +import akka.http.scaladsl.coding.Coders.Gzip import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route -import akka.http.scaladsl.coding.Coders.Gzip - -import scala.concurrent.duration.DurationInt +import akka.pattern.ask +import akka.util.Timeout +import com.olegych.scastie.api._ //not used anymore -class ScalaJsRoutes(dispatchActor: ActorRef)(implicit system: ActorSystem) { +class ScalaJsRoutes(dispatchActor: ActorRef)( + implicit system: ActorSystem +) { import system.dispatcher implicit val timeout: Timeout = Timeout(1.seconds) - val routes: Route = - encodeResponseWith(Gzip)( - concat( - snippetIdEnd(Shared.scalaJsHttpPathPrefix, ScalaTarget.Js.targetFilename)( - sid => - complete( - (dispatchActor ? FetchScalaJs(sid)) - .mapTo[Option[FetchResultScalaJs]] - .map(_.map(_.content)) - ) - ), - snippetIdEnd(Shared.scalaJsHttpPathPrefix, ScalaTarget.Js.sourceFilename)( - sid => - complete( - (dispatchActor ? FetchScalaSource(sid)) - .mapTo[Option[FetchResultScalaSource]] - .map(_.map(_.content)) - ) - ), - snippetIdEnd(Shared.scalaJsHttpPathPrefix, ScalaTarget.Js.sourceMapFilename)( - sid => - complete( - (dispatchActor ? FetchScalaJsSourceMap(sid)) - .mapTo[Option[FetchResultScalaJsSourceMap]] - .map(_.map(_.content)) - ) + val routes: Route = encodeResponseWith(Gzip)( + concat( + snippetIdEnd(Shared.scalaJsHttpPathPrefix, ScalaTarget.Js.targetFilename)(sid => + complete( + (dispatchActor ? FetchScalaJs(sid)) + .mapTo[Option[FetchResultScalaJs]] + .map(_.map(_.content)) + ) + ), + snippetIdEnd(Shared.scalaJsHttpPathPrefix, ScalaTarget.Js.sourceFilename)(sid => + complete( + (dispatchActor ? FetchScalaSource(sid)) + .mapTo[Option[FetchResultScalaSource]] + .map(_.map(_.content)) + ) + ), + snippetIdEnd(Shared.scalaJsHttpPathPrefix, ScalaTarget.Js.sourceMapFilename)(sid => + complete( + (dispatchActor ? FetchScalaJsSourceMap(sid)) + .mapTo[Option[FetchResultScalaJsSourceMap]] + .map(_.map(_.content)) ) ) ) + ) + } diff --git a/server/src/main/scala/com.olegych.scastie.web/routes/ScalaLangRoutes.scala b/server/src/main/scala/com.olegych.scastie.web/routes/ScalaLangRoutes.scala index d5404b392..cc8ad3a6c 100644 --- a/server/src/main/scala/com.olegych.scastie.web/routes/ScalaLangRoutes.scala +++ b/server/src/main/scala/com.olegych.scastie.web/routes/ScalaLangRoutes.scala @@ -1,26 +1,24 @@ package com.olegych.scastie.web.routes -import com.olegych.scastie.api._ -import com.olegych.scastie.web.oauth2._ - -import com.olegych.scastie.balancer._ +import scala.concurrent.duration.DurationInt -import akka.util.Timeout import akka.actor.{ActorRef, ActorSystem} - import akka.http.scaladsl.model.StatusCodes.Created -import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Directives._ - +import akka.http.scaladsl.server.Route import akka.pattern.ask - -import scala.concurrent.duration.DurationInt +import akka.util.Timeout +import com.olegych.scastie.api._ +import com.olegych.scastie.balancer._ +import com.olegych.scastie.web.oauth2._ // temporary route for the scala-lang frontpage class ScalaLangRoutes( - dispatchActor: ActorRef, - userDirectives: UserDirectives -)(implicit system: ActorSystem) { + dispatchActor: ActorRef, + userDirectives: UserDirectives +)( + implicit system: ActorSystem +) { import system.dispatcher import userDirectives.optionalLogin diff --git a/server/src/main/scala/com.olegych.scastie.web/routes/StatusRoutes.scala b/server/src/main/scala/com.olegych.scastie.web/routes/StatusRoutes.scala index d5f18211d..0202fab53 100644 --- a/server/src/main/scala/com.olegych.scastie.web/routes/StatusRoutes.scala +++ b/server/src/main/scala/com.olegych.scastie.web/routes/StatusRoutes.scala @@ -1,73 +1,69 @@ package com.olegych.scastie.web.routes -import akka.NotUsed +import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.duration.DurationInt + import akka.actor.ActorRef import akka.http.scaladsl.marshalling.sse.EventStreamMarshalling._ import akka.http.scaladsl.model._ import akka.http.scaladsl.model.sse.ServerSentEvent import akka.http.scaladsl.model.ws.TextMessage._ -import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{Route, _} +import akka.http.scaladsl.server.Directives._ import akka.pattern.ask import akka.stream.scaladsl._ +import akka.NotUsed import com.olegych.scastie.api._ import com.olegych.scastie.balancer._ import com.olegych.scastie.web.oauth2.UserDirectives import play.api.libs.json.Json -import scala.concurrent.duration.DurationInt -import scala.concurrent.{ExecutionContext, Future} +class StatusRoutes(statusActor: ActorRef, userDirectives: UserDirectives)( + implicit ec: ExecutionContext +) { -class StatusRoutes(statusActor: ActorRef, userDirectives: UserDirectives)(implicit ec: ExecutionContext) { - - val isAdminUser: Directive1[Boolean] = - userDirectives.optionalLogin.map( - user => user.exists(_.isAdmin) - ) + val isAdminUser: Directive1[Boolean] = userDirectives.optionalLogin.map(user => user.exists(_.isAdmin)) - val routes: Route = - isAdminUser { isAdmin => - concat( - path("status-sse")( - complete( - statusSource(isAdmin).map { progress => - ServerSentEvent( - Json.stringify(Json.toJson(progress)) - ) - } - ) - ), - path("status-ws")( - handleWebSocketMessages(webSocketProgress(isAdmin)) + val routes: Route = isAdminUser { isAdmin => + concat( + path("status-sse")( + complete( + statusSource(isAdmin).map { progress => + ServerSentEvent( + Json.stringify(Json.toJson(progress)) + ) + } ) + ), + path("status-ws")( + handleWebSocketMessages(webSocketProgress(isAdmin)) ) - } + ) + } private def statusSource(isAdmin: Boolean) = { def hideTask(progress: StatusProgress): StatusProgress = if (isAdmin) progress - else - progress match { - case StatusProgress.Sbt(runners) => - // Hide the task Queue for non admin users, - // they will only see the runner count - StatusProgress.Sbt( - runners.map(_.copy(tasks = Vector())) - ) + else progress match { + case StatusProgress.Sbt(runners) => + // Hide the task Queue for non admin users, + // they will only see the runner count + StatusProgress.Sbt( + runners.map(_.copy(tasks = Vector())) + ) - case _ => - progress - } + case _ => progress + } Source .fromFuture((statusActor ? SubscribeStatus)(2.seconds).mapTo[Source[StatusProgress, NotUsed]]) .flatMapConcat(s => s.map(hideTask)) } private def webSocketProgress( - isAdmin: Boolean + isAdmin: Boolean ): Flow[ws.Message, ws.Message, _] = { def flow: Flow[String, StatusProgress, NotUsed] = { - val in = Flow[String].to(Sink.ignore) + val in = Flow[String].to(Sink.ignore) val out = statusSource(isAdmin) Flow.fromSinkAndSource(in, out) } @@ -78,8 +74,7 @@ class StatusRoutes(statusActor: ActorRef, userDirectives: UserDirectives)(implic case e => Future.failed(new Exception(e.toString)) } .via(flow) - .map( - progress => ws.TextMessage.Strict(Json.stringify(Json.toJson(progress))) - ) + .map(progress => ws.TextMessage.Strict(Json.stringify(Json.toJson(progress)))) } + } diff --git a/server/src/main/scala/com.olegych.scastie.web/routes/package.scala b/server/src/main/scala/com.olegych.scastie.web/routes/package.scala index d1885ae8b..595e70a0f 100644 --- a/server/src/main/scala/com.olegych.scastie.web/routes/package.scala +++ b/server/src/main/scala/com.olegych.scastie.web/routes/package.scala @@ -1,49 +1,46 @@ package com.olegych.scastie.web -import com.olegych.scastie.api._ - -import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{PathMatcher, Route} +import akka.http.scaladsl.server.Directives._ +import com.olegych.scastie.api._ package object routes { - def snippetIdStart(matcherStart: String)(f: SnippetId => Route): Route = - snippetIdBase( - matcherStart / _, - matcherStart / _ - )(f) - - def snippetId(f: SnippetId => Route): Route = - snippetIdBase( - p => p, - p => p - )(f) - - def snippetIdEnd(matcherStart: String, matcherEnd: String)(f: SnippetId => Route): Route = - snippetIdBase( - matcherStart / _ / matcherEnd, - matcherStart / _ / matcherEnd - )(f) - - def snippetIdExtension(extension: String)(f: SnippetId => Route): Route = - snippetIdBase( - _ ~ extension, - _ ~ extension - )(f) + + def snippetIdStart(matcherStart: String)(f: SnippetId => Route): Route = snippetIdBase( + matcherStart / _, + matcherStart / _ + )(f) + + def snippetId(f: SnippetId => Route): Route = snippetIdBase( + p => p, + p => p + )(f) + + def snippetIdEnd(matcherStart: String, matcherEnd: String)(f: SnippetId => Route): Route = snippetIdBase( + matcherStart / _ / matcherEnd, + matcherStart / _ / matcherEnd + )(f) + + def snippetIdExtension(extension: String)(f: SnippetId => Route): Route = snippetIdBase( + _ ~ extension, + _ ~ extension + )(f) private val uuidMatcher = PathMatcher("[A-Za-z0-9]{22}".r) private def snippetIdBase( - fp1: PathMatcher[Tuple1[String]] => PathMatcher[Tuple1[String]], - fp2: PathMatcher[(String, String, Option[Int])] => PathMatcher[ - (String, String, Option[Int]) - ] + fp1: PathMatcher[Tuple1[String]] => PathMatcher[Tuple1[String]], + fp2: PathMatcher[(String, String, Option[Int])] => PathMatcher[ + (String, String, Option[Int]) + ] )(f: SnippetId => Route): Route = { concat( path(fp1(uuidMatcher) ~ Slash.?)(uuid => f(SnippetId(uuid, None))), - path(fp2(Segment / uuidMatcher ~ (Slash ~ IntNumber).?) ~ Slash.?)( - (user, uuid, update) => f(SnippetId(uuid, Some(SnippetUserPart(user, update.getOrElse(0))))) + path(fp2(Segment / uuidMatcher ~ (Slash ~ IntNumber).?) ~ Slash.?)((user, uuid, update) => + f(SnippetId(uuid, Some(SnippetUserPart(user, update.getOrElse(0))))) ) ) } + } diff --git a/server/src/test/scala/com.olegych.scastie.web.routes/SnippetIdMatcherTests.scala b/server/src/test/scala/com.olegych.scastie.web.routes/SnippetIdMatcherTests.scala index b68c4264a..d9c248cac 100644 --- a/server/src/test/scala/com.olegych.scastie.web.routes/SnippetIdMatcherTests.scala +++ b/server/src/test/scala/com.olegych.scastie.web.routes/SnippetIdMatcherTests.scala @@ -12,11 +12,10 @@ class SnippetIdMatcherTests extends AnyFunSuite with ScalatestRouteTest { def testRoute(snippetIdRoute: Route, f1: String => String, f2: String => String, checkEnd: Boolean = true): Unit = { - val expectedBase = - SnippetId( - "GIbgJuUFSKaVzLDGK4kxdw", - None - ) + val expectedBase = SnippetId( + "GIbgJuUFSKaVzLDGK4kxdw", + None + ) println(f1("/GIbgJuUFSKaVzLDGK4kxdw")) Get(f1("/GIbgJuUFSKaVzLDGK4kxdw")) ~> snippetIdRoute ~> check { @@ -31,11 +30,10 @@ class SnippetIdMatcherTests extends AnyFunSuite with ScalatestRouteTest { } } - val expectedUser = - SnippetId( - "GIbgJuUFSKaVzLDGK4kxdw", - Some(SnippetUserPart("MasseGuillaume", 0)) - ) + val expectedUser = SnippetId( + "GIbgJuUFSKaVzLDGK4kxdw", + Some(SnippetUserPart("MasseGuillaume", 0)) + ) Get(f1("/MasseGuillaume/GIbgJuUFSKaVzLDGK4kxdw")) ~> snippetIdRoute ~> check { val obtained = responseAs[SnippetId] @@ -49,11 +47,10 @@ class SnippetIdMatcherTests extends AnyFunSuite with ScalatestRouteTest { } } - val expectedFull = - SnippetId( - "GIbgJuUFSKaVzLDGK4kxdw", - Some(SnippetUserPart("MasseGuillaume", 2)) - ) + val expectedFull = SnippetId( + "GIbgJuUFSKaVzLDGK4kxdw", + Some(SnippetUserPart("MasseGuillaume", 2)) + ) Get(f1("/MasseGuillaume/GIbgJuUFSKaVzLDGK4kxdw/2")) ~> snippetIdRoute ~> check { val obtained = responseAs[SnippetId] @@ -69,10 +66,9 @@ class SnippetIdMatcherTests extends AnyFunSuite with ScalatestRouteTest { } test("snippetId") { - val snippetIdRoute = - get( - snippetId(sid => complete(sid)) - ) + val snippetIdRoute = get( + snippetId(sid => complete(sid)) + ) testRoute( snippetIdRoute, @@ -84,10 +80,9 @@ class SnippetIdMatcherTests extends AnyFunSuite with ScalatestRouteTest { test("snippetIdStart") { val start = "snippets" - val snippetIdRoute = - get( - snippetIdStart(start)(sid => complete(sid)) - ) + val snippetIdRoute = get( + snippetIdStart(start)(sid => complete(sid)) + ) testRoute( snippetIdRoute, @@ -98,12 +93,11 @@ class SnippetIdMatcherTests extends AnyFunSuite with ScalatestRouteTest { test("snippetIdEnd") { val start = "api" - val end = "foo" + val end = "foo" - val snippetIdRoute = - get( - snippetIdEnd(start, end)(sid => complete(sid)) - ) + val snippetIdRoute = get( + snippetIdEnd(start, end)(sid => complete(sid)) + ) testRoute( snippetIdRoute, @@ -115,10 +109,9 @@ class SnippetIdMatcherTests extends AnyFunSuite with ScalatestRouteTest { test("snippetIdExtension") { val extension = ".js" - val snippetIdRoute = - get( - snippetIdExtension(extension)(sid => complete(sid)) - ) + val snippetIdRoute = get( + snippetIdExtension(extension)(sid => complete(sid)) + ) testRoute( snippetIdRoute, diff --git a/storage/src/main/scala/com.olegych.scastie.storage/OldScastieConverter.scala b/storage/src/main/scala/com.olegych.scastie.storage/OldScastieConverter.scala index 0d99384b1..07ad40914 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/OldScastieConverter.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/OldScastieConverter.scala @@ -3,6 +3,7 @@ package com.olegych.scastie.storage import com.olegych.scastie.api._ object OldScastieConverter { + private def convertLine(line: String): Converter => Converter = { converter => val sv = "scalaVersion := \"" @@ -16,11 +17,9 @@ object OldScastieConverter { case """scalaOrganization in ThisBuild := "org.typelevel"""" => converter.setTargetType(ScalaTargetType.Typelevel) - case "coursier.CoursierPlugin.projectSettings" => - converter + case "coursier.CoursierPlugin.projectSettings" => converter - case _ => - converter.appendSbt(line) + case _ => converter.appendSbt(line) } } } @@ -28,11 +27,10 @@ object OldScastieConverter { def convertOldOutput(content: String): List[SnippetProgress] = { content .split("\n") - .map( - line => - SnippetProgress.default.copy( - userOutput = Some(ProcessOutput(line, ProcessOutputType.StdOut, None)), - isDone = true + .map(line => + SnippetProgress.default.copy( + userOutput = Some(ProcessOutput(line, ProcessOutputType.StdOut, None)), + isDone = true ) ) .toList @@ -40,22 +38,20 @@ object OldScastieConverter { def convertOldInput(content: String): Inputs = { val blockStart = "/***" - val blockEnd = "*/" + val blockEnd = "*/" val blockStartPos = content.indexOf(blockStart) - val blockEndPos = content.indexOf(blockEnd) + val blockEndPos = content.indexOf(blockEnd) if (blockStartPos != -1 && blockEndPos != -1 && blockEndPos > blockStartPos) { val start = blockStartPos + blockStart.length val sbtConfig = content.slice(start, start + blockEndPos - start) - val code = content.drop(blockEndPos + blockEnd.length) + val code = content.drop(blockEndPos + blockEnd.length) - val converterFn = - sbtConfig.split("\n").foldLeft(Converter.nil) { - case (converter, line) => - convertLine(line)(converter) - } + val converterFn = sbtConfig.split("\n").foldLeft(Converter.nil) { case (converter, line) => + convertLine(line)(converter) + } converterFn(Inputs.default).copy(code = code.trim) } else { @@ -64,45 +60,40 @@ object OldScastieConverter { } private object Converter { - def nil: Converter = - Converter( - scalaVersion = None, - targetType = None, - sbtExtra = "" - ) + + def nil: Converter = Converter( + scalaVersion = None, + targetType = None, + sbtExtra = "" + ) + } private case class Converter( - scalaVersion: Option[String], - targetType: Option[ScalaTargetType], - sbtExtra: String + scalaVersion: Option[String], + targetType: Option[ScalaTargetType], + sbtExtra: String ) { - def appendSbt(in: String): Converter = - copy(sbtExtra = sbtExtra + "\n" + in) + def appendSbt(in: String): Converter = copy(sbtExtra = sbtExtra + "\n" + in) - def setTargetType(targetType0: ScalaTargetType): Converter = - copy(targetType = Some(targetType0)) + def setTargetType(targetType0: ScalaTargetType): Converter = copy(targetType = Some(targetType0)) def apply(inputs: Inputs): Inputs = { - val scalaTarget = - targetType match { - case Some(ScalaTargetType.Scala3) => - ScalaTarget.Scala3.default - - case Some(ScalaTargetType.Typelevel) => - scalaVersion - .map(sv => ScalaTarget.Typelevel(sv)) - .getOrElse( - ScalaTarget.Typelevel.default - ) - - case _ => - scalaVersion - .map(sv => ScalaTarget.Jvm(sv)) - .getOrElse( - ScalaTarget.Jvm.default - ) - } + val scalaTarget = targetType match { + case Some(ScalaTargetType.Scala3) => ScalaTarget.Scala3.default + + case Some(ScalaTargetType.Typelevel) => scalaVersion + .map(sv => ScalaTarget.Typelevel(sv)) + .getOrElse( + ScalaTarget.Typelevel.default + ) + + case _ => scalaVersion + .map(sv => ScalaTarget.Jvm(sv)) + .getOrElse( + ScalaTarget.Jvm.default + ) + } inputs.copy( target = scalaTarget, @@ -110,5 +101,7 @@ object OldScastieConverter { _isWorksheetMode = false ) } + } + } diff --git a/storage/src/main/scala/com.olegych.scastie.storage/SnippetsContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/SnippetsContainer.scala index 74a374172..66c552928 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/SnippetsContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/SnippetsContainer.scala @@ -1,29 +1,27 @@ package com.olegych.scastie.storage +import java.nio.file.{Files, Path, Paths} +import scala.concurrent.{ExecutionContext, Future} + import com.olegych.scastie.api._ import com.olegych.scastie.instrumentation.Instrument import com.olegych.scastie.util.Base64UUID - -import net.lingala.zip4j.ZipFile import net.lingala.zip4j.model.ZipParameters - -import java.nio.file.{Files, Path, Paths} -import scala.concurrent.{ExecutionContext, Future} - +import net.lingala.zip4j.ZipFile trait SnippetsContainer { protected implicit val ec: ExecutionContext def appendOutput(progress: SnippetProgress): Future[Unit] + def deleteAll(snippetId: SnippetId): Future[Boolean] = { def deleteUpdate(update: Int): Future[Boolean] = { val updateSnippetId = snippetId.copy(user = snippetId.user.map(_.copy(update = update))) for { read <- readSnippet(updateSnippetId) result <- read match { - case Some(read) => - for { - result <- delete(updateSnippetId) + case Some(read) => for { + result <- delete(updateSnippetId) resultNext <- deleteUpdate(update + 1) } yield result || resultNext case None => Future.successful(false) @@ -32,22 +30,28 @@ trait SnippetsContainer { } deleteUpdate(0) } + protected def delete(snippetId: SnippetId): Future[Boolean] + def removeUserSnippets(user: UserLogin): Future[Boolean] = { listSnippets(user).flatMap(snippets => { - Future.sequence( - snippets - .map(snippet => deleteAll(snippet.snippetId))) - .map(_.fold(true)(_ && _) - ) + Future + .sequence( + snippets + .map(snippet => deleteAll(snippet.snippetId)) + ) + .map(_.fold(true)(_ && _)) }) } + def listSnippets(user: UserLogin): Future[List[SnippetSummary]] def readOldSnippet(id: Int): Future[Option[FetchResult]] def readScalaJs(snippetId: SnippetId): Future[Option[FetchResultScalaJs]] + def readScalaJsSourceMap( - snippetId: SnippetId + snippetId: SnippetId ): Future[Option[FetchResultScalaJsSourceMap]] + def readSnippet(snippetId: SnippetId): Future[Option[FetchResult]] protected def insert(snippetId: SnippetId, inputs: Inputs): Future[Unit] protected def hideFromUserProfile(snippetId: SnippetId): Future[Unit] @@ -64,8 +68,7 @@ trait SnippetsContainer { final def update(snippetId: SnippetId, inputs: Inputs): Future[Option[SnippetId]] = { updateSnippetId(snippetId).flatMap { - case Some(nextSnippetId) => - for { + case Some(nextSnippetId) => for { r <- insert0(nextSnippetId, inputs.copy(forked = Some(snippetId), isShowingInUserProfile = true)) _ <- hideFromUserProfile(snippetId) } yield Some(r) @@ -77,18 +80,15 @@ trait SnippetsContainer { create(inputs.copy(forked = Some(snippetId), isShowingInUserProfile = true), user) final def readScalaSource( - snippetId: SnippetId - ): Future[Option[FetchResultScalaSource]] = - readSnippet(snippetId).map( - _.flatMap( - snippet => - Instrument(snippet.inputs.code, snippet.inputs.target) match { - case Right(instrumented) => - Some(FetchResultScalaSource(instrumented)) - case _ => None - } - ) + snippetId: SnippetId + ): Future[Option[FetchResultScalaSource]] = readSnippet(snippetId).map( + _.flatMap(snippet => + Instrument(snippet.inputs.code, snippet.inputs.target) match { + case Right(instrumented) => Some(FetchResultScalaSource(instrumented)) + case _ => None + } ) + ) final def downloadSnippet(snippetId: SnippetId): Future[Option[Path]] = readSnippet(snippetId).map(_.map(asZip(snippetId))) @@ -129,11 +129,17 @@ trait SnippetsContainer { Files.createDirectories(projectDir) val buildFile = projectDir.resolve("build.sbt") - Files.write(buildFile, inputs.sbtConfig.linesIterator.filterNot(_.contains("org.scastie")).mkString("\n").getBytes()) + Files.write( + buildFile, + inputs.sbtConfig.linesIterator.filterNot(_.contains("org.scastie")).mkString("\n").getBytes() + ) val projectFile = projectDir.resolve("project/plugins.sbt") Files.createDirectories(projectFile.getParent) - Files.write(projectFile, inputs.sbtPluginsConfig.linesIterator.filterNot(_.contains("org.scastie")).mkString("\n").getBytes()) + Files.write( + projectFile, + inputs.sbtPluginsConfig.linesIterator.filterNot(_.contains("org.scastie")).mkString("\n").getBytes() + ) val codeFile = projectDir.resolve(s"src/main/scala/main.${if (inputs.isWorksheetMode) "sc" else "scala"}") Files.createDirectories(codeFile.getParent) diff --git a/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemContainer.scala index a21e8d224..2d03809f0 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemContainer.scala @@ -5,4 +5,5 @@ import scala.concurrent.ExecutionContext class FilesystemContainer(val root: Path, val oldRoot: Path)( implicit val ec: ExecutionContext -) extends FilesystemUsersContainer with FilesystemSnippetsContainer +) extends FilesystemUsersContainer + with FilesystemSnippetsContainer diff --git a/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemSnippetsContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemSnippetsContainer.scala index 4b1965df7..81820b8b9 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemSnippetsContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemSnippetsContainer.scala @@ -1,18 +1,16 @@ package com.olegych.scastie.storage.filesystem +import java.io.IOException +import java.nio.file._ +import scala.concurrent.Future + import com.olegych.scastie.api._ import com.olegych.scastie.storage.OldScastieConverter import com.olegych.scastie.storage.SnippetsContainer import com.olegych.scastie.storage.UserLogin import play.api.libs.json.Json - -import java.io.IOException -import java.nio.file._ -import scala.concurrent.Future - import System.{lineSeparator => nl} - trait FilesystemSnippetsContainer extends SnippetsContainer with GenericFilesystemContainer { val root: Path val oldRoot: Path @@ -25,17 +23,14 @@ trait FilesystemSnippetsContainer extends SnippetsContainer with GenericFilesyst case _ => () } - progress.snippetId.foreach( - sid => append(outputsFile(sid), Json.stringify(Json.toJson(progress)) + nl) - ) + progress.snippetId.foreach(sid => append(outputsFile(sid), Json.stringify(Json.toJson(progress)) + nl)) } def delete(snippetId: SnippetId): Future[Boolean] = { def rootDir(snippetId: SnippetId): Path = { snippetId.user match { - case Some(SnippetUserPart(login, _)) => - root.resolve(login) - case _ => root.resolve(anonFolder) + case Some(SnippetUserPart(login, _)) => root.resolve(login) + case _ => root.resolve(anonFolder) } } @@ -79,7 +74,7 @@ trait FilesystemSnippetsContainer extends SnippetsContainer with GenericFilesyst import java.nio.file.attribute.BasicFileAttributes val filePath = inputsFile(snippetId) - val attr = Files.readAttributes(filePath, classOf[BasicFileAttributes]) + val attr = Files.readAttributes(filePath, classOf[BasicFileAttributes]) attr.creationTime().toMillis } @@ -104,8 +99,7 @@ trait FilesystemSnippetsContainer extends SnippetsContainer with GenericFilesyst updates .flatMap { update => - val snippetId = - SnippetId(uuid, Some(SnippetUserPart(user.login, update))) + val snippetId = SnippetId(uuid, Some(SnippetUserPart(user.login, update))) readInputs(snippetId) match { case Some(inputs) => if (inputs.isShowingInUserProfile) { @@ -129,10 +123,9 @@ trait FilesystemSnippetsContainer extends SnippetsContainer with GenericFilesyst def readOldSnippet(id: Int): Future[Option[FetchResult]] = { - def oldPath(id: Int): Path = - oldRoot - .resolve("paste%20d".format(id).replaceAll(" ", "0")) - .resolve("src/main/scala/") + def oldPath(id: Int): Path = oldRoot + .resolve("paste%20d".format(id).replaceAll(" ", "0")) + .resolve("src/main/scala/") def readOldInputs(id: Int): Option[Inputs] = { slurp(oldPath(id).resolve("test.scala")) @@ -145,47 +138,40 @@ trait FilesystemSnippetsContainer extends SnippetsContainer with GenericFilesyst } Future { - readOldInputs(id).map( - inputs => FetchResult.create(inputs, readOldOutputs(id).getOrElse(Nil)) - ) + readOldInputs(id).map(inputs => FetchResult.create(inputs, readOldOutputs(id).getOrElse(Nil))) } } - def readScalaJs(snippetId: SnippetId): Future[Option[FetchResultScalaJs]] = - Future { - slurp(scalaJsFile(snippetId)).map(content => FetchResultScalaJs(content)) - } + def readScalaJs(snippetId: SnippetId): Future[Option[FetchResultScalaJs]] = Future { + slurp(scalaJsFile(snippetId)).map(content => FetchResultScalaJs(content)) + } def readScalaJsSourceMap( - snippetId: SnippetId + snippetId: SnippetId ): Future[Option[FetchResultScalaJsSourceMap]] = Future { slurp(scalaJsSourceMapFile(snippetId)) .map(content => FetchResultScalaJsSourceMap(content)) } def readSnippet(snippetId: SnippetId): Future[Option[FetchResult]] = Future { - readInputs(snippetId).map( - inputs => FetchResult.create(inputs, readOutputs(snippetId).getOrElse(Nil)) - ) + readInputs(snippetId).map(inputs => FetchResult.create(inputs, readOutputs(snippetId).getOrElse(Nil))) } - protected def insert(snippetId: SnippetId, inputs: Inputs): Future[Unit] = - Future { - write(inputsFile(snippetId), Json.prettyPrint(Json.toJson(inputs.withSavedConfig))) - } + protected def insert(snippetId: SnippetId, inputs: Inputs): Future[Unit] = Future { + write(inputsFile(snippetId), Json.prettyPrint(Json.toJson(inputs.withSavedConfig))) + } - override protected def hideFromUserProfile(snippetId: SnippetId): Future[Unit] = - for { - old <- readSnippet(snippetId) - _ <- Future.traverse(old.toList) { old => - insert(snippetId, old.inputs.copy(isShowingInUserProfile = false)) - } - } yield () + override protected def hideFromUserProfile(snippetId: SnippetId): Future[Unit] = for { + old <- readSnippet(snippetId) + _ <- Future.traverse(old.toList) { old => + insert(snippetId, old.inputs.copy(isShowingInUserProfile = false)) + } + } yield () - private val anonFolder = "_anonymous_" - private val inputFileName = "input3.json" - private val outputFileName = "output3.json" - private val scalaJsFileName = ScalaTarget.Js.targetFilename + private val anonFolder = "_anonymous_" + private val inputFileName = "input3.json" + private val outputFileName = "output3.json" + private val scalaJsFileName = ScalaTarget.Js.targetFilename private val scalaJsSourceMapFileName = ScalaTarget.Js.sourceMapFilename private def inputsFile(snippetId: SnippetId): Path = { @@ -207,43 +193,41 @@ trait FilesystemSnippetsContainer extends SnippetsContainer with GenericFilesyst private def snippetFile(snippetId: SnippetId, fileName: String): Path = { if (!Files.exists(root)) Files.createDirectory(root) - val baseDirectory = - snippetId.user match { - case Some(SnippetUserPart(login, update)) => - val userFolder = root.resolve(login) - if (!Files.exists(userFolder)) Files.createDirectory(userFolder) + val baseDirectory = snippetId.user match { + case Some(SnippetUserPart(login, update)) => + val userFolder = root.resolve(login) + if (!Files.exists(userFolder)) Files.createDirectory(userFolder) - val base = userFolder.resolve(snippetId.base64UUID) - if (!Files.exists(base)) Files.createDirectory(base) + val base = userFolder.resolve(snippetId.base64UUID) + if (!Files.exists(base)) Files.createDirectory(base) - val baseVersion = base.resolve(update.toString) - if (!Files.exists(baseVersion)) Files.createDirectory(baseVersion) + val baseVersion = base.resolve(update.toString) + if (!Files.exists(baseVersion)) Files.createDirectory(baseVersion) - baseVersion - case None => - val anon = root.resolve(anonFolder) - if (!Files.exists(anon)) Files.createDirectory(anon) + baseVersion + case None => + val anon = root.resolve(anonFolder) + if (!Files.exists(anon)) Files.createDirectory(anon) - val base = anon.resolve(snippetId.base64UUID) - if (!Files.exists(base)) Files.createDirectory(base) - base - } + val base = anon.resolve(snippetId.base64UUID) + if (!Files.exists(base)) Files.createDirectory(base) + base + } baseDirectory.resolve(Paths.get(fileName)) } private def readInputs(snippetId: SnippetId): Option[Inputs] = { slurp(inputsFile(snippetId)) - .map( - content => - Json - .fromJson[Inputs](Json.parse(content)) - .fold(e => sys.error(e.toString + s" for ${snippetId} $content"), identity) + .map(content => + Json + .fromJson[Inputs](Json.parse(content)) + .fold(e => sys.error(e.toString + s" for ${snippetId} $content"), identity) ) } private def readOutputs( - snippetId: SnippetId + snippetId: SnippetId ): Option[List[SnippetProgress]] = { slurp(outputsFile(snippetId)).map { _.linesIterator @@ -257,10 +241,9 @@ trait FilesystemSnippetsContainer extends SnippetsContainer with GenericFilesyst } } - private def deleteEmptyDirectories(base: Path): Unit = { def dirIsEmpty(dir: Path): Boolean = { - val ds = Files.newDirectoryStream(dir) + val ds = Files.newDirectoryStream(dir) val ret = ds.iterator().hasNext ds.close() !ret @@ -280,4 +263,5 @@ trait FilesystemSnippetsContainer extends SnippetsContainer with GenericFilesyst () } + } diff --git a/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemUsersContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemUsersContainer.scala index f8c999665..d0174a6f9 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemUsersContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/filesystem/FilesystemUsersContainer.scala @@ -1,20 +1,21 @@ package com.olegych.scastie.storage.filesystem +import java.nio.file._ +import scala.concurrent.Future +import scala.util.Try + import com.olegych.scastie.storage.PolicyAcceptance import com.olegych.scastie.storage.UserLogin import com.olegych.scastie.storage.UsersContainer import play.api.libs.json.Json -import java.nio.file._ -import scala.concurrent.Future -import scala.util.Try - trait FilesystemUsersContainer extends UsersContainer with GenericFilesystemContainer { val root: Path def addNewUser(user: UserLogin): Future[Boolean] = setPrivacyPolicyResponse(user, true) + def deleteUser(user: UserLogin): Future[Boolean] = Future { - val userDir = root.resolve(user.login) + val userDir = root.resolve(user.login) val privacyPolicyFile = userDir.resolve("policy-acceptance.json") Try { @@ -23,7 +24,7 @@ trait FilesystemUsersContainer extends UsersContainer with GenericFilesystemCont } def setPrivacyPolicyResponse(user: UserLogin, status: Boolean): Future[Boolean] = Future { - val userDir = root.resolve(user.login) + val userDir = root.resolve(user.login) val privacyPolicyFile = userDir.resolve("policy-acceptance.json") Try { @@ -33,18 +34,19 @@ trait FilesystemUsersContainer extends UsersContainer with GenericFilesystemCont }.isSuccess } - def getPrivacyPolicyResponse(user: UserLogin): Future[Boolean] = Future { - val userDir = root.resolve(user.login) + val userDir = root.resolve(user.login) val privacyPolicyFile = userDir.resolve("policy-acceptance.json") - val maybePrivacyPolicy = if (Files.exists(privacyPolicyFile)) { - val response = new String(Files.readAllBytes(privacyPolicyFile)) - Json.parse(response).asOpt[PolicyAcceptance] - } else { - None - } + val maybePrivacyPolicy = + if (Files.exists(privacyPolicyFile)) { + val response = new String(Files.readAllBytes(privacyPolicyFile)) + Json.parse(response).asOpt[PolicyAcceptance] + } else { + None + } maybePrivacyPolicy.map(_.acceptedPrivacyPolicy).getOrElse(true) } + } diff --git a/storage/src/main/scala/com.olegych.scastie.storage/filesystem/GenericFilesystemContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/filesystem/GenericFilesystemContainer.scala index 6f19c9aff..ab9d5a96b 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/filesystem/GenericFilesystemContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/filesystem/GenericFilesystemContainer.scala @@ -16,4 +16,5 @@ trait GenericFilesystemContainer { if (Files.exists(src)) Some(new String(Files.readAllBytes(src))) else None } + } diff --git a/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemoryContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemoryContainer.scala index 6b9acbfa4..b7efd66ef 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemoryContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemoryContainer.scala @@ -2,6 +2,7 @@ package com.olegych.scastie.storage.inmemory import scala.concurrent.ExecutionContext -class InMemoryContainer(implicit val ec: ExecutionContext) extends InMemoryUsersContainer with InMemorySnippetsContainer { - -} +class InMemoryContainer( + implicit val ec: ExecutionContext +) extends InMemoryUsersContainer + with InMemorySnippetsContainer {} diff --git a/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemorySnippetsContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemorySnippetsContainer.scala index 2e295a0a1..87532e4d7 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemorySnippetsContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemorySnippetsContainer.scala @@ -1,33 +1,30 @@ package com.olegych.scastie.storage.inmemory -import com.olegych.scastie.api._ -import com.olegych.scastie.storage.SnippetsContainer -import com.olegych.scastie.storage.UserLogin - import scala.collection.mutable import scala.concurrent.Future +import com.olegych.scastie.api._ +import com.olegych.scastie.storage.SnippetsContainer +import com.olegych.scastie.storage.UserLogin import System.{lineSeparator => nl} - trait InMemorySnippetsContainer extends SnippetsContainer { private val snippets = mutable.Map[SnippetId, Storage]() case class Storage( - snippetId: SnippetId, - inputs: Inputs, - progresses: mutable.Queue[SnippetProgress] = mutable.Queue(), - var scalaJsContent: String = "", - var scalaJsSourceMapContent: String = "", - time: Long = System.currentTimeMillis + snippetId: SnippetId, + inputs: Inputs, + progresses: mutable.Queue[SnippetProgress] = mutable.Queue(), + var scalaJsContent: String = "", + var scalaJsSourceMapContent: String = "", + time: Long = System.currentTimeMillis ) def appendOutput(progress: SnippetProgress): Future[Unit] = Future { - progress.snippetId.foreach( - id => snippets.get(id).foreach(storage => storage.progresses += progress) - ) + progress.snippetId.foreach(id => snippets.get(id).foreach(storage => storage.progresses += progress)) } + def delete(snippetId: SnippetId): Future[Boolean] = Future { val found = snippets.contains(snippetId) snippets -= snippetId @@ -48,13 +45,12 @@ trait InMemorySnippetsContainer extends SnippetsContainer { .toList } - def readScalaJs(snippetId: SnippetId): Future[Option[FetchResultScalaJs]] = - Future { - snippets.get(snippetId).map(m => FetchResultScalaJs(m.scalaJsContent)) - } + def readScalaJs(snippetId: SnippetId): Future[Option[FetchResultScalaJs]] = Future { + snippets.get(snippetId).map(m => FetchResultScalaJs(m.scalaJsContent)) + } def readScalaJsSourceMap( - snippetId: SnippetId + snippetId: SnippetId ): Future[Option[FetchResultScalaJsSourceMap]] = Future { snippets .get(snippetId) @@ -67,14 +63,14 @@ trait InMemorySnippetsContainer extends SnippetsContainer { def readOldSnippet(id: Int): Future[Option[FetchResult]] = Future(None) - protected def insert(snippetId: SnippetId, inputs: Inputs): Future[Unit] = - Future { - snippets.update(snippetId, Storage(snippetId, inputs.withSavedConfig)) - } + protected def insert(snippetId: SnippetId, inputs: Inputs): Future[Unit] = Future { + snippets.update(snippetId, Storage(snippetId, inputs.withSavedConfig)) + } override protected def hideFromUserProfile(snippetId: SnippetId): Future[Unit] = Future { for { old <- snippets.get(snippetId) } yield snippets.update(snippetId, old.copy(inputs = old.inputs.copy(isShowingInUserProfile = false))) } + } diff --git a/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemoryUsersContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemoryUsersContainer.scala index f348fbbd1..cbb2fd932 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemoryUsersContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/inmemory/InMemoryUsersContainer.scala @@ -1,12 +1,12 @@ package com.olegych.scastie.storage.inmemory +import scala.collection.mutable +import scala.concurrent.Future + import com.olegych.scastie.storage.PolicyAcceptance import com.olegych.scastie.storage.UserLogin import com.olegych.scastie.storage.UsersContainer -import scala.collection.mutable -import scala.concurrent.Future - trait InMemoryUsersContainer extends UsersContainer { val users: mutable.Set[PolicyAcceptance] = mutable.Set[PolicyAcceptance]() @@ -29,4 +29,5 @@ trait InMemoryUsersContainer extends UsersContainer { // All user containers will be removed after said period of time maybeUser.map(_.acceptedPrivacyPolicy).getOrElse(true) } + } diff --git a/storage/src/main/scala/com.olegych.scastie.storage/mongodb/GenericMongoContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/mongodb/GenericMongoContainer.scala index 346f03bdc..ca74ec04a 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/mongodb/GenericMongoContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/mongodb/GenericMongoContainer.scala @@ -23,4 +23,5 @@ trait GenericMongoContainer { ): Option[T] = { Json.parse(obj.toJson()).asOpt[T] } + } diff --git a/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBContainer.scala index 3eff97f67..006319801 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBContainer.scala @@ -1,13 +1,14 @@ package com.olegych.scastie.storage.mongodb +import scala.concurrent.ExecutionContext + import com.typesafe.config.ConfigFactory import org.mongodb.scala._ -import scala.concurrent.ExecutionContext - class MongoDBContainer(defaultConfig: Boolean = false)( implicit val ec: ExecutionContext -) extends MongoDBUsersContainer with MongoDBSnippetsContainer { +) extends MongoDBUsersContainer + with MongoDBSnippetsContainer { val mongoUri = { if (defaultConfig) s"mongodb://localhost:27017/scastie" diff --git a/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBSnippetsContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBSnippetsContainer.scala index 0d644990e..b0b44f5e3 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBSnippetsContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBSnippetsContainer.scala @@ -1,36 +1,43 @@ package com.olegych.scastie.storage.mongodb +import java.lang.System.{lineSeparator => nl} +import scala.concurrent.duration._ +import scala.concurrent.Await +import scala.concurrent.Future + import com.olegych.scastie.api._ import com.olegych.scastie.storage._ import org.mongodb.scala._ +import org.mongodb.scala.model._ import org.mongodb.scala.model.Filters._ import org.mongodb.scala.model.Updates._ -import org.mongodb.scala.model._ - -import java.lang.System.{lineSeparator => nl} -import scala.concurrent.Await -import scala.concurrent.Future -import scala.concurrent.duration._ - trait MongoDBSnippetsContainer extends SnippetsContainer with GenericMongoContainer { + lazy val snippets = { val db = database.getCollection[Document]("snippets") - Await.result(db.createIndex(Indexes.ascending("simpleSnippetId", "oldId"), IndexOptions().unique(true)).head(), Duration.Inf) - Await.result(Future.sequence(Seq( - Indexes.hashed("simpleSnippetId"), - Indexes.hashed("oldId"), - Indexes.hashed("user"), - Indexes.hashed("snippetId.user.login"), - Indexes.hashed("inputs.isShowingInUserProfile"), - Indexes.hashed("time") - ).map(db.createIndex(_).head())), Duration.Inf) + Await.result( + db.createIndex(Indexes.ascending("simpleSnippetId", "oldId"), IndexOptions().unique(true)).head(), + Duration.Inf + ) + Await.result( + Future.sequence( + Seq( + Indexes.hashed("simpleSnippetId"), + Indexes.hashed("oldId"), + Indexes.hashed("user"), + Indexes.hashed("snippetId.user.login"), + Indexes.hashed("inputs.isShowingInUserProfile"), + Indexes.hashed("time") + ).map(db.createIndex(_).head()) + ), + Duration.Inf + ) db } - def toMongoSnippet(snippetId: SnippetId, inputs: Inputs): MongoSnippet = MongoSnippet( simpleSnippetId = snippetId.url, user = snippetId.user.map(_.login), @@ -141,7 +148,7 @@ trait MongoDBSnippetsContainer extends SnippetsContainer with GenericMongoContai .map(_.flatMap(fromBson[MongoSnippet]).map(_.toFetchResult)) override def removeUserSnippets(user: UserLogin): Future[Boolean] = { - val query = or(Document("user" -> user.login), Document("snippetId.user.login" -> user.login)) + val query = or(Document("user" -> user.login), Document("snippetId.user.login" -> user.login)) val deletion = snippets.deleteMany(query).head().map(_.wasAcknowledged) lazy val validation = listSnippets(user).map(_.isEmpty) diff --git a/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBStoredClasses.scala b/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBStoredClasses.scala index 1f3d48f82..0f4cd8e5b 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBStoredClasses.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBStoredClasses.scala @@ -1,8 +1,8 @@ package com.olegych.scastie.storage -import play.api.libs.json.OFormat -import play.api.libs.json.Json import com.olegych.scastie.api._ +import play.api.libs.json.Json +import play.api.libs.json.OFormat sealed trait BaseMongoSnippet { def snippetId: SnippetId @@ -11,9 +11,9 @@ sealed trait BaseMongoSnippet { } case class ShortMongoSnippet( - snippetId: SnippetId, - inputs: ShortInputs, - time: Long + snippetId: SnippetId, + inputs: ShortInputs, + time: Long ) extends BaseMongoSnippet object ShortMongoSnippet { @@ -27,15 +27,15 @@ object PolicyAcceptance { } case class MongoSnippet( - simpleSnippetId: String, - user: Option[String], - snippetId: SnippetId, - oldId: Long, - inputs: Inputs, - progresses: List[SnippetProgress], - scalaJsContent: String, - scalaJsSourceMapContent: String, - time: Long + simpleSnippetId: String, + user: Option[String], + snippetId: SnippetId, + oldId: Long, + inputs: Inputs, + progresses: List[SnippetProgress], + scalaJsContent: String, + scalaJsSourceMapContent: String, + time: Long ) extends BaseMongoSnippet { def toFetchResult: FetchResult = FetchResult.create(inputs, progresses) } diff --git a/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBUsersContainer.scala b/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBUsersContainer.scala index 5bb67de3d..b2d43a75f 100644 --- a/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBUsersContainer.scala +++ b/storage/src/main/scala/com.olegych.scastie.storage/mongodb/MongoDBUsersContainer.scala @@ -1,15 +1,16 @@ package com.olegych.scastie.storage.mongodb +import scala.concurrent.duration._ +import scala.concurrent.Await +import scala.concurrent.Future + import com.olegych.scastie.storage._ import org.mongodb.scala._ -import org.mongodb.scala.model.Updates._ import org.mongodb.scala.model._ - -import scala.concurrent.Await -import scala.concurrent.Future -import scala.concurrent.duration._ +import org.mongodb.scala.model.Updates._ trait MongoDBUsersContainer extends UsersContainer with GenericMongoContainer { + lazy val users = { val db = database.getCollection[Document]("users") Await.result(db.createIndex(Indexes.ascending("user"), IndexOptions().unique(true)).head(), Duration.Inf) diff --git a/storage/src/test/scala/com.olegych.scastie.storage/ContainerTest.scala b/storage/src/test/scala/com.olegych.scastie.storage/ContainerTest.scala index 93eeac1ac..01796ee9f 100644 --- a/storage/src/test/scala/com.olegych.scastie.storage/ContainerTest.scala +++ b/storage/src/test/scala/com.olegych.scastie.storage/ContainerTest.scala @@ -1,38 +1,37 @@ package com.olegych.scastie.storage -import com.olegych.scastie.api._ -import com.olegych.scastie.storage.filesystem.FilesystemContainer -import com.olegych.scastie.storage.mongodb.MongoDBContainer -import org.scalatest.BeforeAndAfterAll -import org.scalatest.OptionValues -import org.scalatest.funsuite.AnyFunSuite - import java.io.IOException +import java.nio.file.attribute.BasicFileAttributes import java.nio.file.FileVisitResult import java.nio.file.Files import java.nio.file.Path import java.nio.file.SimpleFileVisitor -import java.nio.file.attribute.BasicFileAttributes import java.util.concurrent.Executors +import scala.concurrent.duration._ import scala.concurrent.Await import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future -import scala.concurrent.duration._ import scala.util.Random +import com.olegych.scastie.api._ +import com.olegych.scastie.storage.filesystem.FilesystemContainer +import com.olegych.scastie.storage.mongodb.MongoDBContainer +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.BeforeAndAfterAll +import org.scalatest.OptionValues + class ContainerTest extends AnyFunSuite with BeforeAndAfterAll with OptionValues { val mongo = sys.props.get("SnippetsContainerTest.mongo").flatMap(_.toBooleanOption).contains(true) println(s"ContainerTest using mongodb: $mongo") - val root = Files.createTempDirectory("test") + val root = Files.createTempDirectory("test") val oldRoot = Files.createTempDirectory("old-test") private val testContainer: SnippetsContainer with UsersContainer = { - if (mongo) - new MongoDBContainer(defaultConfig = true) + if (mongo) new MongoDBContainer(defaultConfig = true) else { new FilesystemContainer(root, oldRoot)( - ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor()) + ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor()) ) } } @@ -64,9 +63,8 @@ class ContainerTest extends AnyFunSuite with BeforeAndAfterAll with OptionValues } test("create snippet with logged in user") { - val bob = "bob" - val snippetId = - testContainer.create(Inputs.default, user = Some(UserLogin(bob))) + val bob = "bob" + val snippetId = testContainer.create(Inputs.default, user = Some(UserLogin(bob))) assert(snippetId.await.user.get.login == bob) } @@ -77,22 +75,19 @@ class ContainerTest extends AnyFunSuite with BeforeAndAfterAll with OptionValues } test("create then read") { - val inputs = Inputs.default + val inputs = Inputs.default val snippetId = testContainer.create(inputs, user = None).await - val result = testContainer.readSnippet(snippetId).await + val result = testContainer.readSnippet(snippetId).await assert(result.value.inputs == inputs.withSavedConfig) } test("fork") { - val inputs = - Inputs.default.copy(code = "source", isShowingInUserProfile = true) + val inputs = Inputs.default.copy(code = "source", isShowingInUserProfile = true) val snippetId = testContainer.save(inputs, user = None).await - val forkedInputs = - Inputs.default.copy(code = "forked", isShowingInUserProfile = true) - val forkedSnippetId = - testContainer.fork(snippetId, forkedInputs, user = None).await + val forkedInputs = Inputs.default.copy(code = "forked", isShowingInUserProfile = true) + val forkedSnippetId = testContainer.fork(snippetId, forkedInputs, user = None).await val forkedBis = testContainer.readSnippet(forkedSnippetId).await.get @@ -101,21 +96,22 @@ class ContainerTest extends AnyFunSuite with BeforeAndAfterAll with OptionValues } test("update") { - val user = UserLogin("github-user-update" + Random.nextInt()) - val inputs1 = - Inputs.default.copy(code = "inputs1").copy(isShowingInUserProfile = true) + val user = UserLogin("github-user-update" + Random.nextInt()) + val inputs1 = Inputs.default.copy(code = "inputs1").copy(isShowingInUserProfile = true) val snippetId1 = testContainer.save(inputs1, Some(user)).await assert(snippetId1.user.get.update == 0) - val inputs2 = - Inputs.default.copy(code = "inputs2").copy(isShowingInUserProfile = true) + val inputs2 = Inputs.default.copy(code = "inputs2").copy(isShowingInUserProfile = true) val snippetId2 = testContainer.update(snippetId1, inputs2).await.get assert(snippetId2.user.get.update == 1, "we get a new update id") val readInputs1 = testContainer.readSnippet(snippetId1).await.get.inputs val readInputs2 = testContainer.readSnippet(snippetId2).await.get.inputs - assert(readInputs1 == inputs1.copy(isShowingInUserProfile = false).withSavedConfig, "we don't mutate previous input") + assert( + readInputs1 == inputs1.copy(isShowingInUserProfile = false).withSavedConfig, + "we don't mutate previous input" + ) assert(readInputs2 == inputs2.copy(forked = Some(snippetId1)).withSavedConfig, "we update forked") val snippets = testContainer.listSnippets(user).await @@ -123,7 +119,7 @@ class ContainerTest extends AnyFunSuite with BeforeAndAfterAll with OptionValues } test("listSnippets") { - val user = UserLogin("github-user-list" + Random.nextInt()) + val user = UserLogin("github-user-list" + Random.nextInt()) val user2 = UserLogin("github-user-list2" + Random.nextInt()) val inputs1 = Inputs.default.copy(code = "inputs1") @@ -138,8 +134,7 @@ class ContainerTest extends AnyFunSuite with BeforeAndAfterAll with OptionValues val user2inputs = Inputs.default.copy(code = "inputs3") testContainer.save(user2inputs, Some(user2)).await - val inputs4 = - Inputs.default.copy(code = "inputs4", isShowingInUserProfile = false) + val inputs4 = Inputs.default.copy(code = "inputs4", isShowingInUserProfile = false) testContainer.create(inputs4, Some(user)).await val snippets = testContainer.listSnippets(user).await @@ -151,16 +146,16 @@ class ContainerTest extends AnyFunSuite with BeforeAndAfterAll with OptionValues test("delete") { val user = UserLogin("github-user-delete" + Random.nextInt()) - val inputs1 = Inputs.default.copy(code = "inputs1") + val inputs1 = Inputs.default.copy(code = "inputs1") val snippetId1 = testContainer.save(inputs1, Some(user)).await val inputs1U = Inputs.default.copy(code = "inputs1 updated") testContainer.update(snippetId1, inputs1U).await.get - val inputs2 = Inputs.default.copy(code = "inputs2") + val inputs2 = Inputs.default.copy(code = "inputs2") val snippetId2 = testContainer.save(inputs2, Some(user)).await - val inputs2U = Inputs.default.copy(code = "inputs2 updated") + val inputs2U = Inputs.default.copy(code = "inputs2 updated") val snippetId2U = testContainer.update(snippetId2, inputs2U).await.get assert(testContainer.listSnippets(user).await.size == 2) @@ -174,9 +169,9 @@ class ContainerTest extends AnyFunSuite with BeforeAndAfterAll with OptionValues } test("appendOutput") { - val inputs = Inputs.default + val inputs = Inputs.default val snippetId = testContainer.create(inputs, user = None).await - val progress = SnippetProgress.default.copy(snippetId = Some(snippetId)) + val progress = SnippetProgress.default.copy(snippetId = Some(snippetId)) testContainer.appendOutput(progress) val result = testContainer.readSnippet(snippetId).await @@ -186,13 +181,13 @@ class ContainerTest extends AnyFunSuite with BeforeAndAfterAll with OptionValues test("deleteAllSnippets") { val user = UserLogin("github-user-delete" + Random.nextInt()) - val inputs1 = Inputs.default.copy(code = "inputs1") + val inputs1 = Inputs.default.copy(code = "inputs1") val snippetId1 = testContainer.save(inputs1, Some(user)).await - val inputs2 = Inputs.default.copy(code = "inputs2") + val inputs2 = Inputs.default.copy(code = "inputs2") val snippetId2 = testContainer.save(inputs2, Some(user)).await - val inputs2U = Inputs.default.copy(code = "inputs2 updated") + val inputs2U = Inputs.default.copy(code = "inputs2 updated") val snippetId2U = testContainer.update(snippetId2, inputs2U).await.get assert(testContainer.listSnippets(user).await.size == 2) @@ -216,32 +211,41 @@ class ContainerTest extends AnyFunSuite with BeforeAndAfterAll with OptionValues } test("add new user") { - ensureUserCleanup("bob", { username => - val snippetId = testContainer.addNewUser(UserLogin(username)).await - assert(snippetId) - }) + ensureUserCleanup( + "bob", + { username => + val snippetId = testContainer.addNewUser(UserLogin(username)).await + assert(snippetId) + } + ) } test("get user privacy policy acceptance") { - ensureUserCleanup("bob", { username => - val snippetId = testContainer.addNewUser(UserLogin(username)).await - val response = testContainer.getPrivacyPolicyResponse(UserLogin(username)).await - assert(testContainer.deleteUser(UserLogin(username)).await == true) - }) + ensureUserCleanup( + "bob", + { username => + val snippetId = testContainer.addNewUser(UserLogin(username)).await + val response = testContainer.getPrivacyPolicyResponse(UserLogin(username)).await + assert(testContainer.deleteUser(UserLogin(username)).await == true) + } + ) } test("set user privacy policy acceptance") { - ensureUserCleanup("bob", { username => - val snippetId = testContainer.addNewUser(UserLogin(username)).await - val updatePrivacyPolicy = testContainer.setPrivacyPolicyResponse(UserLogin(username), false).await - val response = testContainer.getPrivacyPolicyResponse(UserLogin(username)).await - assert(response == false) - }) + ensureUserCleanup( + "bob", + { username => + val snippetId = testContainer.addNewUser(UserLogin(username)).await + val updatePrivacyPolicy = testContainer.setPrivacyPolicyResponse(UserLogin(username), false).await + val response = testContainer.getPrivacyPolicyResponse(UserLogin(username)).await + assert(response == false) + } + ) } test("remove user from privacy policy list") { - val username = "bob" - val snippetId = testContainer.addNewUser(UserLogin(username)).await + val username = "bob" + val snippetId = testContainer.addNewUser(UserLogin(username)).await val removeUser = testContainer.deleteUser(UserLogin(username)).await assert(removeUser == true) } diff --git a/utils/src/main/scala/com.olegych.scastie/util/Base64UUID.scala b/utils/src/main/scala/com.olegych.scastie/util/Base64UUID.scala index e94124cd8..38bd49feb 100644 --- a/utils/src/main/scala/com.olegych.scastie/util/Base64UUID.scala +++ b/utils/src/main/scala/com.olegych.scastie/util/Base64UUID.scala @@ -4,12 +4,12 @@ import java.nio.ByteBuffer import java.util.{Base64, UUID} object Base64UUID { + // example output: GGdknrcEQVu3elXyboKcYQ def create: String = { def toBase64(uuid: UUID): String = { - val (high, low) = - (uuid.getMostSignificantBits, uuid.getLeastSignificantBits) - val buffer = ByteBuffer.allocate(java.lang.Long.BYTES * 2) + val (high, low) = (uuid.getMostSignificantBits, uuid.getLeastSignificantBits) + val buffer = ByteBuffer.allocate(java.lang.Long.BYTES * 2) buffer.putLong(high) buffer.putLong(low) val encoded = Base64.getMimeEncoder.encodeToString(buffer.array()) @@ -17,7 +17,7 @@ object Base64UUID { } var res: String = null - val allowed = ('a' to 'z').toSet ++ ('A' to 'Z').toSet ++ ('0' to '9').toSet + val allowed = ('a' to 'z').toSet ++ ('A' to 'Z').toSet ++ ('0' to '9').toSet while (res == null || res.exists(c => !allowed.contains(c))) { val uuid = java.util.UUID.randomUUID() @@ -26,4 +26,5 @@ object Base64UUID { res } + } diff --git a/utils/src/main/scala/com.olegych.scastie/util/BlockingProcess.scala b/utils/src/main/scala/com.olegych.scastie/util/BlockingProcess.scala index f8e48425a..5f5e178fe 100644 --- a/utils/src/main/scala/com.olegych.scastie/util/BlockingProcess.scala +++ b/utils/src/main/scala/com.olegych.scastie/util/BlockingProcess.scala @@ -1,20 +1,20 @@ /** - * Copyright (C) 2009-2014 Typesafe Inc. - */ + * Copyright (C) 2009-2014 Typesafe Inc. + */ package akka.contrib.process -import akka.actor.{Actor, ActorLogging, ActorRef, NoSerializationVerificationNeeded, Props, SupervisorStrategy, Terminated} -import akka.stream.{ActorAttributes, IOResult} -import akka.stream.scaladsl.{Sink, Source, StreamConverters} -import akka.util.{ByteString, Helpers} import java.io.File import java.lang.{Process => JavaProcess, ProcessBuilder => JavaProcessBuilder} import java.util.concurrent.TimeUnit - import scala.collection.immutable -import scala.concurrent.{Future, blocking} +import scala.concurrent.{blocking, Future} import scala.concurrent.duration.Duration +import akka.actor.{Actor, ActorLogging, ActorRef, NoSerializationVerificationNeeded, Props, SupervisorStrategy, Terminated} +import akka.stream.{ActorAttributes, IOResult} +import akka.stream.scaladsl.{Sink, Source, StreamConverters} +import akka.util.{ByteString, Helpers} + object BlockingProcess { def getPid(process: JavaProcess): Long = { @@ -22,143 +22,151 @@ object BlockingProcess { } /** - * The configuration key to use in order to override the dispatcher used for blocking IO. - */ - final val BlockingIODispatcherId = - "akka.process.blocking-process.blocking-io-dispatcher-id" + * The configuration key to use in order to override the dispatcher used for blocking IO. + */ + final val BlockingIODispatcherId = "akka.process.blocking-process.blocking-io-dispatcher-id" /** - * Sent to the receiver on startup - specifies the streams used for managing input, output and error respectively. - * This message should only be received by the parent of the BlockingProcess and should not be passed across the - * JVM boundary (the publishers are not serializable). - * - * @param stdin a `akka.stream.scaladsl.Sink[ByteString, Future[IOResult]]` for the standard input stream of the process - * @param stdout a `akka.stream.scaladsl.Source[ByteString, Future[IOResult]]` for the standard output stream of the process - * @param stderr a `akka.stream.scaladsl.Source[ByteString, Future[IOResult]]` for the standard error stream of the process - */ - case class Started(pid: Option[Long], - stdin: Sink[ByteString, Future[IOResult]], - stdout: Source[ByteString, Future[IOResult]], - stderr: Source[ByteString, Future[IOResult]]) - extends NoSerializationVerificationNeeded + * Sent to the receiver on startup - specifies the streams used for managing input, output and error respectively. + * This message should only be received by the parent of the BlockingProcess and should not be passed across the JVM + * boundary (the publishers are not serializable). + * + * @param stdin + * a `akka.stream.scaladsl.Sink[ByteString, Future[IOResult]]` for the standard input stream of the process + * @param stdout + * a `akka.stream.scaladsl.Source[ByteString, Future[IOResult]]` for the standard output stream of the process + * @param stderr + * a `akka.stream.scaladsl.Source[ByteString, Future[IOResult]]` for the standard error stream of the process + */ + case class Started( + pid: Option[Long], + stdin: Sink[ByteString, Future[IOResult]], + stdout: Source[ByteString, Future[IOResult]], + stderr: Source[ByteString, Future[IOResult]] + ) extends NoSerializationVerificationNeeded /** - * Sent to the receiver after the process has exited. - * - * @param exitValue the exit value of the process - */ + * Sent to the receiver after the process has exited. + * + * @param exitValue + * the exit value of the process + */ case class Exited(exitValue: Int) /** - * Send a request to destroy the process. - * On POSIX, this sends a SIGTERM, but implementation is platform specific. - */ + * Send a request to destroy the process. On POSIX, this sends a SIGTERM, but implementation is platform specific. + */ case object Destroy /** - * Send a request to forcibly destroy the process. - * On POSIX, this sends a SIGKILL, but implementation is platform specific. - */ + * Send a request to forcibly destroy the process. On POSIX, this sends a SIGKILL, but implementation is platform + * specific. + */ case object DestroyForcibly /** - * Sent if stdin from the process is terminated - */ + * Sent if stdin from the process is terminated + */ case object StdinTerminated /** - * Sent if stdout from the process is terminated - */ + * Sent if stdout from the process is terminated + */ case object StdoutTerminated /** - * Sent if stderr from the process is terminated - */ + * Sent if stderr from the process is terminated + */ case object StderrTerminated /** - * Create Props for a [[BlockingProcess]] actor. - * - * @param command signifies the program to be executed and its optional arguments - * @param workingDir the working directory for the process; default is the current working directory - * @param environment the environment for the process; default is `Map.emtpy` - * @param stdioTimeout the amount of time to tolerate waiting for a process to communicate back to this actor - * @return Props for a [[BlockingProcess]] actor - */ - def props(command: immutable.Seq[String], - workingDir: File = new File(System.getProperty("user.dir")), - environment: Map[String, String] = Map.empty, - stdioTimeout: Duration = Duration.Undefined) = - Props(new BlockingProcess(command, workingDir, environment, stdioTimeout)) + * Create Props for a [[BlockingProcess]] actor. + * + * @param command + * signifies the program to be executed and its optional arguments + * @param workingDir + * the working directory for the process; default is the current working directory + * @param environment + * the environment for the process; default is `Map.emtpy` + * @param stdioTimeout + * the amount of time to tolerate waiting for a process to communicate back to this actor + * @return + * Props for a [[BlockingProcess]] actor + */ + def props( + command: immutable.Seq[String], + workingDir: File = new File(System.getProperty("user.dir")), + environment: Map[String, String] = Map.empty, + stdioTimeout: Duration = Duration.Undefined + ) = Props(new BlockingProcess(command, workingDir, environment, stdioTimeout)) private def prepareCommand(command: Seq[String]) = if (Helpers.isWindows) List("cmd", "/c") ++ (command map winQuote) else command /** - * This quoting functionality is as recommended per http://bugs.java.com/view_bug.do?bug_id=6511002 - * The JDK can't change due to its backward compatibility requirements, but we have no such constraint - * here. Args should be able to be expressed consistently by the user of our API no matter whether - * execution is on Windows or not. - * - * @param s command string to be quoted - * @return quoted string - */ + * This quoting functionality is as recommended per http://bugs.java.com/view_bug.do?bug_id=6511002 The JDK can't + * change due to its backward compatibility requirements, but we have no such constraint here. Args should be able to + * be expressed consistently by the user of our API no matter whether execution is on Windows or not. + * + * @param s + * command string to be quoted + * @return + * quoted string + */ private def winQuote(s: String): String = { - def needsQuoting(s: String) = - s.isEmpty || (s exists ( - c => c == ' ' || c == '\t' || c == '\\' || c == '"' - )) + def needsQuoting(s: String) = s.isEmpty || (s exists (c => c == ' ' || c == '\t' || c == '\\' || c == '"')) if (needsQuoting(s)) { val quoted = s .replaceAll("""([\\]*)"""", """$1$1\\"""") .replaceAll("""([\\]*)\z""", "$1$1") s""""$quoted"""" - } else - s + } else s } + } /** - * This actor uses the JDK process API. As such, more memory given that more threads are consumed. Favor the - * [[NonBlockingProcess]] actor unless you *need* to use the JDK. - * - * BlockingProcess encapsulates an operating system process and its ability to be communicated with via stdio i.e. - * stdin, stdout and stderr. The reactive streams for stdio are communicated in a BlockingProcess.Started event - * upon the actor being established. The parent actor is then subsequently streamed - * stdout and stderr events. When the process exists (determined by periodically polling process.isAlive()) then - * the process's exit code is communicated to the receiver in a BlockingProcess.Exited event. - * - * A dispatcher as indicated by the "akka.process.blocking-process.blocking-io-dispatcher-id" setting is used - * internally by the actor as various JDK calls are made which can block. - */ -class BlockingProcess(command: immutable.Seq[String], directory: File, environment: Map[String, String], stdioTimeout: Duration) - extends Actor - with ActorLogging { + * This actor uses the JDK process API. As such, more memory given that more threads are consumed. Favor the + * [[NonBlockingProcess]] actor unless you *need* to use the JDK. + * + * BlockingProcess encapsulates an operating system process and its ability to be communicated with via stdio i.e. + * stdin, stdout and stderr. The reactive streams for stdio are communicated in a BlockingProcess.Started event upon + * the actor being established. The parent actor is then subsequently streamed stdout and stderr events. When the + * process exists (determined by periodically polling process.isAlive()) then the process's exit code is communicated + * to the receiver in a BlockingProcess.Exited event. + * + * A dispatcher as indicated by the "akka.process.blocking-process.blocking-io-dispatcher-id" setting is used + * internally by the actor as various JDK calls are made which can block. + */ +class BlockingProcess( + command: immutable.Seq[String], + directory: File, + environment: Map[String, String], + stdioTimeout: Duration +) extends Actor + with ActorLogging { - import BlockingProcess._ import context.dispatcher + import BlockingProcess._ - override val supervisorStrategy: SupervisorStrategy = - SupervisorStrategy.stoppingStrategy + override val supervisorStrategy: SupervisorStrategy = SupervisorStrategy.stoppingStrategy override def preStart(): Unit = { println("preStart") val process: JavaProcess = { import scala.jdk.CollectionConverters._ val preparedCommand = prepareCommand(command) - val pb = new JavaProcessBuilder(preparedCommand.asJava) + val pb = new JavaProcessBuilder(preparedCommand.asJava) pb.environment().putAll(environment.asJava) pb.directory(directory) pb.start() } - val blockingIODispatcherId = - context.system.settings.config.getString(BlockingIODispatcherId) + val blockingIODispatcherId = context.system.settings.config.getString(BlockingIODispatcherId) try { - val selfDispatcherAttribute = - ActorAttributes.dispatcher(blockingIODispatcherId) + val selfDispatcherAttribute = ActorAttributes.dispatcher(blockingIODispatcherId) val stdin = StreamConverters .fromOutputStream(() => process.getOutputStream(), autoFlush = true) @@ -200,8 +208,7 @@ class BlockingProcess(command: immutable.Seq[String], directory: File, environme case DestroyForcibly => log.debug("Received request to forcibly destroy the process.") tellDestroyer(ProcessDestroyer.DestroyForcibly) - case Terminated(_) => - context.stop(self) + case Terminated(_) => context.stop(self) case StdinTerminated => log.debug("Stdin was terminated") tellDestroyer(ProcessDestroyer.Inspect) @@ -213,34 +220,31 @@ class BlockingProcess(command: immutable.Seq[String], directory: File, environme tellDestroyer(ProcessDestroyer.Inspect) } - private def tellDestroyer(msg: Any) = - context.child("process-destroyer").foreach(_ ! msg) + private def tellDestroyer(msg: Any) = context.child("process-destroyer").foreach(_ ! msg) } private object ProcessDestroyer { /** - * The configuration key to use for the inspection interval. - */ - final val InspectionInterval = - "akka.process.blocking-process.inspection-interval" + * The configuration key to use for the inspection interval. + */ + final val InspectionInterval = "akka.process.blocking-process.inspection-interval" /** - * Inspect the Process to ensure it is still alive. This is necessary because - * a process can exit without its stdout/stderr file handles being closed, for - * instance if a process forks and a child continues to run when it dies, - * it will have a reference to those handles. - */ + * Inspect the Process to ensure it is still alive. This is necessary because a process can exit without its + * stdout/stderr file handles being closed, for instance if a process forks and a child continues to run when it + * dies, it will have a reference to those handles. + */ case object Inspect /** - * Request that process.destroy() be called - */ + * Request that process.destroy() be called + */ case object Destroy /** - * Request that process.destroyForcibly() be called - */ + * Request that process.destroyForcibly() be called + */ case object DestroyForcibly def props(process: JavaProcess, exitValueReceiver: ActorRef): Props = @@ -248,14 +252,13 @@ private object ProcessDestroyer { } private class ProcessDestroyer(process: JavaProcess, exitValueReceiver: ActorRef) extends Actor with ActorLogging { - import ProcessDestroyer._ import context.dispatcher + import ProcessDestroyer._ - private val inspectionInterval = - Duration( - context.system.settings.config.getDuration(InspectionInterval).toMillis, - TimeUnit.MILLISECONDS - ) + private val inspectionInterval = Duration( + context.system.settings.config.getDuration(InspectionInterval).toMillis, + TimeUnit.MILLISECONDS + ) private val inspectionTick = context.system.scheduler.scheduleAtFixedRate(inspectionInterval, inspectionInterval, self, Inspect) @@ -274,12 +277,9 @@ private class ProcessDestroyer(process: JavaProcess, exitValueReceiver: ActorRef } override def receive = { - case Destroy => - blocking(pkill()) - case DestroyForcibly => - blocking(pkill()) - case Inspect => - if (!process.isAlive) { + case Destroy => blocking(pkill()) + case DestroyForcibly => blocking(pkill()) + case Inspect => if (!process.isAlive) { log.debug("Process has terminated, stopping self") context.stop(self) } @@ -294,4 +294,5 @@ private class ProcessDestroyer(process: JavaProcess, exitValueReceiver: ActorRef } exitValueReceiver ! BlockingProcess.Exited(exitValue) } + } diff --git a/utils/src/main/scala/com.olegych.scastie/util/GraphStageForwarder.scala b/utils/src/main/scala/com.olegych.scastie/util/GraphStageForwarder.scala index 4975c8bda..58a639016 100644 --- a/utils/src/main/scala/com.olegych.scastie/util/GraphStageForwarder.scala +++ b/utils/src/main/scala/com.olegych.scastie/util/GraphStageForwarder.scala @@ -1,15 +1,15 @@ package com.olegych.scastie.util +import scala.reflect.runtime.universe._ + import akka.actor.ActorRef import akka.stream.{Attributes, Outlet, SourceShape} import akka.stream.stage.{GraphStage, GraphStageLogic} -import scala.reflect.runtime.universe._ - class GraphStageForwarder[T: TypeTag, U: TypeTag]( - outletName: String, - coordinator: ActorRef, - graphId: U + outletName: String, + coordinator: ActorRef, + graphId: U ) extends GraphStage[SourceShape[T]] { val out: Outlet[T] = Outlet(outletName) override val shape = SourceShape[T](out) diff --git a/utils/src/main/scala/com.olegych.scastie/util/GraphStageLogicForwarder.scala b/utils/src/main/scala/com.olegych.scastie/util/GraphStageLogicForwarder.scala index 3f602211b..a9fdf553b 100644 --- a/utils/src/main/scala/com.olegych.scastie/util/GraphStageLogicForwarder.scala +++ b/utils/src/main/scala/com.olegych.scastie/util/GraphStageLogicForwarder.scala @@ -1,14 +1,18 @@ package com.olegych.scastie.util +import scala.collection.mutable.{Queue => MQueue} +import scala.reflect.runtime.universe._ + import akka.actor.ActorRef import akka.stream.{Outlet, SourceShape} import akka.stream.stage.{GraphStageLogic, OutHandler} -import scala.collection.mutable.{Queue => MQueue} -import scala.reflect.runtime.universe._ - -class GraphStageLogicForwarder[T: TypeTag, U: TypeTag](out: Outlet[T], shape: SourceShape[T], coordinator: ActorRef, graphId: U) - extends GraphStageLogic(shape) { +class GraphStageLogicForwarder[T: TypeTag, U: TypeTag]( + out: Outlet[T], + shape: SourceShape[T], + coordinator: ActorRef, + graphId: U +) extends GraphStageLogic(shape) { setHandler( out, @@ -26,15 +30,12 @@ class GraphStageLogicForwarder[T: TypeTag, U: TypeTag](out: Outlet[T], shape: So private val buffer = MQueue.empty[T] - private def deliver(): Unit = - if (isAvailable(out) && buffer.nonEmpty) - push[T](out, buffer.dequeue) + private def deliver(): Unit = if (isAvailable(out) && buffer.nonEmpty) push[T](out, buffer.dequeue) - private def bufferElement(receive: (ActorRef, Any)): Unit = - receive match { - case (_, element: T @unchecked) => - buffer.enqueue(element) - deliver() - } + private def bufferElement(receive: (ActorRef, Any)): Unit = receive match { + case (_, element: T @unchecked) => + buffer.enqueue(element) + deliver() + } } diff --git a/utils/src/main/scala/com.olegych.scastie/util/ProcessActor.scala b/utils/src/main/scala/com.olegych.scastie/util/ProcessActor.scala index 9b1599837..dbaaf2029 100644 --- a/utils/src/main/scala/com.olegych.scastie/util/ProcessActor.scala +++ b/utils/src/main/scala/com.olegych.scastie/util/ProcessActor.scala @@ -3,28 +3,30 @@ package com.olegych.scastie.util import java.nio.file._ import java.time.Instant import java.util.concurrent.atomic.AtomicLong +import scala.concurrent.duration._ import akka.actor.{Actor, ActorRef, Props, Stash} import akka.contrib.process._ -import akka.stream.scaladsl.{Flow, Framing, Sink, Source} import akka.stream.{ActorMaterializer, ActorMaterializerSettings, OverflowStrategy, ThrottleMode} +import akka.stream.scaladsl.{Flow, Framing, Sink, Source} import akka.util.ByteString import com.olegych.scastie.api.{ProcessOutput, ProcessOutputType} import org.slf4j.LoggerFactory -import scala.concurrent.duration._ - object ProcessActor { case class Input(line: String) case object Shutdown - def props(command: List[String], - workingDir: Path = Paths.get(System.getProperty("user.dir")), - environment: Map[String, String] = Map.empty, - killOnExit: Boolean = false): Props = { + def props( + command: List[String], + workingDir: Path = Paths.get(System.getProperty("user.dir")), + environment: Map[String, String] = Map.empty, + killOnExit: Boolean = false + ): Props = { Props(new ProcessActor(command, workingDir, environment, killOnExit)) } + } /* @@ -34,8 +36,8 @@ object ProcessActor { */ class ProcessActor(command: List[String], workingDir: Path, environment: Map[String, String], killOnExit: Boolean) - extends Actor - with Stash { + extends Actor + with Stash { import ProcessActor._ @@ -49,12 +51,12 @@ class ProcessActor(command: List[String], workingDir: Path, environment: Map[Str // ) // import NonBlockingProcess._ - private val props = - BlockingProcess.props( - command = command, - workingDir = workingDir.toFile, - environment = environment - ) + private val props = BlockingProcess.props( + command = command, + workingDir = workingDir.toFile, + environment = environment + ) + import BlockingProcess._ private val process = context.actorOf(props, name = "process") @@ -76,6 +78,7 @@ class ProcessActor(command: List[String], workingDir: Path, environment: Map[Str ) private val outputId = new AtomicLong(0) + override def receive: Receive = { case Started(pid, stdin, stdout, stderr) => println("process started: " + pid) @@ -90,30 +93,26 @@ class ProcessActor(command: List[String], workingDir: Path, environment: Map[Str } ) .throttle(100, 1.second, 100, ThrottleMode.Shaping) - .runWith(Sink.fold(Instant.now) { - case (ts, output) => - val now = Instant.now - println(s"> ${output.id.getOrElse(0)} ${now.toEpochMilli - ts.toEpochMilli}ms: ${output.line}") - context.parent ! output - now + .runWith(Sink.fold(Instant.now) { case (ts, output) => + val now = Instant.now + println(s"> ${output.id.getOrElse(0)} ${now.toEpochMilli - ts.toEpochMilli}ms: ${output.line}") + context.parent ! output + now }) - val stdin2: Source[ByteString, ActorRef] = - Source - .actorRef[Input](Int.MaxValue, OverflowStrategy.fail) - .map { case Input(line) => ByteString(line + "\n") } + val stdin2: Source[ByteString, ActorRef] = Source + .actorRef[Input](Int.MaxValue, OverflowStrategy.fail) + .map { case Input(line) => ByteString(line + "\n") } - val ref: ActorRef = - Flow[ByteString] - .to(stdin) - .runWith(stdin2) + val ref: ActorRef = Flow[ByteString] + .to(stdin) + .runWith(stdin2) context.become(active(ref)) unstashAll() - case input: Input => - stash() + case input: Input => stash() } private def active(stdin: ActorRef): Receive = { @@ -121,9 +120,9 @@ class ProcessActor(command: List[String], workingDir: Path, environment: Map[Str println(s"< ${outputId.incrementAndGet()}: $input") stdin ! input - case Exited(exitValue) => - if (killOnExit) { + case Exited(exitValue) => if (killOnExit) { throw new Exception("process exited: " + exitValue) } } + } diff --git a/utils/src/main/scala/com.olegych.scastie/util/ReconnectingActor.scala b/utils/src/main/scala/com.olegych.scastie/util/ReconnectingActor.scala index fcc76a811..782aed396 100644 --- a/utils/src/main/scala/com.olegych.scastie/util/ReconnectingActor.scala +++ b/utils/src/main/scala/com.olegych.scastie/util/ReconnectingActor.scala @@ -1,12 +1,12 @@ package com.olegych.scastie.util +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global + import akka.actor.{Actor, ActorContext, ActorLogging, Cancellable} import akka.remote.DisassociatedEvent import com.olegych.scastie.api.ActorConnected -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ - case class ReconnectInfo(serverHostname: String, serverAkkaPort: Int, actorHostname: String, actorAkkaPort: Int) trait ActorReconnecting extends Actor with ActorLogging { @@ -49,15 +49,13 @@ trait ActorReconnecting extends Actor with ActorLogging { case ev: DisassociatedEvent => { println("DisassociatedEvent " + ev) - val isServerHostname = - reconnectInfo - .map(info => ev.remoteAddress.host.contains(info.serverHostname)) - .getOrElse(false) + val isServerHostname = reconnectInfo + .map(info => ev.remoteAddress.host.contains(info.serverHostname)) + .getOrElse(false) - val isServerAkkaPort = - reconnectInfo - .map(info => ev.remoteAddress.port.contains(info.serverAkkaPort)) - .getOrElse(false) + val isServerAkkaPort = reconnectInfo + .map(info => ev.remoteAddress.port.contains(info.serverAkkaPort)) + .getOrElse(false) if (isServerHostname && isServerAkkaPort && ev.inbound) { log.warning("Disconnected from server") @@ -66,4 +64,5 @@ trait ActorReconnecting extends Actor with ActorLogging { } } } + } diff --git a/utils/src/main/scala/com.olegych.scastie/util/SbtTask.scala b/utils/src/main/scala/com.olegych.scastie/util/SbtTask.scala index b0dfb5a05..599987157 100644 --- a/utils/src/main/scala/com.olegych.scastie/util/SbtTask.scala +++ b/utils/src/main/scala/com.olegych.scastie/util/SbtTask.scala @@ -1,8 +1,7 @@ package com.olegych.scastie.util -import com.olegych.scastie.api._ - import akka.actor.ActorRef +import com.olegych.scastie.api._ case class SbtTask(snippetId: SnippetId, inputs: Inputs, ip: String, login: Option[String], progressActor: ActorRef) diff --git a/utils/src/main/scala/com.olegych.scastie/util/ScastieFileUtil.scala b/utils/src/main/scala/com.olegych.scastie/util/ScastieFileUtil.scala index d9fa1902c..01d48a79b 100644 --- a/utils/src/main/scala/com.olegych.scastie/util/ScastieFileUtil.scala +++ b/utils/src/main/scala/com.olegych.scastie/util/ScastieFileUtil.scala @@ -1,10 +1,11 @@ package com.olegych.scastie.util -import java.nio.file._ import java.lang.management.ManagementFactory import java.nio.charset.StandardCharsets +import java.nio.file._ object ScastieFileUtil { + def slurp(src: Path): Option[String] = { if (Files.exists(src)) Some(Files.readAllLines(src).toArray.mkString("\n")) else None @@ -24,7 +25,7 @@ object ScastieFileUtil { } def writeRunningPid(name: String): String = { - val pid = ManagementFactory.getRuntimeMXBean.getName.split("@").head + val pid = ManagementFactory.getRuntimeMXBean.getName.split("@").head val pidFile = Paths.get(name) Files.write(pidFile, pid.getBytes(StandardCharsets.UTF_8)) sys.addShutdownHook { @@ -32,4 +33,5 @@ object ScastieFileUtil { } pid } + } diff --git a/utils/src/test/scala/com.olegych.scastie.util/ProcessActorTest.scala b/utils/src/test/scala/com.olegych.scastie.util/ProcessActorTest.scala index 0d0f2e9cd..5501ce3cf 100644 --- a/utils/src/test/scala/com.olegych.scastie.util/ProcessActorTest.scala +++ b/utils/src/test/scala/com.olegych.scastie.util/ProcessActorTest.scala @@ -2,18 +2,21 @@ package com.olegych.scastie.util import java.io.File import java.nio.file.{Files, StandardCopyOption} +import scala.concurrent.duration._ import akka.actor.{Actor, ActorRef, ActorSystem} import akka.testkit.{ImplicitSender, TestActorRef, TestKit, TestProbe} import com.olegych.scastie.api.ProcessOutput import com.olegych.scastie.api.ProcessOutputType._ import com.olegych.scastie.util.ProcessActor._ -import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.BeforeAndAfterAll -import scala.concurrent.duration._ - -class ProcessActorTest() extends TestKit(ActorSystem("ProcessActorTest")) with ImplicitSender with AnyFunSuiteLike with BeforeAndAfterAll { +class ProcessActorTest() + extends TestKit(ActorSystem("ProcessActorTest")) + with ImplicitSender + with AnyFunSuiteLike + with BeforeAndAfterAll { test("do it") { (1 to 10).foreach { i => @@ -48,15 +51,16 @@ class ProcessActorTest() extends TestKit(ActorSystem("ProcessActorTest")) with I override def afterAll(): Unit = { TestKit.shutdownActorSystem(system) } + } class ProcessReceiver(command: String, probe: ActorRef) extends Actor { - private val props = - ProcessActor.props(command = List("bash", "-c", command.replace("\\", "/")), killOnExit = false) + private val props = ProcessActor.props(command = List("bash", "-c", command.replace("\\", "/")), killOnExit = false) private val process = context.actorOf(props, name = "process-receiver") override def receive: Receive = { case output: ProcessOutput => probe ! output case input: Input => process ! input } + }