├── .github └── workflows │ ├── bootstrap.yml │ ├── main.yaml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── core └── src │ ├── main │ ├── resources │ │ └── NOTICE.md │ ├── scala-3.3.6 │ │ └── replpp │ │ │ ├── DottyRandomStuff.scala │ │ │ ├── DottyReplDriver.scala │ │ │ └── InteractiveShell.scala │ ├── scala-3.6.4 │ │ └── replpp │ │ │ ├── DottyRandomStuff.scala │ │ │ ├── DottyReplDriver.scala │ │ │ └── InteractiveShell.scala │ ├── scala-3.7.0 │ │ └── replpp │ │ │ ├── DottyRandomStuff.scala │ │ │ ├── DottyReplDriver.scala │ │ │ └── InteractiveShell.scala │ └── scala │ │ └── replpp │ │ ├── Config.scala │ │ ├── Dependencies.scala │ │ ├── JLineTerminal.scala │ │ ├── Main.scala │ │ ├── Operators.scala │ │ ├── PPrinter.scala │ │ ├── Rendering.scala │ │ ├── ReplDriver.scala │ │ ├── ReplDriverBase.scala │ │ ├── SyntaxHighlighting.scala │ │ ├── UsingDirectives.scala │ │ ├── package.scala │ │ ├── scripting │ │ ├── NonForkingScriptRunner.scala │ │ ├── ScriptRunner.scala │ │ ├── ScriptingDriver.scala │ │ └── WrapForMainArgs.scala │ │ └── util │ │ ├── Cache.scala │ │ ├── ClasspathHelper.scala │ │ ├── SimpleDriver.scala │ │ └── package.scala │ └── test │ ├── resources │ ├── demo-project │ │ ├── build.sbt │ │ ├── plus.sc │ │ ├── project │ │ │ ├── build.properties │ │ │ └── plugins.sbt │ │ ├── src │ │ │ └── main │ │ │ │ └── scala │ │ │ │ └── stringcalc │ │ │ │ ├── Domain.scala │ │ │ │ └── Main.scala │ │ └── stringcalc │ └── invalid-ansi.txt │ └── scala │ └── replpp │ ├── ConfigTests.scala │ ├── DependenciesTests.scala │ ├── OperatorsTests.scala │ ├── PPrinterTests.scala │ ├── UsingDirectivesTests.scala │ ├── scripting │ └── ScriptRunnerTests.scala │ └── util │ ├── CacheTests.scala │ ├── ClasspathHelperTests.scala │ └── ProjectRoot.scala ├── demo.cast ├── demo.gif ├── demo.txt ├── integration-tests └── src │ └── test │ └── scala │ └── replpp │ └── server │ └── EmbeddedReplTests.scala ├── project ├── Build.scala ├── build.properties └── plugins.sbt ├── server └── src │ ├── main │ └── scala │ │ └── replpp │ │ └── server │ │ ├── Config.scala │ │ ├── EmbeddedRepl.scala │ │ ├── Main.scala │ │ ├── ReplServer.scala │ │ └── WebServiceWithWebSocket.scala │ └── test │ └── scala │ └── replpp │ └── server │ ├── ConfigTests.scala │ └── ReplServerTests.scala ├── shaded-libs ├── import-instructions.md └── src │ └── main │ └── scala │ └── replpp │ └── shaded │ ├── coursier │ └── core │ │ └── Version.scala │ ├── fansi │ ├── Fansi.scala │ └── LICENSE │ ├── geny │ ├── ByteData.scala │ ├── Bytes.scala │ ├── Generator.scala │ ├── Internal.scala │ └── Writable.scala │ ├── mainargs │ ├── Annotations.scala │ ├── Compat.scala │ ├── Flag.scala │ ├── Invoker.scala │ ├── LICENSE │ ├── Leftover.scala │ ├── Macros.scala │ ├── Parser.scala │ ├── ParserForClassCompanionVersionSpecific.scala │ ├── ParserForMethodsCompanionVersionSpecific.scala │ ├── Renderer.scala │ ├── Result.scala │ ├── TokenGrouping.scala │ ├── TokensReader.scala │ ├── Util.scala │ └── acyclic.scala │ ├── os │ ├── GeneratedTupleConversions.scala │ ├── GlobInterpolator.scala │ ├── Internals.scala │ ├── LICENSE │ ├── Macros.scala │ ├── Model.scala │ ├── Path.scala │ ├── ProcessOps.scala │ ├── ResourcePath.scala │ ├── Source.scala │ ├── SubProcess.scala │ └── package.scala │ ├── pprint │ ├── LICENSE │ ├── PPrinter.scala │ ├── ProductSupport.scala │ ├── Renderer.scala │ ├── Result.scala │ ├── StringPrefix.scala │ ├── TPrint.scala │ ├── TPrintImpl.scala │ ├── Truncated.scala │ ├── Util.scala │ ├── Walker.scala │ ├── package.scala │ └── scala │ │ └── collection │ │ └── internal │ │ └── pprint │ │ └── CollectionName.scala │ ├── scopt │ ├── LICENSE.md │ ├── OEffect.scala │ ├── OParser.scala │ ├── OParserSetup.scala │ ├── ORunner.scala │ ├── OptionDef.scala │ ├── OptionParser.scala │ ├── PlatformReadInstances.scala │ ├── Read.scala │ ├── RenderingMode.scala │ └── Validation.scala │ └── sourcecode │ ├── LICENSE │ ├── Macros.scala │ └── SourceContext.scala ├── srp ├── srp-server ├── srp-server.bat ├── srp.bat └── test-scripts ├── main-requiring-import.sc ├── main-using-import.sc ├── multiple-mains.sc ├── runBeforeCode-main.sc ├── runBeforeCode.sc └── sayHello-function.sc /.github/workflows/bootstrap.yml: -------------------------------------------------------------------------------- 1 | name: bootstrap 2 | 3 | on: 4 | workflow_dispatch: # only manually trigger this workflow 5 | inputs: 6 | version: 7 | description: 'version (git tag without the `v` prefix)' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: coursier/setup-action@v1 16 | with: 17 | jvm: temurin:19 18 | - name: cs bootstrap 19 | run: cs bootstrap com.michaelpollmeier::scala-repl-pp:${{ inputs.version }} -o srp 20 | - name: Release 21 | uses: softprops/action-gh-release@v1 22 | with: 23 | name: ${{ inputs.version }} 24 | tag_name: ${{ inputs.version }} 25 | files: srp 26 | fail_on_unmatched_files: true 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ["*"] 6 | jobs: 7 | main: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | - name: Setup JDK 14 | uses: actions/setup-java@v4 15 | with: 16 | distribution: temurin 17 | java-version: 21 18 | cache: sbt 19 | - uses: sbt/setup-sbt@v1 20 | - name: Build and test 21 | shell: bash 22 | run: sbt -v clean test 23 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | on: pull_request 3 | jobs: 4 | pr: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [ubuntu-latest, windows-latest, macos-latest] 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 1 13 | - name: Setup JDK 14 | uses: actions/setup-java@v4 15 | with: 16 | distribution: temurin 17 | java-version: 21 18 | cache: sbt 19 | - uses: sbt/setup-sbt@v1 20 | - name: Build and test 21 | shell: bash 22 | run: sbt -v test 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | concurrency: release 3 | on: 4 | workflow_dispatch: # manually trigger this workflow 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: write 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Setup JDK 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: temurin 18 | java-version: 21 19 | cache: sbt 20 | - uses: sbt/setup-sbt@v1 21 | - run: sudo apt update && sudo apt install -y gnupg 22 | - run: echo $PGP_SECRET | base64 --decode | gpg --batch --import 23 | env: 24 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 25 | - name: Build, test and tag 26 | shell: bash 27 | run: sbt -v clean test ciReleaseTagNextVersion 28 | - name: Export ENV vars 29 | run: | 30 | echo "LATEST_TAG=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV 31 | echo $GITHUB_ENV 32 | - name: Package core (will be uploaded to github release) 33 | shell: bash 34 | run: sbt -v releasePackage 35 | - name: Create github release and upload package 36 | uses: ncipollo/release-action@v1 37 | with: 38 | tag: ${{ env.LATEST_TAG }} 39 | artifacts: "target/srp.zip" # same as in `releasePackage` in build.sbt 40 | artifactErrorsFailBuild: true 41 | - name: Deploy to sonatype and release to maven central 42 | shell: bash 43 | run: sbt -v ciReleaseSonatype 44 | env: 45 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 46 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | /.bsp 4 | /gnupg-* 5 | /src/test/resources/demo-project/.bsp/ 6 | /core/src/test/resources/demo-project/.bsp/ 7 | /*.sc 8 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "scala-repl-pp-root" 2 | ThisBuild/organization := "com.michaelpollmeier" 3 | publish/skip := true 4 | 5 | val scalaLTSVersion = "3.3.6" 6 | val scalaVersions = Seq(scalaLTSVersion, "3.6.4", "3.7.0") 7 | ThisBuild/scalaVersion := scalaVersions.max 8 | lazy val Slf4jVersion = "2.0.16" 9 | 10 | lazy val releasePackage = taskKey[File]("package up a downloadable release") 11 | releasePackage := { 12 | // same as in `.github/workflows/release.yml` 13 | val releaseFile = target.value / "srp.zip" 14 | IO.copyFile((core_370/Universal/packageBin).value, releaseFile) 15 | streams.value.log.info(s"packaged up a release in $releaseFile") 16 | releaseFile 17 | } 18 | 19 | lazy val core_370 = Build 20 | .newProject("core", "3.7.0", "scala-repl-pp") 21 | .dependsOn(shadedLibs) 22 | .enablePlugins(JavaAppPackaging) 23 | .settings(coreSettings) 24 | 25 | lazy val core_364 = Build 26 | .newProject("core", "3.6.4", "scala-repl-pp") 27 | .dependsOn(shadedLibs) 28 | .enablePlugins(JavaAppPackaging) 29 | .settings(coreSettings) 30 | 31 | // Scala LTS version, only replace with next LTS release 32 | lazy val core_336 = Build 33 | .newProject("core", "3.3.6", "scala-repl-pp") 34 | .dependsOn(shadedLibs) 35 | .enablePlugins(JavaAppPackaging) 36 | .settings(coreSettings) 37 | 38 | lazy val coreSettings = commonSettings ++ Seq( 39 | Compile/mainClass := Some("replpp.Main"), 40 | executableScriptName := "srp", 41 | Universal/topLevelDirectory := Some("srp"), 42 | libraryDependencies += "org.scala-lang" %% "scala3-compiler" % scalaVersion.value, 43 | ) 44 | 45 | lazy val shadedLibs = project.in(file("shaded-libs")).settings( 46 | name := "scala-repl-pp-shaded-libs", 47 | scalaVersion := scalaVersions.min, 48 | Compile/compile/scalacOptions ++= Seq( 49 | "-language:implicitConversions", 50 | "-Wconf:any:silent", // silence warnings from shaded libraries 51 | "-explain" 52 | ), 53 | Compile/doc/scalacOptions += "-nowarn", 54 | commonSettings, 55 | ) 56 | 57 | lazy val server_370 = Build 58 | .newProject("server", "3.7.0", "scala-repl-pp-server") 59 | .dependsOn(core_370) 60 | .enablePlugins(JavaAppPackaging) 61 | .settings(serverSettings) 62 | 63 | lazy val server_364 = Build 64 | .newProject("server", "3.6.4", "scala-repl-pp-server") 65 | .dependsOn(core_364) 66 | .enablePlugins(JavaAppPackaging) 67 | .settings(serverSettings) 68 | 69 | lazy val server_336 = Build 70 | .newProject("server", "3.3.6", "scala-repl-pp-server") 71 | .dependsOn(core_336) 72 | .enablePlugins(JavaAppPackaging) 73 | .settings(serverSettings) 74 | 75 | lazy val serverSettings = commonSettings ++ Seq( 76 | Compile/mainClass := Some("replpp.server.Main"), 77 | libraryDependencies ++= Seq( 78 | "com.lihaoyi" %% "cask" % "0.9.5", 79 | "org.slf4j" % "slf4j-api" % Slf4jVersion, 80 | "org.slf4j" % "slf4j-simple" % Slf4jVersion % Optional, 81 | "com.lihaoyi" %% "requests" % "0.8.2" % Test, 82 | ), 83 | executableScriptName := "srp-server", 84 | fork := true, // important: otherwise we run into classloader issues 85 | ) 86 | 87 | lazy val integrationTests = project.in(file("integration-tests")) 88 | .dependsOn(server_370) 89 | .settings( 90 | name := "integration-tests", 91 | fork := true, // important: otherwise we run into classloader issues 92 | libraryDependencies += "org.slf4j" % "slf4j-simple" % Slf4jVersion % Optional, 93 | publish/skip := true 94 | ) 95 | 96 | lazy val commonSettings = Seq(maintainer.withRank(KeyRanks.Invisible) := "michael@michaelpollmeier.com") 97 | 98 | ThisBuild/libraryDependencies ++= Seq( 99 | "org.scalatest" %% "scalatest" % "3.2.19" % Test, 100 | "com.lihaoyi" %% "os-lib" % "0.9.1" % Test, 101 | ) 102 | 103 | ThisBuild/versionScheme := Some("strict") 104 | 105 | ThisBuild/javacOptions ++= Seq( 106 | "-g", //debug symbols 107 | "--release", "11" 108 | ) 109 | 110 | ThisBuild/scalacOptions ++= Seq( 111 | "-release", "11", 112 | "-deprecation", 113 | "-feature", 114 | ) 115 | 116 | ThisBuild/Test/fork := false 117 | 118 | ThisBuild/publishTo := sonatypePublishToBundle.value 119 | ThisBuild/scmInfo := Some(ScmInfo(url("https://github.com/mpollmeier/scala-repl-pp"), 120 | "scm:git@github.com:mpollmeier/scala-repl-pp.git")) 121 | ThisBuild/homepage := Some(url("https://github.com/mpollmeier/scala-repl-pp/")) 122 | ThisBuild/licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")) 123 | ThisBuild/developers := List( 124 | Developer("mpollmeier", "Michael Pollmeier", "michael@michaelpollmeier.com", url("http://www.michaelpollmeier.com/")) 125 | ) 126 | 127 | Global/onChangedBuildSource := ReloadOnSourceChanges 128 | -------------------------------------------------------------------------------- /core/src/main/resources/NOTICE.md: -------------------------------------------------------------------------------- 1 | Dotty (https://dotty.epfl.ch) 2 | Copyright 2012-2023 EPFL 3 | Copyright 2012-2023 Lightbend, Inc. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"): 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | 14 | The dotty compiler frontend has been developed since November 2012 by Martin 15 | Odersky. It is expected and hoped for that the list of contributors to the 16 | codebase will grow quickly. Dotty draws inspiration and code from the original 17 | Scala compiler "nsc", which is developed at scala/scala [1]. 18 | 19 | The majority of the dotty codebase is new code, with the exception of the 20 | components mentioned below. We have for each component tried to come up with a 21 | list of the original authors in the scala/scala [1] codebase. Apologies if some 22 | major authors were omitted by oversight. 23 | 24 | * dotty.tools.dotc.ast: The syntax tree handling is mostly new, but some 25 | elements, such as the idea of tree copiers and the `TreeInfo` module, 26 | were adopted from scala/scala [1]. The original authors of these parts 27 | include Martin Odersky, Paul Phillips, Adriaan Moors, and Matthias Zenger. 28 | 29 | * dotty.tools.dotc.classpath: The classpath handling is taken mostly as is 30 | from scala/scala [1]. The original authors were Grzegorz Kossakowski, 31 | Michał Pociecha, Lukas Rytz, Jason Zaugg and others. 32 | 33 | * dotty.tools.dotc.config: The configuration components were adapted and 34 | extended from scala/scala [1]. The original sources were authored by Paul 35 | Phillips with contributions from Martin Odersky, Miguel Garcia and others. 36 | 37 | * dotty.tools.dotc.core: The core data structures and operations are mostly 38 | new. Some parts (e.g. those dealing with names) were adapted from 39 | scala/scala [1]. These were originally authored by Martin Odersky, Adriaan 40 | Moors, Jason Zaugg, Paul Phillips, Eugene Burmako and others. 41 | 42 | * dotty.tools.dotc.core.pickling: The classfile readers were adapted from the 43 | current Scala compiler. Original authors were Martin Odersky, Iulian 44 | Dragos, Matthias Zenger and others. 45 | 46 | * dotty.tools.dotc.parsing: The lexical and syntactic analysis components 47 | were adapted from the current Scala compiler. They were originally authored 48 | by Martin Odersky, Burak Emir, Paul Phillips, Lex Spoon, Sean McDirmid and 49 | others. 50 | 51 | * dotty.tools.dotc.profile: The per-phase profiling support is taken mostly 52 | as is from scala/scala. The original author was Mike Skells. 53 | 54 | * dotty.tools.dotc.reporting: Adapted from scala/scala [1] with some heavy 55 | modifications. They were originally authored by Matthias Zenger, Martin 56 | Odersky, and others. 57 | 58 | * dotty.tools.dotc.typer: This is new code except for some minor components 59 | (e.g. the ConstantFolder). It uses however many solution details that have 60 | been developed over time by many people, including Jason Zaugg, Adriaan 61 | Moors, Lukas Rytz, Paul Phillips, Grzegorz Kossakowski, and others. 62 | 63 | * dotty.tools.dotc.util: The utilities package is a mix of new and adapted 64 | components. The files in scala/scala [1] were originally authored by many 65 | people, including Paul Phillips, Martin Odersky, Sean McDirmid, and others. 66 | 67 | * dotty.tools.io: The I/O support library was adapted from current Scala 68 | compiler. Original authors were Paul Phillips and others. 69 | 70 | * dotty.test.DottyBytecodeTest: Is an adaptation of the bytecode testing from 71 | scala/scala [1]. It has been reworked to fit the needs of dotty. Original 72 | authors include: Adrian Moors, Lukas Rytz, Grzegorz Kossakowski, Paul 73 | Phillips. 74 | 75 | * dotty.tools.dotc.sbt and everything in sbt-bridge: The sbt compiler phases 76 | are based on [2] which attempts to integrate the sbt phases into scalac and 77 | is itself based on the compiler bridge in sbt 0.13 [3], but has been 78 | heavily adapted and refactored. Original authors were Mark Harrah, Grzegorz 79 | Kossakowski, Martin Duhemm, Adriaan Moors and others. 80 | 81 | * dotty.tools.dotc.plugins: Adapted from scala/scala [1] with some 82 | modifications. They were originally authored by Lex Spoon, Som Snytt, 83 | Adriaan Moors, Paul Phillips and others. 84 | 85 | * dotty.tools.scaladoc: The Scaladoc documentation utility ships some 86 | third-party JavaScript and CSS libraries which are located under 87 | scaladoc/resources/dotty_res/styles/, scaladoc/resources/dotty_res/scripts/, docs/css/ and 88 | docs/js/. Please refer to the license header of the concerned files for 89 | details. 90 | 91 | * dotty.tools.dotc.coverage: Coverage instrumentation utilities have been 92 | adapted from the scoverage plugin for scala 2 [5], which is under the 93 | Apache 2.0 license. 94 | 95 | * The Dotty codebase contains parts which are derived from 96 | the ScalaPB protobuf library [4], which is under the Apache 2.0 license. 97 | 98 | 99 | [1] https://github.com/scala/scala 100 | [2] https://github.com/adriaanm/scala/tree/sbt-api-consolidate/src/compiler/scala/tools/sbt 101 | [3] https://github.com/sbt/sbt/tree/0.13/compile/interface/src/main/scala/xsbt 102 | [4] https://github.com/lampepfl/dotty/pull/5783/files 103 | [5] https://github.com/scoverage/scalac-scoverage-plugin 104 | -------------------------------------------------------------------------------- /core/src/main/scala-3.3.6/replpp/DottyRandomStuff.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import dotty.tools.dotc.reporting.{HideNonSensicalMessages, StoreReporter, UniqueMessagePositions} 4 | import dotty.tools.repl.* 5 | 6 | /** random code that I needed to copy over from dotty to make things work, usually because it was `private[repl]` 7 | */ 8 | private[replpp] object DottyRandomStuff { 9 | 10 | /** Create empty outer store reporter 11 | * copied from https://github.com/lampepfl/dotty/blob/3.3.6/compiler/src/dotty/tools/repl/package.scala#L6 */ 12 | def newStoreReporter: StoreReporter = { 13 | new StoreReporter(null) with UniqueMessagePositions with HideNonSensicalMessages 14 | } 15 | 16 | /** Based on https://github.com/scala/scala3/blob/3.3.6/compiler/src/dotty/tools/repl/ParseResult.scala#L135 17 | * change: removed [private] classifier so we can access it... 18 | * alternatively we could use reflection... 19 | */ 20 | object ParseResult { 21 | val commands: List[(String, String => ParseResult)] = List( 22 | Quit.command -> (_ => Quit), 23 | Quit.alias -> (_ => Quit), 24 | Help.command -> (_ => Help), 25 | Reset.command -> (arg => Reset(arg)), 26 | Imports.command -> (_ => Imports), 27 | Load.command -> (arg => Load(arg)), 28 | TypeOf.command -> (arg => TypeOf(arg)), 29 | DocOf.command -> (arg => DocOf(arg)), 30 | Settings.command -> (arg => Settings(arg)), 31 | Silent.command -> (_ => Silent), 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/scala-3.3.6/replpp/InteractiveShell.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import dotty.tools.repl.State 4 | 5 | import scala.util.control.NoStackTrace 6 | 7 | object InteractiveShell { 8 | 9 | def run(config: Config): Unit = { 10 | import config.colors 11 | val config0 = precompilePredefFiles(config) 12 | val compilerArgs = replpp.compilerArgs(config0) 13 | val verbose = verboseEnabled(config) 14 | 15 | val replDriver = new ReplDriver( 16 | compilerArgs, 17 | greeting = config0.greeting, 18 | prompt = config0.prompt.getOrElse("scala"), 19 | maxHeight = config0.maxHeight, 20 | runAfter = config0.runAfter, 21 | verbose = verbose 22 | ) 23 | 24 | if (verbose) println(s"compiler arguments: ${compilerArgs.mkString(",")}") 25 | 26 | var state: State = replDriver.initialState 27 | var expectedStateObjectIndex = 0 28 | Seq(DefaultRunBeforeLines, globalRunBeforeLines, config.runBefore).foreach { runBeforeLines => 29 | val runBeforeCode = runBeforeLines.mkString("\n").trim 30 | if (runBeforeCode.nonEmpty) { 31 | expectedStateObjectIndex += 1 32 | state = 33 | if (verbose) { 34 | println(s"executing runBeforeCode: $runBeforeCode") 35 | replDriver.run(runBeforeCode)(using state) 36 | } else { 37 | replDriver.runQuietly(runBeforeCode)(using state) 38 | } 39 | } 40 | } 41 | 42 | assert( 43 | state.objectIndex == expectedStateObjectIndex, 44 | s"compilation error(s) for predef code - see error above ^^^" 45 | ) 46 | 47 | replDriver.runUntilQuit(using state)() 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/scala-3.6.4/replpp/DottyRandomStuff.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import dotty.tools.dotc.reporting.{HideNonSensicalMessages, StoreReporter, UniqueMessagePositions} 4 | import dotty.tools.repl.* 5 | 6 | /** random code that I needed to copy over from dotty to make things work, usually because it was `private[repl]` 7 | */ 8 | private[replpp] object DottyRandomStuff { 9 | 10 | /** Create empty outer store reporter 11 | * copied from https://github.com/lampepfl/dotty/blob/3.3.0-RC5/compiler/src/dotty/tools/repl/package.scala#L6 */ 12 | def newStoreReporter: StoreReporter = { 13 | new StoreReporter(null) with UniqueMessagePositions with HideNonSensicalMessages 14 | } 15 | 16 | /** Based on https://github.com/scala/scala3/blob/3.6.4/compiler/src/dotty/tools/repl/ParseResult.scala#L135 17 | * change: removed [private] classifier so we can access it... 18 | * alternatively we could use reflection... 19 | */ 20 | object ParseResult { 21 | val commands: List[(String, String => ParseResult)] = List( 22 | Quit.command -> (_ => Quit), 23 | Quit.alias -> (_ => Quit), 24 | Help.command -> (_ => Help), 25 | Reset.command -> (arg => Reset(arg)), 26 | Imports.command -> (_ => Imports), 27 | Load.command -> (arg => Load(arg)), 28 | TypeOf.command -> (arg => TypeOf(arg)), 29 | DocOf.command -> (arg => DocOf(arg)), 30 | Settings.command -> (arg => Settings(arg)), 31 | Silent.command -> (_ => Silent), 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/scala-3.6.4/replpp/InteractiveShell.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import dotty.tools.repl.State 4 | 5 | import scala.util.control.NoStackTrace 6 | 7 | object InteractiveShell { 8 | 9 | def run(config: Config): Unit = { 10 | import config.colors 11 | val config0 = precompilePredefFiles(config) 12 | val compilerArgs = replpp.compilerArgs(config0) 13 | val verbose = verboseEnabled(config) 14 | 15 | val replDriver = new ReplDriver( 16 | compilerArgs, 17 | greeting = config0.greeting, 18 | prompt = config0.prompt.getOrElse("scala"), 19 | maxHeight = config0.maxHeight, 20 | runAfter = config0.runAfter, 21 | verbose = verbose 22 | ) 23 | 24 | if (verbose) println(s"compiler arguments: ${compilerArgs.mkString(",")}") 25 | 26 | var state: State = replDriver.initialState.copy(quiet = !verbose) 27 | var expectedStateObjectIndex = 0 28 | Seq(DefaultRunBeforeLines, globalRunBeforeLines, config.runBefore).foreach { runBeforeLines => 29 | val runBeforeCode = runBeforeLines.mkString("\n").trim 30 | if (runBeforeCode.nonEmpty) { 31 | expectedStateObjectIndex += 1 32 | if (verbose) println(s"executing runBeforeCode: $runBeforeCode") 33 | state = replDriver.run(runBeforeCode)(using state) 34 | } 35 | } 36 | 37 | assert( 38 | state.objectIndex == expectedStateObjectIndex, 39 | s"compilation error(s) for predef code - see error above ^^^" 40 | ) 41 | 42 | state = state.copy(quiet = false) 43 | replDriver.runUntilQuit(using state)() 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/scala-3.7.0/replpp/DottyRandomStuff.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import dotty.tools.dotc.reporting.{HideNonSensicalMessages, StoreReporter, UniqueMessagePositions} 4 | import dotty.tools.repl.* 5 | 6 | private[replpp] object DottyRandomStuff { 7 | 8 | /** Create empty outer store reporter - copied from 9 | * https://github.com/scala/scala3/blob/c92e20e6be2117365361abfd0b7e6cb72720d5db/compiler/src/dotty/tools/repl/package.scala#L7 10 | * only change: removed [private] classifier so we can access it... 11 | */ 12 | def newStoreReporter: StoreReporter = { 13 | new StoreReporter(null) with UniqueMessagePositions with HideNonSensicalMessages 14 | } 15 | 16 | /** copied from https://github.com/scala/scala3/blob/3.7.0/compiler/src/dotty/tools/repl/ParseResult.scala#L162 17 | * only change: removed [private] classifier so we can access it... 18 | * alternatively we could use reflection... 19 | */ 20 | object ParseResult { 21 | val commands: List[(String, String => ParseResult)] = List( 22 | Quit.command -> (_ => Quit), 23 | Quit.alias -> (_ => Quit), 24 | Help.command -> (_ => Help), 25 | Reset.command -> (arg => Reset(arg)), 26 | Imports.command -> (_ => Imports), 27 | JarCmd.command -> (arg => JarCmd(arg)), 28 | KindOf.command -> (arg => KindOf(arg)), 29 | Load.command -> (arg => Load(arg)), 30 | Require.command -> (arg => Require(arg)), 31 | TypeOf.command -> (arg => TypeOf(arg)), 32 | DocOf.command -> (arg => DocOf(arg)), 33 | Settings.command -> (arg => Settings(arg)), 34 | Sh.command -> (arg => Sh(arg)), 35 | Silent.command -> (_ => Silent), 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/scala-3.7.0/replpp/InteractiveShell.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import dotty.tools.repl.State 4 | 5 | import scala.util.control.NoStackTrace 6 | 7 | object InteractiveShell { 8 | 9 | def run(config: Config): Unit = { 10 | import config.colors 11 | val config0 = precompilePredefFiles(config) 12 | val compilerArgs = replpp.compilerArgs(config0) 13 | val verbose = verboseEnabled(config) 14 | 15 | val replDriver = new ReplDriver( 16 | compilerArgs, 17 | greeting = config0.greeting, 18 | prompt = config0.prompt.getOrElse("scala"), 19 | maxHeight = config0.maxHeight, 20 | runAfter = config0.runAfter, 21 | verbose = verbose 22 | ) 23 | 24 | if (verbose) println(s"compiler arguments: ${compilerArgs.mkString(",")}") 25 | 26 | var state: State = replDriver.initialState.copy(quiet = !verbose) 27 | var expectedStateObjectIndex = 0 28 | Seq(DefaultRunBeforeLines, globalRunBeforeLines, config.runBefore).foreach { runBeforeLines => 29 | val runBeforeCode = runBeforeLines.mkString("\n").trim 30 | if (runBeforeCode.nonEmpty) { 31 | expectedStateObjectIndex += 1 32 | if (verbose) println(s"executing runBeforeCode: $runBeforeCode") 33 | state = replDriver.run(runBeforeCode)(using state) 34 | } 35 | } 36 | 37 | assert( 38 | state.objectIndex == expectedStateObjectIndex, 39 | s"compilation error(s) for predef code - see error above ^^^" 40 | ) 41 | 42 | state = state.copy(quiet = false) 43 | replDriver.runUntilQuit(using state)() 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/Dependencies.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import replpp.shaded.os 4 | import replpp.util.Cache 5 | 6 | import java.net.URI 7 | import java.nio.file.{Path, Paths} 8 | import scala.util.{Failure, Success, Try} 9 | 10 | object Dependencies { 11 | 12 | private val CoursierJarDownloadUrl = new URI("https://github.com/coursier/launchers/raw/master/coursier") 13 | 14 | def resolve(coordinates: Seq[String], additionalRepositories: Seq[String] = Nil, verbose: Boolean = false): Try[Seq[Path]] = { 15 | if (coordinates.isEmpty) { 16 | Try(Seq.empty) 17 | } else { 18 | resolve0(coordinates, additionalRepositories, verbose) 19 | } 20 | } 21 | 22 | private def resolve0(coordinates: Seq[String], additionalRepositories: Seq[String], verbose: Boolean): Try[Seq[Path]] = { 23 | val coursierJarPath = Cache.getOrDownload("coursier.jar", CoursierJarDownloadUrl) 24 | val repositoryArgs = additionalRepositories.flatMap { repo => 25 | Seq("--repository", repo) 26 | } 27 | val command = Seq("java", "-jar", util.pathAsString(coursierJarPath), "fetch") ++ repositoryArgs ++ coordinates 28 | if (verbose) println(s"executing `${command.mkString(" ")}`") 29 | 30 | Try(os.proc(command).call()) match { 31 | case Success(commandResult) => 32 | Success(commandResult.out.text().split(System.lineSeparator()).map(Paths.get(_)).toIndexedSeq) 33 | case Failure(exception) => 34 | Failure(new AssertionError(s"${getClass.getName}: error while invoking `${command.mkString(" ")}`", exception)) 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/Main.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import replpp.scripting.ScriptRunner 4 | 5 | object Main { 6 | def main(args: Array[String]): Unit = 7 | run(Config.parse(args)) 8 | 9 | def run(config: Config): Unit = { 10 | if (config.scriptFile.isDefined) { 11 | ScriptRunner.exec(config).get 12 | } else { 13 | InteractiveShell.run(config) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/PPrinter.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import replpp.shaded.fansi 4 | import replpp.shaded.pprint 5 | import replpp.shaded.pprint.{PPrinter, Renderer, Result, Tree, Truncated} 6 | 7 | object PPrinter { 8 | private var pprinter: pprint.PPrinter = null 9 | private var maxHeight: Int = Int.MaxValue 10 | private var nocolors: Boolean = false 11 | 12 | def apply(objectToRender: Any, maxHeight: Int = Int.MaxValue, nocolors: Boolean = false): String = { 13 | val _pprinter = this.synchronized { 14 | // initialise on first use and whenever the maxHeight setting changed 15 | if (pprinter == null || this.maxHeight != maxHeight || this.nocolors != nocolors) { 16 | pprinter = create(maxHeight, nocolors) 17 | this.maxHeight = maxHeight 18 | this.nocolors = nocolors 19 | } 20 | pprinter 21 | } 22 | _pprinter.apply(objectToRender).render 23 | } 24 | 25 | private def create(maxHeight: Int, nocolors: Boolean): pprint.PPrinter = { 26 | val (colorLiteral, colorApplyPrefix) = 27 | if (nocolors) (fansi.Attrs.Empty, fansi.Attrs.Empty) 28 | else (fansi.Color.Green, fansi.Color.Yellow) 29 | 30 | new pprint.PPrinter( 31 | defaultHeight = maxHeight, 32 | colorLiteral = colorLiteral, 33 | colorApplyPrefix = colorApplyPrefix) { 34 | 35 | override def tokenize(x: Any, 36 | width: Int = defaultWidth, 37 | height: Int = defaultHeight, 38 | indent: Int = defaultIndent, 39 | initialOffset: Int = 0, 40 | escapeUnicode: Boolean, 41 | showFieldNames: Boolean): Iterator[fansi.Str] = { 42 | val tree = this.treeify(x, escapeUnicode = escapeUnicode, showFieldNames = showFieldNames) 43 | val renderer = new Renderer(width, this.colorApplyPrefix, this.colorLiteral, indent) { 44 | override def rec(x: Tree, leftOffset: Int, indentCount: Int): Result = x match { 45 | case Tree.Literal(body) if isAnsiEncoded(body) => 46 | // this is the part we're overriding, everything else is just boilerplate 47 | Result.fromString(fixForFansi(body)) 48 | case _ => super.rec(x, leftOffset, indentCount) 49 | } 50 | } 51 | val rendered = renderer.rec(tree, initialOffset, 0).iter 52 | new Truncated(rendered, width, height) 53 | } 54 | } 55 | } 56 | 57 | def isAnsiEncoded(string: String): Boolean = 58 | string.exists(c => c == '\u001b' || c == '\u009b') 59 | 60 | /** We use source-highlight to encode source as ansi strings, e.g. the .dump step Ammonite uses fansi for it's 61 | * colour-coding, and while both pledge to follow the ansi codec, they aren't compatible TODO: PR for fansi to 62 | * support these standard encodings out of the box 63 | */ 64 | def fixForFansi(ansiEncoded: String): fansi.Str = { 65 | val sanitized = ansiEncoded 66 | .replaceAll("\u001b\\[m", "\u001b[39m") // encoding ends with [39m for fansi instead of [m 67 | .replaceAll("\u001b\\[0(\\d)m", "\u001b[$1m") // `[01m` is encoded as `[1m` in fansi for all single digit numbers 68 | .replaceAll("\u001b\\[0?(\\d+);0?(\\d+)m", "\u001b[$1m\u001b[$2m") // `[01;34m` is encoded as `[1m[34m` in fansi 69 | .replaceAll( 70 | "\u001b\\[[00]+;0?(\\d+);0?(\\d+);0?(\\d+)m", 71 | "\u001b[$1;$2;$3m" 72 | ) // `[00;38;05;70m` is encoded as `[38;5;70m` in fansi - 8bit color encoding 73 | 74 | fansi.Str(sanitized, errorMode = fansi.ErrorMode.Sanitize) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/ReplDriver.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import dotty.tools.dotc.core.Contexts 4 | import dotty.tools.dotc.core.Contexts.Context 5 | import dotty.tools.repl.* 6 | import dotty.tools.repl.Rendering.showUser 7 | import org.jline.reader.* 8 | 9 | import java.io.PrintStream 10 | import scala.annotation.tailrec 11 | import scala.jdk.CollectionConverters.* 12 | import scala.util.{Failure, Success, Try} 13 | 14 | class ReplDriver(compilerArgs: Array[String], 15 | out: PrintStream = scala.Console.out, 16 | greeting: Option[String], 17 | prompt: String, 18 | maxHeight: Option[Int] = None, 19 | classLoader: Option[ClassLoader] = None, 20 | runAfter: Seq[String] = Nil, 21 | verbose: Boolean = false)(using Colors) 22 | extends ReplDriverBase(compilerArgs, out, maxHeight, classLoader) { 23 | 24 | /** Run REPL with `state` until `:quit` command found 25 | * Main difference to the 'original': different greeting, trap Ctrl-c 26 | */ 27 | override def runUntilQuit(using initialState: State)(): State = { 28 | val terminal = new replpp.JLineTerminal { 29 | override protected def promptStr = prompt 30 | } 31 | greeting.foreach(out.println) 32 | 33 | @tailrec 34 | def loop(using state: State)(): State = { 35 | Try { 36 | val inputLines = readLine(terminal, state) 37 | interpretInput(inputLines, state, pwd) 38 | } match { 39 | case Success(newState) => 40 | loop(using newState)() 41 | case Failure(_: EndOfFileException) => 42 | // Ctrl+D -> user wants to quit 43 | runAfter.foreach { code => 44 | if (verbose) println(code) 45 | run(code)(using state) 46 | } 47 | state 48 | case Failure(_: UserInterruptException) => 49 | // Ctrl+C -> swallow, do nothing 50 | loop(using state)() 51 | case Failure(exception) => 52 | throw exception 53 | } 54 | } 55 | 56 | try runBody { 57 | loop(using initialState)() 58 | } 59 | finally terminal.close() 60 | } 61 | 62 | /** Blockingly read a line, getting back a parse result. 63 | * The input may be multi-line. 64 | * If the input contains a using file directive (e.g. `//> using file abc.sc`), then we interpret everything up 65 | * until the directive, then interpret the directive (i.e. import that file) and continue with the remainder of 66 | * our input. That way, we import the file in-place, while preserving line numbers for user feedback. */ 67 | private def readLine(terminal: replpp.JLineTerminal, state: State): IterableOnce[String] = { 68 | given Context = state.context 69 | val completer: Completer = { (lineReader, line, candidates) => 70 | def makeCandidate(label: String) = { 71 | new Candidate( 72 | /* value = */ label, 73 | /* displ = */ stripBackTicks(label), // displayed value 74 | /* group = */ null, // can be used to group completions together 75 | /* descr = */ null, // TODO use for documentation? 76 | /* suffix = */ null, 77 | /* key = */ null, 78 | /* complete = */ false // if true adds space when completing 79 | ) 80 | } 81 | val comps = completions(line.cursor, line.line, state) 82 | candidates.addAll(comps.map(_.label).distinct.map(makeCandidate).asJava) 83 | val lineWord = line.word() 84 | comps.filter(c => c.label == lineWord && c.symbols.nonEmpty) match 85 | case Nil => 86 | case exachMatches => 87 | val terminal = lineReader.nn.getTerminal 88 | lineReader.callWidget(LineReader.CLEAR) 89 | terminal.writer.println() 90 | exachMatches.foreach: exact => 91 | exact.symbols.foreach: sym => 92 | terminal.writer.println(SyntaxHighlighting.highlight(sym.showUser)) 93 | lineReader.callWidget(LineReader.REDRAW_LINE) 94 | lineReader.callWidget(LineReader.REDISPLAY) 95 | terminal.flush() 96 | } 97 | 98 | terminal.readLine(completer).linesIterator 99 | } 100 | 101 | private def stripBackTicks(label: String) = 102 | if label.startsWith("`") && label.endsWith("`") then 103 | label.drop(1).dropRight(1) 104 | else 105 | label 106 | 107 | } 108 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/ReplDriverBase.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import dotty.tools.repl.* 4 | import replpp.shaded.fansi 5 | 6 | import java.io.PrintStream 7 | import java.lang.System.lineSeparator 8 | import java.nio.file.{Files, Path} 9 | 10 | abstract class ReplDriverBase(compilerArgs: Array[String], 11 | out: PrintStream, 12 | maxHeight: Option[Int], 13 | classLoader: Option[ClassLoader])(using Colors) 14 | extends DottyReplDriver(compilerArgs, out, maxHeight, classLoader) { 15 | 16 | protected def interpretInput(lines: IterableOnce[String], state: State, currentFile: Path): State = { 17 | val parsedLines = Seq.newBuilder[String] 18 | var currentState = state 19 | 20 | def handleImportFileDirective(line: String) = { 21 | val linesBeforeUsingFileDirective = parsedLines.result() 22 | parsedLines.clear() 23 | if (linesBeforeUsingFileDirective.nonEmpty) { 24 | // interpret everything until here 25 | val parseResult = parseInput(linesBeforeUsingFileDirective, currentState) 26 | currentState = interpret(parseResult)(using currentState) 27 | } 28 | 29 | // now read and interpret the given file 30 | val pathStr = line.trim.drop(UsingDirectives.FileDirective.length) 31 | val path = resolveFile(currentFile, pathStr) 32 | if (Files.exists(path)) { 33 | val linesFromFile = util.linesFromFile(path) 34 | println(s"> importing $path (${linesFromFile.size} lines)") 35 | currentState = interpretInput(linesFromFile, currentState, path) 36 | } else { 37 | System.err.println(util.colorise(s"Warning: given file `$path` does not exist.", fansi.Color.Red)) 38 | } 39 | } 40 | 41 | for (line <- lines.iterator) { 42 | if (line.trim.startsWith(UsingDirectives.FileDirective)) 43 | handleImportFileDirective(line) 44 | else 45 | parsedLines.addOne(line) 46 | } 47 | 48 | val parseResult = parseInput(parsedLines.result(), currentState) 49 | interpret(parseResult)(using currentState) 50 | } 51 | 52 | private def parseInput(lines: IterableOnce[String], state: State): ParseResult = 53 | parseInput(lines.iterator.mkString(lineSeparator), state) 54 | 55 | private def parseInput(input: String, state: State): ParseResult = 56 | ParseResult(input)(using state) 57 | 58 | } 59 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/SyntaxHighlighting.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import scala.language.unsafeNulls 4 | 5 | import dotty.tools.dotc.CompilationUnit 6 | import dotty.tools.dotc.ast.untpd 7 | import dotty.tools.dotc.core.Contexts.* 8 | import dotty.tools.dotc.core.StdNames.* 9 | import dotty.tools.dotc.parsing.Parsers.Parser 10 | import dotty.tools.dotc.parsing.Scanners.Scanner 11 | import dotty.tools.dotc.parsing.Tokens.* 12 | import dotty.tools.dotc.reporting.Reporter 13 | import dotty.tools.dotc.util.Spans.Span 14 | import dotty.tools.dotc.util.SourceFile 15 | 16 | import java.util.Arrays 17 | 18 | /** Based on https://github.com/lampepfl/dotty/blob/3.4.1/compiler/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala 19 | * and adapted for our needs 20 | * 21 | * This object provides functions for syntax highlighting in the REPL */ 22 | object SyntaxHighlighting { 23 | 24 | /** if true, log erroneous positions being highlighted */ 25 | private inline val debug = true 26 | 27 | // Keep in sync with SyntaxHighlightingTests 28 | val NoColor: String = Console.RESET 29 | val CommentColor: String = Console.BLUE 30 | val KeywordColor: String = Console.YELLOW 31 | val ValDefColor: String = Console.CYAN 32 | val LiteralColor: String = Console.GREEN 33 | val TypeColor: String = Console.GREEN 34 | val AnnotationColor: String = Console.MAGENTA 35 | 36 | def highlight(in: String)(using Context): String = { 37 | def freshCtx = ctx.fresh.setReporter(Reporter.NoReporter) 38 | if (in.isEmpty || ctx.settings.color.value == "never") in 39 | else { 40 | val source = SourceFile.virtual("", in) 41 | 42 | given Context = freshCtx 43 | .setCompilationUnit(CompilationUnit(source, mustExist = false)(using freshCtx)) 44 | 45 | val colorAt = Array.fill(in.length)(NoColor) 46 | 47 | def highlightRange(from: Int, to: Int, color: String) = 48 | Arrays.fill(colorAt.asInstanceOf[Array[AnyRef]], from, to, color) 49 | 50 | def highlightPosition(span: Span, color: String) = if (span.exists) 51 | if (span.start < 0 || span.end > in.length) { 52 | if (debug) 53 | println(s"Trying to highlight erroneous position $span. Input size: ${in.length}") 54 | } 55 | else 56 | highlightRange(span.start, span.end, color) 57 | 58 | val scanner = new Scanner(source) 59 | while (scanner.token != EOF) { 60 | val start = scanner.offset 61 | val token = scanner.token 62 | val name = scanner.name 63 | val isSoftModifier = scanner.isSoftModifierInModifierPosition 64 | scanner.nextToken() 65 | val end = scanner.lastOffset 66 | 67 | // Branch order is important. For example, 68 | // `true` is at the same time a keyword and a literal 69 | token match { 70 | case _ if literalTokens.contains(token) => 71 | highlightRange(start, end, LiteralColor) 72 | 73 | case STRINGPART => 74 | // String interpolation parts include `$` but 75 | // we don't highlight it, hence the `-1` 76 | highlightRange(start, end - 1, LiteralColor) 77 | 78 | case _ if alphaKeywords.contains(token) || isSoftModifier => 79 | highlightRange(start, end, KeywordColor) 80 | 81 | case IDENTIFIER if name == nme.??? => 82 | highlightRange(start, end, Console.RED_B) 83 | 84 | case _ => 85 | } 86 | } 87 | 88 | for (comment <- scanner.comments) 89 | highlightPosition(comment.span, CommentColor) 90 | 91 | object TreeHighlighter extends untpd.UntypedTreeTraverser { 92 | import untpd.* 93 | 94 | def ignored(tree: NameTree) = { 95 | val name = tree.name.toTermName 96 | // trees named and have weird positions 97 | name == nme.ERROR || name == nme.CONSTRUCTOR 98 | } 99 | 100 | def highlightAnnotations(tree: MemberDef): Unit = { 101 | // TODO reimplement 102 | // for (annotation <- tree.rawMods.annotations) 103 | // highlightPosition(annotation.span, AnnotationColor) 104 | } 105 | 106 | def highlight(trees: List[Tree])(using Context): Unit = 107 | trees.foreach(traverse) 108 | 109 | def traverse(tree: Tree)(using Context): Unit = { 110 | tree match { 111 | case tree: NameTree if ignored(tree) => 112 | () 113 | case tree: ValOrDefDef => 114 | highlightAnnotations(tree) 115 | highlightPosition(tree.nameSpan, ValDefColor) 116 | highlightPosition(tree.endSpan, ValDefColor) 117 | case tree: MemberDef /* ModuleDef | TypeDef */ => 118 | highlightAnnotations(tree) 119 | highlightPosition(tree.nameSpan, TypeColor) 120 | highlightPosition(tree.endSpan, TypeColor) 121 | case tree: Ident if tree.isType => 122 | highlightPosition(tree.span, TypeColor) 123 | case _: TypeTree => 124 | highlightPosition(tree.span, TypeColor) 125 | case _ => 126 | } 127 | traverseChildren(tree) 128 | } 129 | } 130 | 131 | val parser = new Parser(source) 132 | val trees = parser.blockStatSeq() 133 | TreeHighlighter.highlight(trees) 134 | 135 | val highlighted = new StringBuilder() 136 | 137 | for (idx <- colorAt.indices) { 138 | val prev = if (idx == 0) NoColor else colorAt(idx - 1) 139 | val curr = colorAt(idx) 140 | if (curr != prev) 141 | highlighted.append(curr) 142 | highlighted.append(in(idx)) 143 | } 144 | 145 | if (colorAt.last != NoColor) 146 | highlighted.append(NoColor) 147 | 148 | highlighted.toString 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/UsingDirectives.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import java.nio.file.{Files, Path} 4 | import scala.collection.mutable 5 | 6 | object UsingDirectives { 7 | private val Prefix = "//> using" 8 | val LibDirective = s"$Prefix dep " 9 | val FileDirective = s"$Prefix file " 10 | val ResolverDirective = s"$Prefix resolver" 11 | val ClasspathDirective = s"$Prefix classpath" 12 | 13 | def findImportedFilesRecursively(path: Path, visited: Set[Path] = Set.empty): Seq[Path] = { 14 | val rootDir: Path = 15 | if (Files.isDirectory(path)) path 16 | else path.toAbsolutePath.getParent 17 | 18 | val importedFiles = findImportedFiles(util.linesFromFile(path), rootDir) 19 | val recursivelyImportedFiles = importedFiles.filterNot(visited.contains).flatMap { file => 20 | findImportedFilesRecursively(file, visited + file) 21 | } 22 | (importedFiles ++ recursivelyImportedFiles).distinct 23 | } 24 | 25 | def findImportedFiles(lines: IterableOnce[String], rootPath: Path): Seq[Path] = 26 | scanFor(FileDirective, lines).iterator.map(resolveFile(rootPath, _)).toSeq 27 | 28 | def findDeclaredDependencies(lines: IterableOnce[String]): Seq[String] = 29 | scanFor(LibDirective, lines) 30 | 31 | def findResolvers(lines: IterableOnce[String]): Seq[String] = 32 | scanFor(ResolverDirective, lines) 33 | 34 | def findClasspathEntriesInLines(sourceLines: IterableOnce[String], relativeTo: Path): Seq[Path] = { 35 | for { 36 | classpathEntry <- scanFor(ClasspathDirective, sourceLines) 37 | pathRelativeToDeclaringFile = relativeTo.resolve(classpathEntry) 38 | } yield pathRelativeToDeclaringFile 39 | } 40 | 41 | def findClasspathEntriesInFiles(files: IterableOnce[Path]): Seq[Path] = { 42 | files.iterator.flatMap { file => 43 | val dir = file.getParent 44 | findClasspathEntriesInLines(util.linesFromFile(file), dir) 45 | }.toSeq 46 | } 47 | 48 | private def scanFor(directive: String, lines: IterableOnce[String]): Seq[String] = { 49 | lines 50 | .iterator 51 | .map(_.trim) 52 | .filter(_.startsWith(directive)) 53 | .map(_.drop(directive.length).trim) 54 | .toSeq 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/package.scala: -------------------------------------------------------------------------------- 1 | import replpp.util.{ClasspathHelper, SimpleDriver, linesFromFile} 2 | 3 | import java.nio.file.{Files, Path, Paths} 4 | import scala.collection.mutable 5 | 6 | package object replpp { 7 | enum Colors { case BlackWhite, Default } 8 | val VerboseEnvVar = "SCALA_REPL_PP_VERBOSE" 9 | lazy val pwd: Path = Paths.get(".").toAbsolutePath 10 | lazy val home: Path = Paths.get(System.getProperty("user.home")) 11 | lazy val globalRunBeforeFile: Path = home.resolve(".scala-repl-pp.sc") 12 | lazy val globalRunBeforeFileMaybe: Option[Path] = Option(globalRunBeforeFile).filter(Files.exists(_)) 13 | lazy val globalRunBeforeLines: Seq[String] = globalRunBeforeFileMaybe.map(linesFromFile).getOrElse(Seq.empty) 14 | 15 | private[replpp] def DefaultRunBeforeLines(using colors: Colors) = { 16 | val colorsImport = colors match { 17 | case Colors.BlackWhite => "replpp.Colors.BlackWhite" 18 | case Colors.Default => "replpp.Colors.Default" 19 | } 20 | Seq( 21 | "import replpp.Operators.*", 22 | s"given replpp.Colors = $colorsImport", 23 | ) 24 | } 25 | 26 | /** verbose mode can either be enabled via the config, or the environment variable `SCALA_REPL_PP_VERBOSE=true` */ 27 | def verboseEnabled(config: Config): Boolean = { 28 | config.verbose || 29 | sys.env.get(VerboseEnvVar).getOrElse("false").toLowerCase.trim == "true" 30 | } 31 | 32 | def compilerArgs(config: Config): Array[String] = { 33 | val compilerArgs = Array.newBuilder[String] 34 | compilerArgs ++= Array("-classpath", ClasspathHelper.create(config)) 35 | compilerArgs += "-explain" // verbose scalac error messages 36 | compilerArgs += "-deprecation" 37 | if (config.nocolors) compilerArgs ++= Array("-color", "never") 38 | 39 | compilerArgs.result() 40 | } 41 | 42 | /** recursively find all relevant source files from main script, global predef file, 43 | * provided predef files, other scripts that were imported with `using file` directive */ 44 | def allSourceFiles(config: Config): Seq[Path] = 45 | (allPredefFiles(config) ++ config.scriptFile ++ globalRunBeforeFileMaybe).distinct.sorted 46 | 47 | def allPredefFiles(config: Config): Seq[Path] = { 48 | val allPredefFiles = mutable.Set.empty[Path] 49 | allPredefFiles ++= config.predefFiles 50 | 51 | // the directly referenced predef files might reference additional files via `using` directive 52 | val predefFilesDirect = allPredefFiles.toSet 53 | predefFilesDirect.foreach { predefFile => 54 | val importedFiles = UsingDirectives.findImportedFilesRecursively(predefFile, visited = allPredefFiles.toSet) 55 | allPredefFiles ++= importedFiles 56 | } 57 | 58 | // the script (if any) might also reference additional files via `using` directive 59 | config.scriptFile.foreach { scriptFile => 60 | val importedFiles = UsingDirectives.findImportedFilesRecursively(scriptFile, visited = allPredefFiles.toSet) 61 | allPredefFiles ++= importedFiles 62 | } 63 | 64 | allPredefFiles.toSeq.filter(Files.exists(_)).sorted 65 | } 66 | 67 | def allSourceLines(config: Config): Seq[String] = 68 | allSourceFiles(config).flatMap(linesFromFile) ++ config.runBefore 69 | 70 | /** precompile given predef files (if any) and update Config to include the results in the classpath */ 71 | def precompilePredefFiles(config: Config): Config = { 72 | if (config.predefFiles.nonEmpty) { 73 | val predefClassfilesDir = new SimpleDriver().compileAndGetOutputDir( 74 | replpp.compilerArgs(config), 75 | inputFiles = allPredefFiles(config), 76 | verbose = config.verbose 77 | ).get 78 | config.withAdditionalClasspathEntry(predefClassfilesDir) 79 | } else config 80 | } 81 | 82 | /** 83 | * resolve absolute or relative paths to an absolute path 84 | * - if given pathStr is an absolute path, just take that 85 | * - if it's a relative path, use given base path to resolve it to an absolute path 86 | * - if the base path is a file, take it's root directory - anything else doesn't make any sense. 87 | */ 88 | def resolveFile(base: Path, pathStr: String): Path = { 89 | val path = Paths.get(pathStr) 90 | if (path.isAbsolute) path 91 | else { 92 | val base0 = 93 | if (Files.isDirectory(base)) base 94 | else base.getParent 95 | base0.resolve(path) 96 | } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/scripting/NonForkingScriptRunner.scala: -------------------------------------------------------------------------------- 1 | package replpp.scripting 2 | 3 | import replpp.{Config, allPredefFiles} 4 | 5 | import java.nio.file.Files 6 | import scala.util.{Failure, Success} 7 | import scala.util.control.NoStackTrace 8 | 9 | /** 10 | * Main entrypoint for ScriptingDriver, i.e. it takes commandline arguments and executes a script on the current JVM. 11 | * Note: because it doesn't spawn a new JVM it doesn't work in all setups: e.g. when starting from `sbt test` 12 | * with `fork := false` it runs into classloader/classpath issues. Same goes for some IDEs, depending on their 13 | * classloader setup. 14 | * Because of these issues, the forking `ScriptRunner` is the default option. It simply spawns a new JVM and invokes 15 | * the NonForkingScriptRunner :) 16 | */ 17 | object NonForkingScriptRunner { 18 | 19 | def main(args: Array[String]): Unit = { 20 | val config = Config.parse(args) 21 | exec(config) 22 | } 23 | 24 | def exec(config: Config): Unit = { 25 | val scriptFile = config.scriptFile.getOrElse(throw new AssertionError("script file not defined - please specify e.g. via `--script=myscript.sc`")) 26 | if (!Files.exists(scriptFile)) { 27 | throw new AssertionError(s"given script file $scriptFile does not exist") 28 | } 29 | 30 | val paramsInfoMaybe = 31 | if (config.params.nonEmpty) s" with params=${config.params}" 32 | else "" 33 | System.err.println(s"executing $scriptFile$paramsInfoMaybe") 34 | val scriptArgs: Seq[String] = { 35 | val commandArgs = config.command.toList 36 | val parameterArgs = config.params.flatMap { case (key, value) => Seq(s"--$key", value) } 37 | commandArgs ++ parameterArgs 38 | } 39 | 40 | val verboseEnabled = replpp.verboseEnabled(config) 41 | new ScriptingDriver( 42 | compilerArgs = replpp.compilerArgs(config) :+ "-nowarn", 43 | predefFiles = allPredefFiles(config), 44 | runBefore = config.runBefore, 45 | runAfter = config.runAfter, 46 | scriptFile = scriptFile, 47 | scriptArgs = scriptArgs.toArray, 48 | verbose = verboseEnabled 49 | ).compileAndRun() match { 50 | case Success(_) => // no exception, i.e. all is good 51 | if (verboseEnabled) System.err.println(s"script finished successfully") 52 | case Failure(exception) => 53 | throw new RuntimeException(s"error during script execution: ${exception.getMessage}") with NoStackTrace 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/scripting/ScriptRunner.scala: -------------------------------------------------------------------------------- 1 | package replpp.scripting 2 | 3 | import replpp.{Config, verboseEnabled} 4 | import replpp.util.ClasspathHelper 5 | 6 | import scala.util.{Failure, Success, Try} 7 | import sys.process.Process 8 | 9 | /** Executes a script by spawning/forking a new JVM process and then invoking the `NonForkingScriptRunner`. 10 | * 11 | * Alternatively you can directly invoke the `NonForkingScriptRunner`, but some environments have complex classloader 12 | * setups which cause issues with the non-forking ScriptRunner - examples include some IDEs and 13 | * sbt (e.g. test|console) in non-fork mode. Therefor, this forking ScriptRunner is the default one. */ 14 | object ScriptRunner { 15 | 16 | val RemoteJvmDebugConfig = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005" 17 | 18 | def exec(config: Config): Try[Unit] = { 19 | val classpath = ClasspathHelper.create(config, quiet = true) 20 | val mainClass = "replpp.scripting.NonForkingScriptRunner" 21 | val args = { 22 | val builder = Seq.newBuilder[String] 23 | if (config.remoteJvmDebugEnabled) builder += RemoteJvmDebugConfig 24 | builder ++= Seq("-classpath", classpath) 25 | builder += mainClass 26 | builder ++= config.asJavaArgs 27 | 28 | builder.result() 29 | } 30 | if (replpp.verboseEnabled(config)) 31 | println(s"executing `java ${args.mkString(" ")}`") 32 | 33 | Process("java", args).run().exitValue() match { 34 | case 0 => Success(()) 35 | case nonZeroExitValue => 36 | Failure(new AssertionError( 37 | s"${getClass.getName}: error while invoking $mainClass: exit code was $nonZeroExitValue" 38 | )) 39 | } 40 | } 41 | 42 | def main(args: Array[String]): Unit = { 43 | val config = Config.parse(args) 44 | exec(config).get 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/scripting/ScriptingDriver.scala: -------------------------------------------------------------------------------- 1 | package replpp.scripting 2 | 3 | import dotty.tools.dotc.core.Contexts 4 | import dotty.tools.dotc.core.Contexts.Context 5 | import dotty.tools.io.ClassPath 6 | import replpp.scripting.ScriptingDriver.* 7 | import replpp.util.{SimpleDriver, deleteRecursively} 8 | 9 | import java.lang.reflect.Method 10 | import java.net.URLClassLoader 11 | import java.nio.file.{Files, Path, Paths} 12 | import scala.language.unsafeNulls 13 | import scala.util.{Failure, Try} 14 | 15 | /** 16 | * Runs a given script on the current JVM. 17 | * 18 | * Similar to dotty.tools.scripting.ScriptingDriver, but simpler and faster. 19 | * Main difference: we don't (need to) recursively look for main method entry points in the entire classpath, 20 | * because we have a fixed class and method name that ScriptRunner uses when it embeds the script and predef code. 21 | * */ 22 | class ScriptingDriver(compilerArgs: Array[String], 23 | predefFiles: Seq[Path], 24 | runBefore: Seq[String], 25 | runAfter: Seq[String], 26 | scriptFile: Path, 27 | scriptArgs: Array[String], 28 | verbose: Boolean) { 29 | private val wrappingResult = WrapForMainArgs(Files.readString(scriptFile), runBefore, runAfter) 30 | private val wrappedScript = Files.createTempFile("wrapped-script", ".sc") 31 | private val tempFiles = Seq.newBuilder[Path] 32 | private var executed = false 33 | 34 | Files.writeString(wrappedScript, wrappingResult.fullScript) 35 | tempFiles += wrappedScript 36 | 37 | if (verbose) { 38 | println(s"predefFiles: ${predefFiles.mkString(";")}") 39 | println(s"full script content (including wrapper code) ($wrappedScript)") 40 | println(wrappingResult.fullScript) 41 | println(s"script arguments: ${scriptArgs.mkString(",")}") 42 | println(s"compiler arguments: ${compilerArgs.mkString(",")}") 43 | } 44 | 45 | def compileAndRun(): Try[Object] = { 46 | assert(!executed, "scripting driver can only be used once, and this instance has already been used.") 47 | executed = true 48 | val inputFiles = (wrappedScript +: predefFiles).filter(Files.exists(_)) 49 | val driver = new SimpleDriver( 50 | linesBeforeRunBeforeCode = wrappingResult.linesBeforeRunBeforeCode, 51 | linesBeforeScript = wrappingResult.linesBeforeScript 52 | ) 53 | val result = driver.compile(compilerArgs, inputFiles, verbose) { (ctx, outDir) => 54 | given Context = ctx 55 | tempFiles += outDir 56 | 57 | val inheritedClasspath = ctx.settings.classpath.value 58 | val classpathEntries = ClassPath.expandPath(inheritedClasspath, expandStar = true).map(Paths.get(_)) 59 | val mainMethod = lookupMainMethod(outDir, classpathEntries) 60 | mainMethod.invoke(null, scriptArgs) 61 | } 62 | tempFiles.result().foreach(deleteRecursively) 63 | 64 | result.recoverWith { case e => 65 | val msgAddonMaybe = if (verbose) "" else "Re-run with `--verbose` for more details" 66 | Failure(CompilerError( 67 | s"""Error during compilation: ${e.getMessage} 68 | |Please check error output above! 69 | |For given input files: ${inputFiles.mkString(", ")} 70 | |$msgAddonMaybe 71 | |""".stripMargin 72 | )) 73 | } 74 | } 75 | 76 | private def lookupMainMethod(outDir: Path, classpathEntries: Seq[Path]): Method = { 77 | val classpathUrls = (classpathEntries :+ outDir).map(_.toUri.toURL) 78 | val clazz = URLClassLoader(classpathUrls.toArray).loadClass(MainClassName) 79 | clazz.getMethod(MainMethodName, classOf[Array[String]]) 80 | } 81 | } 82 | 83 | object ScriptingDriver { 84 | val MainClassName = "ScalaReplPP" 85 | val MainMethodName = "main" 86 | } 87 | case class CompilerError(msg: String) extends RuntimeException(msg) 88 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/scripting/WrapForMainArgs.scala: -------------------------------------------------------------------------------- 1 | package replpp.scripting 2 | 3 | /** So that we can execute the given script and potentially handle parameters, we wrap it in some code using 4 | * https://github.com/com-lihaoyi/mainargs */ 5 | object WrapForMainArgs { 6 | 7 | /** linesBeforeScript: allows us to adjust line numbers in error reporting... */ 8 | case class WrappingResult(fullScript: String, linesBeforeRunBeforeCode: Int, linesBeforeScript: Int) 9 | 10 | def apply(scriptCode: String, runBefore: Seq[String], runAfter: Seq[String]): WrappingResult = { 11 | val wrapperCodeStart0 = 12 | s"""import replpp.shaded.mainargs 13 | |import mainargs.main // intentionally shadow any potentially given @main 14 | | 15 | |// ScriptingDriver expects an object with a predefined name and a main entrypoint method 16 | |object ${ScriptingDriver.MainClassName} { 17 | |// runBeforeCode START 18 | |""".stripMargin 19 | val linesBeforeRunBeforeCode = 20 | wrapperCodeStart0.lines().count().toInt 21 | + 1 // for the line break after `wrapperCodeStart0` 22 | 23 | val wrapperCodeStart1 = 24 | s"""$wrapperCodeStart0 25 | |${runBefore.mkString("\n")} 26 | |// runBeforeCode END 27 | |""".stripMargin 28 | 29 | var linesBeforeScript = 0 // to adjust line number reporting 30 | 31 | val mainImpl = 32 | if (scriptCode.contains("@main")) 33 | scriptCode 34 | else { 35 | linesBeforeScript += 1 // because we added the following line _before_ the wrapped script code 36 | s"""@main def _execMain(): Unit = { 37 | |$scriptCode 38 | |}""".stripMargin 39 | } 40 | 41 | linesBeforeScript += wrapperCodeStart1.lines().count().toInt 42 | linesBeforeScript += 1 // for the line break after `wrapperCodeStart1` 43 | val fullScript = 44 | s"""$wrapperCodeStart1 45 | |$mainImpl 46 | | 47 | | def ${ScriptingDriver.MainMethodName}(args: Array[String]): Unit = { 48 | | mainargs.ParserForMethods(this).runOrExit(args.toSeq) 49 | | 50 | | ${runAfter.mkString("\n")} 51 | | } 52 | |} 53 | |""".stripMargin 54 | 55 | WrappingResult(fullScript, linesBeforeRunBeforeCode, linesBeforeScript) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/util/Cache.scala: -------------------------------------------------------------------------------- 1 | package replpp.util 2 | 3 | import replpp.home 4 | 5 | import java.io.InputStream 6 | import java.net.URI 7 | import java.nio.file.{Files, Path} 8 | 9 | /** A simple cache for `cacheKey` -> `Path`, where `Path` is a single file */ 10 | object Cache { 11 | lazy val Dir: Path = { 12 | val dir = home.resolve(".cache/scala-repl-pp") 13 | if (Files.notExists(dir)) Files.createDirectories(dir) 14 | dir 15 | } 16 | 17 | def getOrObtain(cacheKey: String, obtain: () => InputStream): Path = { 18 | val path = targetPath(cacheKey) 19 | this.synchronized { 20 | if (Files.exists(path)) { 21 | path 22 | } else { 23 | val inputStream = obtain() 24 | Files.copy(inputStream, path) 25 | inputStream.close() 26 | path 27 | } 28 | } 29 | } 30 | 31 | /** similar to `getOrObtain`, but specifically for files that need to be downloaded */ 32 | def getOrDownload(cacheKey: String, downloadUrl: URI): Path = { 33 | getOrObtain(cacheKey, obtain = () => { 34 | downloadUrl.toURL.openStream() 35 | }) 36 | } 37 | 38 | /** 39 | * @return true if cache entry did actually exist 40 | */ 41 | def remove(cacheKey: String): Boolean = { 42 | val path = targetPath(cacheKey) 43 | val entryExisted = Files.exists(path) 44 | 45 | if (entryExisted) 46 | deleteRecursively(path) 47 | 48 | entryExisted 49 | } 50 | 51 | private def targetPath(cacheKey: String): Path = 52 | Dir.resolve(cacheKey) 53 | 54 | } 55 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/util/ClasspathHelper.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | package util 3 | 4 | import java.io.File.pathSeparator 5 | import java.net.URL 6 | import java.nio.file.{Path, Paths} 7 | import scala.io.Source 8 | import scala.util.Using 9 | 10 | object ClasspathHelper { 11 | 12 | /** 13 | * Concatenates the classpath from multiple sources, each are required for different scenarios: 14 | * - `java.class.path` system property 15 | * - jars from current class loader (recursively) 16 | * - dependency artifacts as passed via (command-line) configuration 17 | * The exact behaviour regarding inherited classpath (java.class.path and outer classloader) can be configured via `Config.ForClasspath 18 | * 19 | * To have reproducible results, we order the classpath entries. This may interfere with the user's deliberate choice 20 | * of order, but since we need to concatenate the classpath from different sources, the user can't really depend on 21 | * the order anyway. 22 | */ 23 | def create(config: Config, quiet: Boolean = false): String = 24 | create(fromConfig(config, quiet).map(util.pathAsString)) 25 | 26 | def create(entries: Seq[String]): String = { 27 | /** Important: we absolutely have to make sure this starts and ends with a `pathSeparator`. 28 | * Otherwise, the last entry is lost somewhere down the line (I didn't find the exact location where things go 29 | * wrong, but it looked like somewhere in dotty 3.3.0). */ 30 | entries.distinct.mkString(pathSeparator, pathSeparator, pathSeparator) 31 | } 32 | 33 | protected[util] def fromConfig(config: Config, quiet: Boolean = false): Seq[Path] = { 34 | val entries = Seq.newBuilder[Path] 35 | val debugPrint = verboseEnabled(config) && !quiet 36 | 37 | // add select entries from out inherited classpath to the resulting classpath 38 | def addToEntriesMaybe(path: Path): Unit = { 39 | val classpathConfig = config.classpathConfig 40 | val filename = path.getFileName.toString 41 | val included = classpathConfig.inheritClasspath || classpathConfig.inheritClasspathIncludes.exists(filename.matches(_)) 42 | lazy val excluded = classpathConfig.inheritClasspathExcludes.exists(filename.matches(_)) 43 | if (included && !excluded) { 44 | if (debugPrint) println(s"using jar from inherited classpath: $path") 45 | entries.addOne(path) 46 | } else { 47 | if (debugPrint) println(s"excluding jar from inherited classpath (included=$included; excluded=$excluded: $path)") 48 | } 49 | } 50 | System.getProperty("java.class.path").split(pathSeparator).filter(_.nonEmpty).map(Paths.get(_)).foreach(addToEntriesMaybe) 51 | jarsFromClassLoaderRecursively(classOf[replpp.ReplDriver].getClassLoader).map(url => Paths.get(url.toURI)).foreach(addToEntriesMaybe) 52 | 53 | val fromDependencies = dependencyArtifacts(config) 54 | fromDependencies.foreach(entries.addOne) 55 | if (fromDependencies.nonEmpty && !quiet) { 56 | println(s"resolved dependencies - adding ${fromDependencies.size} artifact(s) to classpath - to list them, enable verbose mode") 57 | if (verboseEnabled(config)) fromDependencies.foreach(println) 58 | } 59 | 60 | entries.addAll(config.classpathConfig.additionalClasspathEntries.map(Paths.get(_))) 61 | entries.addAll(UsingDirectives.findClasspathEntriesInFiles(allSourceFiles(config))) 62 | 63 | val runBeforeCodeAsLines = config.runBefore.flatMap(_.linesIterator) 64 | entries.addAll(UsingDirectives.findClasspathEntriesInLines(runBeforeCodeAsLines, currentWorkingDirectory)) 65 | 66 | val result = entries.result().distinct.sorted 67 | if (debugPrint) { 68 | println("classpath entries from config >") 69 | println(result.mkString("\n")) 70 | println("< classpath entries from config") 71 | } 72 | 73 | result 74 | } 75 | 76 | private[util] def dependencyArtifacts(config: Config): Seq[Path] = { 77 | val allLines = allSourceLines(config) 78 | val resolvers = config.classpathConfig.resolvers ++ UsingDirectives.findResolvers(allLines) 79 | val allDependencies = config.classpathConfig.dependencies ++ UsingDirectives.findDeclaredDependencies(allLines) 80 | Dependencies.resolve(allDependencies, resolvers, verboseEnabled(config)).get 81 | } 82 | 83 | private def jarsFromClassLoaderRecursively(classLoader: ClassLoader): Seq[URL] = { 84 | classLoader match { 85 | case cl: java.net.URLClassLoader => 86 | jarsFromClassLoaderRecursively(cl.getParent) ++ cl.getURLs 87 | case _ => 88 | Seq.empty 89 | } 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/util/SimpleDriver.scala: -------------------------------------------------------------------------------- 1 | package replpp.util 2 | 3 | import dotty.tools.dotc.Driver 4 | import dotty.tools.dotc.config.Settings 5 | import dotty.tools.dotc.core.Contexts 6 | import dotty.tools.dotc.core.Contexts.Context 7 | import dotty.tools.dotc.reporting.{Diagnostic, Reporter} 8 | import dotty.tools.dotc.util.SourcePosition 9 | import dotty.tools.io.{Directory, PlainDirectory} 10 | import replpp.scripting.CompilerError 11 | 12 | import java.nio.file.{Files, Path} 13 | import scala.language.unsafeNulls 14 | import scala.util.Try 15 | import scala.util.control.NoStackTrace 16 | 17 | /** Compiles input files to a temporary directory 18 | * 19 | * TODO: use a VirtualDirectory for the output - I didn't manage to find a good way to pass those to the 20 | * Context of the DottyReplDriver yet... 21 | * val virtualDirectory = new VirtualDirectory("(virtual)") 22 | * val cp = ClassPathFactory.newClassPath(virtualDirectory) 23 | * 24 | * TODO: cache results 25 | * i.e. store hash of all inputs? 26 | * that functionality must exist somewhere already, e.g. zinc incremental compiler, or even in dotty itself? 27 | */ 28 | class SimpleDriver(linesBeforeRunBeforeCode: Int = 0, linesBeforeScript: Int = 0) extends Driver { 29 | 30 | def compileAndGetOutputDir[A](compilerArgs: Array[String], inputFiles: Seq[Path], verbose: Boolean): Try[Path] = 31 | compile(compilerArgs, inputFiles, verbose) { (ctx, outDir) => outDir } 32 | 33 | /** compiles given inputFiles and returns root directory that contains the class and tasty files */ 34 | def compile[A](compilerArgs: Array[String], inputFiles: Seq[Path], verbose: Boolean)(fun: (Context, Path) => A): Try[A] = { 35 | if (verbose) { 36 | println(s"compiler arguments: ${compilerArgs.mkString(",")}") 37 | println(s"inputFiles: ${inputFiles.mkString(";")}") 38 | } 39 | 40 | val inputFiles0 = inputFiles.map(pathAsString).toArray 41 | val allArgs = compilerArgs ++ inputFiles0 42 | Try { 43 | val (toCompile, rootCtx) = setup(allArgs, initCtx.fresh) 44 | .getOrElse(throw CompilerError(s"error during setup with args=`${allArgs.mkString(" ")}`, details should have been reported already on stderr/stdout")) 45 | 46 | val outDir = Files.createTempDirectory("scala-repl-pp") 47 | 48 | given ctx0: Context = { 49 | val ctx = rootCtx.fresh 50 | 51 | if (linesBeforeRunBeforeCode != 0 || linesBeforeScript != 0) { 52 | ctx.setReporter(createReporter(linesBeforeRunBeforeCode, linesBeforeScript, rootCtx.reporter)) 53 | } 54 | 55 | ctx.setSetting(rootCtx.settings.outputDir, new PlainDirectory(Directory(outDir))) 56 | .setSetting(rootCtx.settings.help, verbose) 57 | .setSetting(rootCtx.settings.XshowPhases, verbose) 58 | .setSetting(rootCtx.settings.Vhelp, verbose) 59 | .setSetting(rootCtx.settings.Vprofile, verbose) 60 | .setSetting(rootCtx.settings.explain, verbose) 61 | 62 | // Scala 3.3.6 has `YnoEnrichErrorMessages` rather than `XnoEnrichErrorMessages` 63 | // So as long as we still support 3.3.6 (i.e. until the next LTS release) we need the 64 | // following section. 65 | val noEnrichErrorMessagesRegex = "-[XY]no-enrich-error-messages".r 66 | rootCtx.settings.allSettings 67 | .find(setting => noEnrichErrorMessagesRegex.matches(setting.name)) 68 | .foreach { noEnrichErrorMessagesSetting => 69 | // println(s"setting $noEnrichErrorMessagesSetting to ${!verbose}") 70 | ctx.setSetting(noEnrichErrorMessagesSetting.asInstanceOf[Settings.Setting[Boolean]], !verbose) 71 | } 72 | // Once that's not required any longer, replace the above with this: 73 | // ctx.setSetting(rootCtx.settings.XnoEnrichErrorMessages, !verbose) 74 | ctx 75 | } 76 | 77 | if (doCompile(newCompiler, toCompile).hasErrors) { 78 | val msgAddonMaybe = if (verbose) "" else " - try `--verbose` for more output" 79 | throw new CompilerError(s"Errors encountered during compilation$msgAddonMaybe") with NoStackTrace 80 | } else { 81 | fun(ctx0, outDir) 82 | } 83 | } 84 | } 85 | 86 | private def createReporter(linesBeforeRunBeforeCode: Int, linesBeforeScript: Int, originalReporter: Reporter): Reporter = { 87 | new Reporter { 88 | override def doReport(dia: Diagnostic)(using Context): Unit = { 89 | val adjustedPos = new SourcePosition(source = dia.pos.source, span = dia.pos.span, outer = dia.pos.outer) { 90 | override def line: Int = { 91 | val original = super.line 92 | val adjusted = original - linesBeforeScript 93 | if (adjusted >= 0) { 94 | adjusted 95 | } else { 96 | // adjusted line number is negative, i.e. the error must be in the `runBefore` code 97 | original - linesBeforeRunBeforeCode 98 | } 99 | } 100 | } 101 | originalReporter.doReport(new Diagnostic(dia.msg, adjustedPos, dia.level)) 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /core/src/main/scala/replpp/util/package.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import java.nio.file.{FileSystems, Files, Path} 4 | import replpp.shaded.fansi 5 | import scala.collection.immutable.Seq 6 | import scala.io.Source 7 | import scala.util.{Try, Using} 8 | 9 | package object util { 10 | def currentWorkingDirectory = Path.of(".") 11 | 12 | def sequenceTry[A](tries: Seq[Try[A]]): Try[Seq[A]] = { 13 | tries.foldRight(Try(Seq.empty[A])) { 14 | case (next, accumulator) => 15 | for { 16 | a <- next 17 | acc <- accumulator 18 | } yield a +: acc 19 | } 20 | } 21 | 22 | def linesFromFile(path: Path): Seq[String] = 23 | Using.resource(Source.fromFile(path.toFile))(_.getLines.toSeq) 24 | 25 | def deleteRecursively(path: Path): Unit = { 26 | if (Files.isDirectory(path)) 27 | Files.list(path).forEach(deleteRecursively) 28 | 29 | Files.deleteIfExists(path) 30 | } 31 | 32 | def readFileFromZip(zipFile: Path, fileName: String): Try[Array[Byte]] = { 33 | Using(FileSystems.newFileSystem(zipFile, null)) { fileSystem => 34 | Files.readAllBytes(fileSystem.getPath(fileName)) 35 | } 36 | } 37 | 38 | def colorise(value: String, color: fansi.EscapeAttr)(using colors: Colors): String = { 39 | colors match { 40 | case Colors.BlackWhite => value 41 | case Colors.Default => color(value).render 42 | } 43 | } 44 | 45 | /** Lookup the current terminal width - useful e.g. if you want to render something using the maximum space available. 46 | * Uses jline-jna, which therefor needs to be in the repl's classpath, which is why it's listed in 47 | * {{{replpp.Config.ForClasspath.DefaultInheritClasspathIncludes}}} */ 48 | def terminalWidth: Try[Int] = 49 | Using(org.jline.terminal.TerminalBuilder.terminal)(_.getWidth) 50 | 51 | def pathAsString(path: Path): String = 52 | path.toAbsolutePath.toString 53 | 54 | } 55 | -------------------------------------------------------------------------------- /core/src/test/resources/demo-project/build.sbt: -------------------------------------------------------------------------------- 1 | name := "stringcalc" 2 | 3 | scalaVersion := "3.6.4" 4 | val srpVersion = "0.5.4" 5 | 6 | libraryDependencies ++= Seq( 7 | "com.michaelpollmeier" % "scala-repl-pp" % srpVersion cross CrossVersion.full, 8 | "com.github.scopt" %% "scopt" % "4.1.0", 9 | ) 10 | 11 | enablePlugins(JavaAppPackaging) 12 | -------------------------------------------------------------------------------- /core/src/test/resources/demo-project/plus.sc: -------------------------------------------------------------------------------- 1 | println(add(One, Two)) 2 | -------------------------------------------------------------------------------- /core/src/test/resources/demo-project/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.11 2 | -------------------------------------------------------------------------------- /core/src/test/resources/demo-project/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.1") 2 | -------------------------------------------------------------------------------- /core/src/test/resources/demo-project/src/main/scala/stringcalc/Domain.scala: -------------------------------------------------------------------------------- 1 | package stringcalc 2 | 3 | trait Number(numeric: Int) { 4 | override def toString = s"Number($numeric)" 5 | } 6 | object One extends Number(1) 7 | object Two extends Number(2) 8 | object Three extends Number(3) 9 | 10 | 11 | object StringCalculator { 12 | def add(number1: Number, number2: Number): Number = 13 | (number1, number2) match { 14 | case (One, One) => Two 15 | case (One, Two) => Three 16 | case (Two, One) => Three 17 | case _ => ??? 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src/test/resources/demo-project/src/main/scala/stringcalc/Main.scala: -------------------------------------------------------------------------------- 1 | package stringcalc 2 | 3 | import java.nio.file.{Files, Path} 4 | import scopt.OParser 5 | 6 | def main(args: Array[String]) = { 7 | Config.parse(args) match { 8 | case Some(config) => 9 | replpp.Main.run( 10 | replpp.Config( 11 | scriptFile = config.scriptFile, 12 | prompt = Some("stringcalc"), 13 | greeting = Some("Welcome to the magical world of string calculation! \nType `help` for help"), 14 | verbose = config.verbose, 15 | runBefore = Seq( 16 | "import stringcalc.*", 17 | "import StringCalculator.*", 18 | """def help: Unit = println("try this: `add(One, Two)`")""" 19 | ) 20 | ) 21 | ) 22 | case None => System.exit(1) 23 | } 24 | } 25 | 26 | case class Config(verbose: Boolean = false, scriptFile: Option[Path] = None) 27 | object Config { 28 | def parse(args: Array[String]): Option[Config] = 29 | OParser.parse(parser, args, Config()) 30 | 31 | private val builder = OParser.builder[Config] 32 | private val parser = { 33 | import builder._ 34 | OParser.sequence( 35 | programName("stringcalc"), 36 | opt[Boolean]('v', "verbose") 37 | .action((x, c) => c.copy(verbose = x)) 38 | .text("enable verbose mode"), 39 | opt[Path]("script") 40 | .action((x, c) => c.copy(scriptFile = Option(x))) 41 | .text("path to script file") 42 | .validate(path => 43 | if (Files.exists(path)) success 44 | else failure(s"script file $path does not exist") 45 | ) 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/test/resources/demo-project/stringcalc: -------------------------------------------------------------------------------- 1 | target/universal/stage/bin/stringcalc -------------------------------------------------------------------------------- /core/src/test/resources/invalid-ansi.txt: -------------------------------------------------------------------------------- 1 | › 2 | -------------------------------------------------------------------------------- /core/src/test/scala/replpp/ConfigTests.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import replpp.Colors 6 | 7 | import java.nio.file.Paths 8 | 9 | class ConfigTests extends AnyWordSpec with Matchers { 10 | val apacheRepo = "https://repository.apache.org/content/groups/public" 11 | val sonatypeRepo = "https://oss.sonatype.org/content/repositories/public" 12 | 13 | "color modes" in { 14 | Config().colors shouldBe Colors.Default 15 | Config(nocolors = false).colors shouldBe Colors.Default 16 | Config(nocolors = true).colors shouldBe Colors.BlackWhite 17 | } 18 | 19 | "asJavaArgs (inverse of Config.parse)" in { 20 | val config = Config( 21 | predefFiles = List(Paths.get("/some/path/predefFile1"), Paths.get("/some/path/predefFile2")), 22 | runBefore = List("val foo = 42", "println(foo)"), 23 | runAfter = List("""val msg = "goodbye!"""", "println(msg)"), 24 | nocolors = true, 25 | verbose = true, 26 | classpathConfig = Config.ForClasspath( 27 | additionalClasspathEntries = Seq("cp1", "../path/to/cp2"), 28 | inheritClasspath = true, 29 | inheritClasspathIncludes = Config.ForClasspath.DefaultInheritClasspathIncludes ++ Seq(".*include1", "include2.*"), 30 | inheritClasspathExcludes = Seq(".*exclude1", "exclude2.*"), 31 | dependencies = Seq("com.michaelpollmeier:versionsort:1.0.7", "foo:bar:1.2.3"), 32 | resolvers = Seq(apacheRepo, sonatypeRepo), 33 | ), 34 | maxHeight = Some(10000), 35 | scriptFile = Some(Paths.get("/some/script.sc")), 36 | command = Some("someCommand"), 37 | params = Map("param1" -> "value1", "param2" -> "222", "someEquation" -> "40 + 2 = 42"), 38 | ) 39 | 40 | val javaArgs = config.asJavaArgs 41 | javaArgs shouldBe Seq( 42 | "--predef", Paths.get("/some/path/predefFile1").toString, 43 | "--predef", Paths.get("/some/path/predefFile2").toString, 44 | "--runBefore", "val foo = 42", 45 | "--runBefore", "println(foo)", 46 | "--runAfter", """val msg = "goodbye!"""", 47 | "--runAfter", "println(msg)", 48 | "--nocolors", 49 | "--verbose", 50 | "--classpathEntry", "cp1", 51 | "--classpathEntry", "../path/to/cp2", 52 | "--cpinherit", 53 | "--cpinclude", ".*include1", 54 | "--cpinclude", "include2.*", 55 | "--cpexclude", ".*exclude1", 56 | "--cpexclude", "exclude2.*", 57 | "--dep", "com.michaelpollmeier:versionsort:1.0.7", 58 | "--dep", "foo:bar:1.2.3", 59 | "--repo", apacheRepo, 60 | "--repo", sonatypeRepo, 61 | "--maxHeight", "10000", 62 | "--script", Paths.get("/some/script.sc").toString, 63 | "--command", "someCommand", 64 | "--param", "param1=value1", 65 | "--param", "param2=222", 66 | "--param", "someEquation=40 + 2 = 42", 67 | ) 68 | 69 | // round trip 70 | Config.parse(javaArgs.toArray) shouldBe config 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /core/src/test/scala/replpp/DependenciesTests.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | 6 | import java.nio.file.Path 7 | 8 | class DependenciesTests extends AnyWordSpec with Matchers { 9 | val coursierCache = os.home / ".cache" / "coursier" / "v1" 10 | 11 | /** these require internet access... */ 12 | "artifact resolution including transitive dependencies" should { 13 | "work for java artifacts" in { 14 | val jars = Dependencies.resolve(Seq("io.shiftleft:overflowdb-core:1.171")).get 15 | 16 | ensureContains("overflowdb-core-1.171.jar", jars) 17 | ensureContains("h2-mvstore-1.4.200.jar", jars) 18 | } 19 | 20 | "work for scala artifacts" in { 21 | val jars = Dependencies.resolve(Seq("com.lihaoyi::sourcecode:0.3.0")).get 22 | jars.size shouldBe 3 23 | ensureContains("sourcecode_3-0.3.0.jar", jars) 24 | ensureContains("scala3-library_3-3.1.3.jar", jars) 25 | } 26 | 27 | def ensureContains(fileName: String, jars: Seq[Path]): Unit = { 28 | assert(jars.exists(_.toString.endsWith(fileName)), s"expected $fileName, but it's not in the results: $jars") 29 | } 30 | } 31 | 32 | "return failure for invalid dependency coordinate" in { 33 | val parseResult = Dependencies.resolve(Seq("not-a-valid-maven-coordinate")) 34 | val errorMessage = parseResult.failed.get.getMessage 35 | errorMessage should include("fetch not-a-valid-maven-coordinate") 36 | } 37 | 38 | "return failure for invalid repository" in { 39 | val parseResult = Dependencies.resolve( 40 | Seq("com.lihaoyi::sourcecode:0.3.0"), 41 | Seq("not-a-valid-repository"), 42 | ) 43 | val errorMessage = parseResult.failed.get.getMessage 44 | errorMessage should include("not-a-valid-repository") 45 | } 46 | } 47 | 48 | object DependenciesTests { 49 | 50 | def main(args: Array[String]): Unit = { 51 | /** only to be run manually, sorry... 52 | * verify that we can access an artifact that's only available on a separate, password-protected repository 53 | * note: relies on the local ~/.config/coursier/credentials.properties 54 | * and the private jfrog artifactory in shiftleft.jfrog.io 55 | */ 56 | 57 | println( 58 | Dependencies.resolve( 59 | Seq("io.shiftleft::common:0.3.109"), 60 | Seq("https://shiftleft.jfrog.io/shiftleft/libs-release-local") 61 | ).get 62 | ) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /core/src/test/scala/replpp/OperatorsTests.scala: -------------------------------------------------------------------------------- 1 | package replpp.util 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import replpp.Colors 6 | import replpp.Operators.* 7 | 8 | import scala.jdk.CollectionConverters.* 9 | import System.lineSeparator 10 | 11 | /* note: `inheritIO` mode can only be tested manually: it's supposed to open `less` in the terminal with the given input 12 | ``` 13 | "this is a test" #|^ "less" 14 | 15 | Seq("this is a test", "another one") #|^ "less" 16 | 17 | import scala.jdk.CollectionConverters.* 18 | Seq("this is a test", "another one").asJava #|^ "less" 19 | ``` 20 | */ 21 | class OperatorsTests extends AnyWordSpec with Matchers { 22 | given Colors = Colors.BlackWhite 23 | 24 | case class PrettyPrintable(s: String, i: Int) 25 | 26 | "#> and #>> override and append to file" when { 27 | "using single objects" in { 28 | val result = withTempFile { path => 29 | "foo" #> path 30 | PrettyPrintable("two", 2) #>> path 31 | } 32 | result shouldBe 33 | """foo 34 | |PrettyPrintable(s = "two", i = 2) 35 | |""".stripMargin 36 | 37 | // double checking that it ends with a lineSeparator 38 | result shouldBe "foo" + lineSeparator + """PrettyPrintable(s = "two", i = 2)""" + lineSeparator 39 | } 40 | 41 | "using IterableOnce" in { 42 | val values: IterableOnce[?] = Seq("foo", PrettyPrintable("two", 2)) 43 | withTempFile { path => 44 | values #> path 45 | "-----" #>> path 46 | values #>> path 47 | } shouldBe 48 | """foo 49 | |PrettyPrintable(s = "two", i = 2) 50 | |----- 51 | |foo 52 | |PrettyPrintable(s = "two", i = 2) 53 | |""".stripMargin 54 | } 55 | 56 | "using java.lang.Iterable" in { 57 | val values: java.lang.Iterable[?] = Seq("foo", PrettyPrintable("two", 2)).asJava 58 | withTempFile { path => 59 | values #> path 60 | "-----" #>> path 61 | values #>> path 62 | } shouldBe 63 | """foo 64 | |PrettyPrintable(s = "two", i = 2) 65 | |----- 66 | |foo 67 | |PrettyPrintable(s = "two", i = 2) 68 | |""".stripMargin 69 | } 70 | 71 | "using Array" in { 72 | val values: Array[?] = Seq("foo", PrettyPrintable("two", 2)).toArray 73 | withTempFile { path => 74 | values #> path 75 | "-----" #>> path 76 | values #>> path 77 | } shouldBe 78 | """foo 79 | |PrettyPrintable(s = "two", i = 2) 80 | |----- 81 | |foo 82 | |PrettyPrintable(s = "two", i = 2) 83 | |""".stripMargin 84 | } 85 | 86 | "using Iterator" in { 87 | def values: Iterator[?] = Seq("foo", PrettyPrintable("two", 2)).iterator 88 | withTempFile { path => 89 | values #> path 90 | "-----" #>> path 91 | values #>> path 92 | } shouldBe 93 | """foo 94 | |PrettyPrintable(s = "two", i = 2) 95 | |----- 96 | |foo 97 | |PrettyPrintable(s = "two", i = 2) 98 | |""".stripMargin 99 | } 100 | 101 | "using java.util.Iterator" in { 102 | def values: java.util.Iterator[?] = Seq("foo", PrettyPrintable("two", 2)).asJava.iterator() 103 | withTempFile { path => 104 | values #> path 105 | "-----" #>> path 106 | values #>> path 107 | } shouldBe 108 | """foo 109 | |PrettyPrintable(s = "two", i = 2) 110 | |----- 111 | |foo 112 | |PrettyPrintable(s = "two", i = 2) 113 | |""".stripMargin 114 | } 115 | } 116 | 117 | "#| pipes into an external command" when { 118 | if (scala.util.Properties.isWin) { 119 | info("#| is not unit-tested yet on windows - no idea what the equivalent of `cat` is") 120 | } else { 121 | "using String" in { 122 | val value = "foo" 123 | val result = value #| "cat" 124 | result shouldBe value 125 | } 126 | 127 | "using case class" in { 128 | val result = PrettyPrintable("two", 2) #| "cat" 129 | result shouldBe """PrettyPrintable(s = "two", i = 2)""" 130 | } 131 | 132 | "using list types" when { 133 | val values = Seq("foo", PrettyPrintable("two", 2)) 134 | Seq( 135 | ("IterableOnce", values: IterableOnce[?]), 136 | ("java.lang.Iterable", values.asJava: java.lang.Iterable[?]), 137 | ("Array", values.toArray: Array[?]), 138 | ("Iterator", values.iterator: Iterator[?]), 139 | ("java.util.Iterator", values.asJava.iterator: java.util.Iterator[?]), 140 | ).foreach { case (listType, list) => 141 | listType in { 142 | val result = list #| "cat" 143 | result shouldBe 144 | """foo 145 | |PrettyPrintable(s = "two", i = 2)""".stripMargin 146 | } 147 | } 148 | } 149 | 150 | "passing arguments to the external command" in { 151 | val result1 = Seq("foo", "bar", "foobar") #| ("grep", "foo") 152 | result1 shouldBe """foo 153 | |foobar""".stripMargin 154 | 155 | 156 | val input2 = """foo 157 | |bar 158 | |foobar""".stripMargin 159 | val result2 = input2 #| ("grep", "foo") 160 | result2 shouldBe 161 | """foo 162 | |foobar""".stripMargin 163 | } 164 | } 165 | } 166 | 167 | "unwrapping of list-types should only happen at root level" should { 168 | "happen at root level" in { 169 | withTempFile { file => 170 | Seq( 171 | "one", 172 | Seq("two"), 173 | Seq("three", 4), 174 | 5 175 | ) #> file 176 | } shouldBe 177 | """one 178 | |List("two") 179 | |List("three", 4) 180 | |5 181 | |""".stripMargin 182 | } 183 | } 184 | 185 | def withTempFile(funWithPath: String => Unit): String = { 186 | val tmpFile = os.temp(contents = "initial contents", prefix = getClass.getName) 187 | try { 188 | funWithPath(tmpFile.toString) 189 | os.read(tmpFile) 190 | } finally { 191 | os.remove(tmpFile) 192 | } 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /core/src/test/scala/replpp/PPrinterTests.scala: -------------------------------------------------------------------------------- 1 | package replpp 2 | 3 | import java.nio.file.Files 4 | import org.scalatest.matchers.should.Matchers 5 | import org.scalatest.wordspec.AnyWordSpec 6 | import replpp.shaded.fansi 7 | import replpp.util.ProjectRoot 8 | import scala.util.Try 9 | 10 | /** We use source-highlight to encode source as ansi strings, e.g. the .dump step Ammonite uses fansi for it's 11 | * colour-coding, and while both pledge to follow the ansi codec, they aren't compatible TODO: PR for fansi to support 12 | * these standard encodings out of the box 13 | */ 14 | class PPrinterTests extends AnyWordSpec with Matchers { 15 | 16 | "print some common datastructures" in { 17 | testRendering( 18 | List(1, 2), 19 | expectedUncolored = "List(1, 2)", 20 | expectedColored = "(, )" 21 | ) 22 | 23 | testRendering( 24 | (1,2,"three"), 25 | expectedUncolored = """(1, 2, "three")""", 26 | expectedColored = """(, , )""" 27 | ) 28 | 29 | case class A(i: Int, s: String) 30 | testRendering( 31 | A(42, "foo bar"), 32 | expectedUncolored = """A(i = 42, s = "foo bar")""", 33 | expectedColored = """(i = , s = )""" 34 | ) 35 | 36 | val productWithLabels = new Product { 37 | override def productPrefix = "Foo" 38 | 39 | def productArity = 2 40 | 41 | override def productElementName(n: Int) = 42 | n match { 43 | case 0 => "first" 44 | case 1 => "second" 45 | } 46 | 47 | def productElement(n: Int) = n match { 48 | case 0 => "one" 49 | case 1 => "two" 50 | } 51 | 52 | def canEqual(that: Any): Boolean = ??? 53 | } 54 | 55 | testRendering( 56 | productWithLabels, 57 | expectedUncolored = """Foo(first = "one", second = "two")""", 58 | expectedColored = """(first = , second = )""" 59 | ) 60 | } 61 | 62 | "render strings with string escapes using triple quotes" in { 63 | PPrinter("""a\b""", nocolors = true) shouldBe " \"\"\"a\\b\"\"\" ".trim 64 | } 65 | 66 | "don't error on invalid ansi encodings" in { 67 | val invalidAnsi = Files.readString(ProjectRoot.relativise("core/src/test/resources/invalid-ansi.txt")) 68 | Try { 69 | PPrinter(invalidAnsi, nocolors = true) 70 | PPrinter(invalidAnsi) 71 | }.isSuccess shouldBe true 72 | } 73 | 74 | "fansi encoding fix" must { 75 | "handle different ansi encoding termination" in { 76 | // encoding ends with [39m for fansi instead of [m 77 | val fixedForFansi = PPrinter.fixForFansi(IntGreenForeground) 78 | fixedForFansi shouldBe fansi.Str("\u001b[32mint\u001b[39m") 79 | fansi.Str(fixedForFansi) shouldBe fansi.Color.Green("int") 80 | } 81 | 82 | "handle different single-digit encodings" in { 83 | // `[01m` is encoded as `[1m` in fansi for all single digit numbers 84 | val fixedForFansi = PPrinter.fixForFansi(FBold) 85 | fixedForFansi shouldBe fansi.Str("\u001b[1mF\u001b[39m") 86 | fansi.Str(fixedForFansi) shouldBe fansi.Str("F").overlay(fansi.Bold.On) 87 | } 88 | 89 | "handle multi-encoded parts" in { 90 | // `[01;34m` is encoded as `[1m[34m` in fansi 91 | val fixedForFansi = PPrinter.fixForFansi(IfBlueBold) 92 | fixedForFansi shouldBe fansi.Str("\u001b[1m\u001b[34mif\u001b[39m") 93 | fansi.Str(fixedForFansi) shouldBe fansi.Color.Blue("if").overlay(fansi.Bold.On) 94 | } 95 | 96 | "handle 8bit (256) 'full' colors" in { 97 | // `[00;38;05;70m` is encoded as `[38;5;70m` in fansi 98 | val fixedForFansi = PPrinter.fixForFansi(X8bit) 99 | fixedForFansi shouldBe fansi.Str("\u001b[38;5;70mX\u001b[39m") 100 | fansi.Str(fixedForFansi) shouldBe fansi.Color.Full(70)("X") 101 | } 102 | 103 | // ansi colour-encoded strings as source-highlight produces them 104 | lazy val IntGreenForeground = "\u001b[32mint\u001b[m" 105 | lazy val IfBlueBold = "\u001b[01;34mif\u001b[m" 106 | lazy val FBold = "\u001b[01mF\u001b[m" 107 | lazy val X8bit = "\u001b[00;38;05;70mX\u001b[m" 108 | } 109 | 110 | def testRendering(value: AnyRef, expectedUncolored: String, expectedColored: String): Unit = { 111 | PPrinter(value, nocolors = true) shouldBe expectedUncolored 112 | 113 | val resultColored = PPrinter(value) 114 | replaceColorEncodingForTest(resultColored) shouldBe expectedColored 115 | } 116 | 117 | // adapted from dotty SyntaxHighlightingTests 118 | private def replaceColorEncodingForTest(s: String): String = { 119 | val res = s 120 | .replace(Console.RESET, ">") 121 | .replace(fansi.Color.Reset.escape, ">") 122 | .replace(Console.YELLOW, " using file simple.sc 16 | |//> using file /path/to/absolute.sc 17 | |//> using file ../path/to/relative.sc 18 | |// //> using file commented_out.sc 19 | |""".stripMargin 20 | 21 | val results = UsingDirectives.findImportedFiles(source.lines().iterator().asScala, rootPath) 22 | results should contain(Paths.get("./simple.sc")) 23 | results should contain(Paths.get("/path/to/absolute.sc")) 24 | results should contain(Paths.get("./../path/to/relative.sc")) 25 | results should not contain Paths.get("./commented_out.sc") 26 | } 27 | 28 | "recursively resolve `//> using file` directive" in { 29 | val additionalFile2 = os.temp( 30 | contents = """val predef2 = 10""", 31 | suffix = "additionalFile2" 32 | ) 33 | val additionalFile1 = os.temp( 34 | contents = s"""//> using file $additionalFile2 35 | |val predef1 = 20""".stripMargin, 36 | suffix = "additionalFile1" 37 | ) 38 | val predefFile = os.temp( 39 | contents = s"""//> using file $additionalFile1 40 | |val predef0 = 0""".stripMargin) 41 | 42 | UsingDirectives.findImportedFilesRecursively(predefFile.toNIO).sorted shouldBe 43 | Seq(additionalFile1, additionalFile2).map(_.toNIO).sorted 44 | } 45 | 46 | "recursively resolve `//> using file` directive - and handle recursive loops" in { 47 | val additionalFile2 = os.temp(suffix = "additionalFile2") 48 | val additionalFile1 = os.temp(suffix = "additionalFile1") 49 | val predefFile = os.temp( 50 | contents = s"""//> using file $additionalFile1 51 | |val predef0 = 0""".stripMargin) 52 | 53 | os.write.over(additionalFile1, 54 | s"""//> using file $additionalFile2 55 | |val predef1 = 10""".stripMargin) 56 | os.write.over(additionalFile2, 57 | s"""//> using file $additionalFile1 58 | |val predef2 = 20""".stripMargin) 59 | 60 | UsingDirectives.findImportedFilesRecursively(predefFile.toNIO).sorted shouldBe 61 | Seq(additionalFile1, additionalFile2).map(_.toNIO).sorted 62 | // most importantly, this should not loop endlessly due to the recursive imports 63 | } 64 | 65 | "find declared dependencies" in { 66 | val source = 67 | """ 68 | |//> using dep com.example:some-dependency:1.1 69 | |//> using dep com.example::scala-dependency:1.2 70 | |// //> using dep commented:out:1.3 71 | |""".stripMargin 72 | 73 | val results = UsingDirectives.findDeclaredDependencies(source.linesIterator) 74 | results should contain("com.example:some-dependency:1.1") 75 | results should contain("com.example::scala-dependency:1.2") 76 | results should not contain "commented:out:1.3" 77 | } 78 | 79 | "find declared resolvers" in { 80 | val source = 81 | """ 82 | |//> using resolver https://repository.apache.org/content/groups/public 83 | |//> using resolver https://shiftleft.jfrog.io/shiftleft/libs-release-local 84 | |// //> using resolver https://commented.out/repo 85 | |""".stripMargin 86 | 87 | val results = UsingDirectives.findResolvers(source.linesIterator) 88 | results should contain("https://repository.apache.org/content/groups/public") 89 | results should contain("https://shiftleft.jfrog.io/shiftleft/libs-release-local") 90 | results should not contain "https://commented.out/repo" 91 | } 92 | 93 | if (scala.util.Properties.isWin) { 94 | info("paths work differently on windows - ignoring some tests") 95 | } else { 96 | "find declared classpath entries" in { 97 | val scriptFile = os.temp( 98 | """ 99 | |//> using classpath /path/to/cp1 100 | |//> using classpath path/to/cp2 101 | |//> using classpath ../path/to/cp3 102 | |// //> using classpath cp4 103 | |""".stripMargin 104 | ).toNIO 105 | 106 | val scriptParentDir = scriptFile.getParent 107 | 108 | val results = UsingDirectives.findClasspathEntriesInFiles(Seq(scriptFile)) 109 | results should contain(Path.of("/path/to/cp1")) 110 | results should contain(scriptParentDir.resolve("path/to/cp2")) 111 | results should contain(scriptParentDir.resolve("../path/to/cp3")) 112 | results should not contain Path.of("cp3") 113 | results.size shouldBe 3 // just to triple check 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /core/src/test/scala/replpp/util/CacheTests.scala: -------------------------------------------------------------------------------- 1 | package replpp.util 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | 6 | import java.io.FileInputStream 7 | import java.net.URI 8 | import java.nio.file.Files 9 | import java.util.UUID 10 | 11 | class CacheTests extends AnyWordSpec with Matchers { 12 | 13 | "caches a file by it's key" in { 14 | var obtainedFunctionInvocations = 0 15 | val cacheKey = newRandomCacheKey() 16 | Cache.remove(cacheKey) 17 | 18 | Cache.getOrObtain( 19 | cacheKey, 20 | obtain = () => { 21 | obtainedFunctionInvocations += 1 22 | new FileInputStream(ProjectRoot.relativise("README.md").toFile) 23 | } 24 | ) 25 | val cachedFile = Cache.getOrObtain( 26 | cacheKey, 27 | obtain = () => { 28 | obtainedFunctionInvocations += 1 29 | new FileInputStream(ProjectRoot.relativise("README.md").toFile) 30 | } 31 | ) 32 | 33 | obtainedFunctionInvocations shouldBe 1 34 | Files.size(cachedFile) should be > 1024L 35 | Cache.remove(cacheKey) shouldBe true 36 | } 37 | 38 | "convenience function to download by URL" in { 39 | val cacheKey = newRandomCacheKey() 40 | Cache.remove(cacheKey) 41 | 42 | val cachedFile = Cache.getOrDownload(cacheKey, new URI("https://raw.githubusercontent.com/mpollmeier/scala-repl-pp/main/README.md")) 43 | 44 | Files.size(cachedFile) should be > 1024L 45 | Cache.remove(cacheKey) shouldBe true 46 | } 47 | 48 | private def newRandomCacheKey() = 49 | s"test-cacheKey-${UUID.randomUUID}".substring(0, 32) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /core/src/test/scala/replpp/util/ClasspathHelperTests.scala: -------------------------------------------------------------------------------- 1 | package replpp.util 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import replpp.Config 6 | 7 | import java.io.File.pathSeparator 8 | 9 | class ClasspathHelperTests extends AnyWordSpec with Matchers { 10 | 11 | "basic generation" in { 12 | ClasspathHelper.fromConfig(Config()).size should be > 2 13 | // exact content depends on test run environment, since the current classpath is included as well 14 | } 15 | 16 | "must start and end with pathSeparator" in { 17 | // to circumvent a flakiness that caused much headaches 18 | val cp = ClasspathHelper.create(Config()) 19 | cp should startWith(pathSeparator) 20 | cp should endWith(pathSeparator) 21 | } 22 | 23 | "resolves dependencies" when { 24 | if (scala.util.Properties.isWin) { 25 | info("test for dependency resolution disabled on windows - in general it works, but it's flaky :(") 26 | } else { 27 | "declared in config" in { 28 | val config = Config(classpathConfig = Config.ForClasspath(dependencies = Seq( 29 | "org.scala-lang:scala-library:2.13.10", 30 | "org.scala-lang::scala3-library:3.3.0", 31 | ))) 32 | val deps = ClasspathHelper.dependencyArtifacts(config) 33 | deps.size shouldBe 2 34 | 35 | assert(deps.find(_.endsWith("scala3-library_3-3.3.0.jar")).isDefined) 36 | assert(deps.find(_.endsWith("scala-library-2.13.10.jar")).isDefined) 37 | } 38 | 39 | "declared in scriptFile" in { 40 | val script = os.temp("//> using dep com.michaelpollmeier::colordiff:0.36") 41 | val deps = ClasspathHelper.dependencyArtifacts(Config(scriptFile = Some(script.toNIO))) 42 | deps.size shouldBe 4 43 | 44 | assert(deps.find(_.endsWith("colordiff_3-0.36.jar")).isDefined) 45 | assert(deps.find(_.endsWith("scala3-library_3-3.3.0.jar")).isDefined) 46 | assert(deps.find(_.endsWith("diffutils-1.3.0.jar")).isDefined) 47 | assert(deps.find(_.endsWith("scala-library-2.13.10.jar")).isDefined) 48 | } 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /core/src/test/scala/replpp/util/ProjectRoot.scala: -------------------------------------------------------------------------------- 1 | package replpp.util 2 | 3 | import scala.annotation.tailrec 4 | import java.nio.file.{Files, Path, Paths} 5 | 6 | /** Finds the relative location of the project root. 7 | * 8 | * Used in tests which rely on the working directory - unfortunately Intellij and sbt have different default working 9 | * directories for executing tests from subprojects: while sbt defaults to the project root, intellij defaults to the 10 | * subproject. 11 | * 12 | * Previously a consistent behaviour was achieved by setting `Test / baseDirectory := (ThisBuild / Test / run / 13 | * baseDirectory).value`, however that broke the bsp build within Intellij - it simply wouldn't recognise subprojects 14 | * with this setting any more. 15 | */ 16 | object ProjectRoot { 17 | 18 | private val SEARCH_DEPTH = 4 19 | object SearchDepthExceededError extends Error 20 | 21 | def relativise(path: String): Path = 22 | Paths.get(s"./$find/$path") 23 | 24 | def find: Path = { 25 | val fileThatOnlyExistsInRoot = ".git" 26 | 27 | @tailrec def loop(depth: Int): Path = { 28 | val rootDirCandidate = Paths.get("../" * depth) 29 | 30 | if (Files.exists(rootDirCandidate.resolve(fileThatOnlyExistsInRoot))) 31 | rootDirCandidate 32 | else if (depth < SEARCH_DEPTH) 33 | loop(depth + 1) 34 | else 35 | throw SearchDepthExceededError 36 | } 37 | 38 | loop(0) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mpollmeier/scala-repl-pp/67bd88c7677878d9814805863fc0c240dfe85cb8/demo.gif -------------------------------------------------------------------------------- /demo.txt: -------------------------------------------------------------------------------- 1 | prereq: shorten the prompt in ~/.zshenv - search for PS1 2 | asciinema rec --idle-time-limit 2 --title scala-repl-pp --overwrite demo.cast 3 | 4 | ./srp 5 | case class Person(name: String) 6 | 1.to(10).map(i => Person(s"Foo Bar $i")) 7 | 8 | val people = 1.to(1000).map(i => Person(s"Foo Bar $i")) 9 | 10 | people #| ("grep", "20") 11 | people #|^ "less" 12 | people #> "out.txt" 13 | :exit 14 | cat out.txt 15 | 16 | ./srp --dep com.michaelpollmeier:versionsort:1.0.14 17 | versionsort.VersionHelper.compare("0.9.0", "0.10.0") 18 | 19 | cat test-simple.sc 20 | ./srp --script test-simple.sc 21 | 22 | simple script with args: 23 | cat test-main-withargs.sc 24 | ./srp --script test-main-withargs.sc --param first=Foo --param last=Bar 25 | 26 | demo project: 27 | cd core/src/test/resources/demo-project 28 | ./stringcalc 29 | stringcalc> add(One, Two) 30 | 31 | cat plus.sc 32 | ./stringcalc --script plus.sc 33 | 34 | 35 | agg --rows 18 --cols 60 demo.cast demo.gif 36 | agg --rows 12 --cols 60 demo.cast demo-smaller.gif 37 | -------------------------------------------------------------------------------- /integration-tests/src/test/scala/replpp/server/EmbeddedReplTests.scala: -------------------------------------------------------------------------------- 1 | package replpp.server 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import scala.concurrent.Await 6 | import scala.concurrent.duration.Duration 7 | 8 | /** Moved to integrationTests, because of some strange interaction with ReplServerTests: 9 | * if EmbeddedReplTests would run *before* ReplServerTests, the latter would stall (forever) 10 | * after a few sucessful tests. 11 | * Run with `sbt integrationTest/test` 12 | */ 13 | class EmbeddedReplTests extends AnyWordSpec with Matchers { 14 | 15 | "execute commands synchronously" in { 16 | val repl = new EmbeddedRepl(defaultCompilerArgs, runBeforeCode = Seq("import Short.MaxValue")) 17 | 18 | repl.query("val x = MaxValue").output.trim shouldBe "val x: Short = 32767" 19 | repl.query("x + 1").output.trim shouldBe "val res0: Int = 32768" 20 | 21 | repl.shutdown() 22 | } 23 | 24 | "execute a command asynchronously" in { 25 | val repl = new EmbeddedRepl(defaultCompilerArgs, runBeforeCode = Seq("import Short.MaxValue")) 26 | val (uuid, futureResult) = repl.queryAsync("val x = MaxValue") 27 | val result = Await.result(futureResult, Duration.Inf) 28 | result.trim shouldBe "val x: Short = 32767" 29 | repl.shutdown() 30 | } 31 | 32 | val defaultCompilerArgs = { 33 | val inheritedClasspath = System.getProperty("java.class.path") 34 | Array( 35 | "-classpath", inheritedClasspath, 36 | "-explain", // verbose scalac error messages 37 | "-deprecation", 38 | "-color", "never" 39 | ) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt.* 2 | 3 | object Build { 4 | 5 | def newProject(internalName: String, scalaVersion: String, moduleName: String): Project = { 6 | val internalId = s"${internalName}_${scalaVersion}".replaceAll("\\.", "") 7 | val baseDir = file(internalName) 8 | 9 | Project(internalId, baseDir).settings( 10 | Keys.name := moduleName, 11 | Keys.scalaVersion := scalaVersion, 12 | Compile/Keys.unmanagedSourceDirectories += (Compile/Keys.sourceDirectory).value / s"scala-$scalaVersion", 13 | Keys.target := Keys.target.value / scalaVersion, 14 | Keys.crossVersion := CrossVersion.full, 15 | ) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.11 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.4") 2 | addSbtPlugin("io.shiftleft" % "sbt-ci-release-early" % "2.0.49") 3 | -------------------------------------------------------------------------------- /server/src/main/scala/replpp/server/Config.scala: -------------------------------------------------------------------------------- 1 | package replpp.server 2 | 3 | import replpp.shaded.scopt.{OParser, OParserBuilder} 4 | 5 | case class Config(baseConfig: replpp.Config, 6 | serverHost: String = "localhost", 7 | serverPort: Int = 8080, 8 | serverAuthUsername: Option[String] = None, 9 | serverAuthPassword: Option[String] = None) 10 | 11 | object Config { 12 | 13 | def parse(args: Array[String]): Config = { 14 | given builder: OParserBuilder[Config] = OParser.builder[Config] 15 | import builder.* 16 | val parser = OParser.sequence( 17 | programName("scala-repl-pp-server"), 18 | replpp.Config.opts.predef((x, c) => c.copy(baseConfig = c.baseConfig.copy(predefFiles = c.baseConfig.predefFiles :+ x))), 19 | replpp.Config.opts.runBefore((x, c) => c.copy(baseConfig = c.baseConfig.copy(runBefore = c.baseConfig.runBefore :+ x))), 20 | replpp.Config.opts.runAfter((x, c) => c.copy(baseConfig = c.baseConfig.copy(runAfter = c.baseConfig.runAfter :+ x))), 21 | replpp.Config.opts.verbose((_, c) => c.copy(baseConfig = c.baseConfig.copy(verbose = true))), 22 | replpp.Config.opts.inheritClasspath((_, c) => c.copy(baseConfig = c.baseConfig.copy(classpathConfig = c.baseConfig.classpathConfig.copy(inheritClasspath = true)))), 23 | replpp.Config.opts.classpathIncludesEntry((x, c) => { 24 | val bc = c.baseConfig 25 | val cpc = bc.classpathConfig 26 | c.copy(baseConfig = bc.copy(classpathConfig = cpc.copy(inheritClasspathIncludes = cpc.inheritClasspathIncludes :+ x))) 27 | }), 28 | replpp.Config.opts.classpathExcludesEntry((x, c) => { 29 | val bc = c.baseConfig 30 | val cpc = bc.classpathConfig 31 | c.copy(baseConfig = bc.copy(classpathConfig = cpc.copy(inheritClasspathExcludes = cpc.inheritClasspathExcludes :+ x))) 32 | }), 33 | replpp.Config.opts.dependency((x, c) => c.copy(baseConfig = c.baseConfig.copy(classpathConfig = c.baseConfig.classpathConfig.copy(dependencies = c.baseConfig.classpathConfig.dependencies :+ x)))), 34 | replpp.Config.opts.repo((x, c) => c.copy(baseConfig = c.baseConfig.copy(classpathConfig = c.baseConfig.classpathConfig.copy(resolvers = c.baseConfig.classpathConfig.resolvers :+ x)))), 35 | replpp.Config.opts.remoteJvmDebug((_, c) => c.copy(baseConfig = c.baseConfig.copy(remoteJvmDebugEnabled = true))), 36 | 37 | note("Server mode"), 38 | opt[Unit]("colors") 39 | .action((_, c) => c.copy(baseConfig = c.baseConfig.copy(nocolors = false))) 40 | .text("use colored output (disabled by default for server mode)"), 41 | 42 | opt[String]("server-host") 43 | .action((x, c) => c.copy(serverHost = x)) 44 | .text("Hostname on which to expose the REPL server"), 45 | 46 | opt[Int]("server-port") 47 | .action((x, c) => c.copy(serverPort = x)) 48 | .text("Port on which to expose the REPL server"), 49 | 50 | opt[String]("server-auth-username") 51 | .action((x, c) => c.copy(serverAuthUsername = Option(x))) 52 | .text("Basic auth username for the REPL server"), 53 | 54 | opt[String]("server-auth-password") 55 | .action((x, c) => c.copy(serverAuthPassword = Option(x))) 56 | .text("Basic auth password for the REPL server"), 57 | 58 | help("help").text("Print this help text"), 59 | ) 60 | 61 | OParser.parse(parser, args, Config(replpp.Config(nocolors = true))) 62 | .getOrElse(throw new AssertionError("error while parsing commandline args - see errors above")) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /server/src/main/scala/replpp/server/EmbeddedRepl.scala: -------------------------------------------------------------------------------- 1 | package replpp.server 2 | 3 | import dotty.tools.repl.State 4 | import org.slf4j.LoggerFactory 5 | import replpp.Colors.BlackWhite 6 | import replpp.{ReplDriverBase, pwd} 7 | 8 | import java.io.* 9 | import java.nio.charset.StandardCharsets 10 | import java.util.UUID 11 | import java.util.concurrent.Executors 12 | import scala.concurrent.duration.Duration 13 | import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutorService, Future} 14 | 15 | class EmbeddedRepl( 16 | compilerArgs: Array[String], 17 | runBeforeCode: Seq[String] = Nil, 18 | runAfterCode: Seq[String] = Nil, 19 | verbose: Boolean = false) { 20 | private val logger = LoggerFactory.getLogger(getClass) 21 | 22 | /** repl and compiler output ends up in this replOutputStream */ 23 | private val replOutputStream = new ByteArrayOutputStream() 24 | 25 | private val replDriver = ReplDriver(compilerArgs, new PrintStream(replOutputStream), classLoader = None) 26 | 27 | private var state: State = { 28 | if (runBeforeCode.nonEmpty) 29 | replDriver.execute(runBeforeCode) 30 | else 31 | replDriver.initialState 32 | } 33 | 34 | private val singleThreadedJobExecutor: ExecutionContextExecutorService = 35 | ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor()) 36 | 37 | /** Execute `inputLines` in REPL (in single threaded ExecutorService) and provide Future for result callback */ 38 | def queryAsync(code: String): (UUID, Future[String]) = 39 | queryAsync(code.linesIterator) 40 | 41 | /** Execute `inputLines` in REPL (in single threaded ExecutorService) and provide Future for result callback */ 42 | def queryAsync(inputLines: IterableOnce[String]): (UUID, Future[String]) = { 43 | val uuid = UUID.randomUUID() 44 | val future = Future { 45 | state = replDriver.execute(inputLines)(using state) 46 | readAndResetReplOutputStream() 47 | } (using singleThreadedJobExecutor) 48 | 49 | (uuid, future) 50 | } 51 | 52 | private def readAndResetReplOutputStream(): String = { 53 | val result = replOutputStream.toString(StandardCharsets.UTF_8) 54 | replOutputStream.reset() 55 | result 56 | } 57 | 58 | /** Submit query to the repl, await and return results. */ 59 | def query(code: String): QueryResult = 60 | query(code.linesIterator) 61 | 62 | /** Submit query to the repl, await and return results. */ 63 | def query(inputLines: IterableOnce[String]): QueryResult = { 64 | val (uuid, futureResult) = queryAsync(inputLines) 65 | val result = Await.result(futureResult, Duration.Inf) 66 | QueryResult(result, uuid, success = true) 67 | } 68 | 69 | /** Shutdown the embedded shell and associated threads. 70 | */ 71 | def shutdown(): Unit = { 72 | logger.info("shutting down") 73 | if (runAfterCode.nonEmpty) { 74 | if (verbose) logger.info(s"executing: $runAfterCode") 75 | replDriver.execute(runAfterCode) 76 | } 77 | singleThreadedJobExecutor.shutdown() 78 | } 79 | } 80 | 81 | class ReplDriver(args: Array[String], out: PrintStream, classLoader: Option[ClassLoader]) 82 | extends ReplDriverBase(args, out, maxHeight = None, classLoader)(using BlackWhite) { 83 | def execute(inputLines: IterableOnce[String])(using state: State = initialState): State = 84 | interpretInput(inputLines, state, pwd) 85 | } 86 | -------------------------------------------------------------------------------- /server/src/main/scala/replpp/server/Main.scala: -------------------------------------------------------------------------------- 1 | package replpp.server 2 | 3 | object Main { 4 | def main(args: Array[String]): Unit = { 5 | val config = Config.parse(args) 6 | ReplServer.startHttpServer(config) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /server/src/main/scala/replpp/server/ReplServer.scala: -------------------------------------------------------------------------------- 1 | package replpp.server 2 | 3 | import cask.model.{Request, Response} 4 | import org.slf4j.LoggerFactory 5 | import replpp.precompilePredefFiles 6 | import ujson.Obj 7 | 8 | import java.io.{PrintWriter, StringWriter} 9 | import java.util.UUID 10 | import scala.util.{Failure, Success} 11 | 12 | /** Result of executing a query, containing in particular output received on standard out. */ 13 | case class QueryResult(output: String, uuid: UUID, success: Boolean) extends HasUUID 14 | 15 | object ReplServer { 16 | private val logger = LoggerFactory.getLogger(getClass) 17 | 18 | def startHttpServer(serverConfig: Config): Unit = { 19 | val authenticationMaybe = for { 20 | username <- serverConfig.serverAuthUsername 21 | password <- serverConfig.serverAuthPassword 22 | } yield UsernamePasswordAuth(username, password) 23 | 24 | val baseConfig = precompilePredefFiles(serverConfig.baseConfig) 25 | val compilerArgs = replpp.compilerArgs(baseConfig) 26 | val embeddedRepl = new EmbeddedRepl(compilerArgs, baseConfig.runBefore, baseConfig.runAfter, baseConfig.verbose) 27 | Runtime.getRuntime.addShutdownHook(new Thread(() => { 28 | logger.info("Shutting down embedded repl...") 29 | embeddedRepl.shutdown() 30 | })) 31 | 32 | val server = new ReplServer(embeddedRepl, serverConfig.serverHost, serverConfig.serverPort, authenticationMaybe) 33 | logger.info(s"Starting REPL server on ${serverConfig.serverHost}:${serverConfig.serverPort}") 34 | try { 35 | server.main(Array.empty) 36 | } catch { 37 | case _: java.net.BindException => 38 | logger.error(s"Could not bind socket on port ${serverConfig.serverPort} - exiting.") 39 | embeddedRepl.shutdown() 40 | System.exit(1) 41 | case e: Throwable => 42 | logger.error("Unhandled exception thrown while attempting to start server - exiting", e) 43 | 44 | embeddedRepl.shutdown() 45 | System.exit(1) 46 | } 47 | } 48 | } 49 | 50 | class ReplServer(repl: EmbeddedRepl, 51 | host: String, 52 | port: Int, 53 | authenticationMaybe: Option[UsernamePasswordAuth] = None) 54 | extends WebServiceWithWebSocket[QueryResult](host, port, authenticationMaybe) { 55 | 56 | @cask.websocket("/connect") 57 | override def handler(): cask.WebsocketResult = super.handler() 58 | 59 | @basicAuth() 60 | @cask.get("/result/:uuidParam") 61 | override def getResult(uuidParam: String)(isAuthorized: Boolean): Response[Obj] = { 62 | val response = super.getResult(uuidParam)(isAuthorized) 63 | logger.debug(s"GET /result/$uuidParam: statusCode=${response.statusCode}") 64 | response 65 | } 66 | 67 | @basicAuth() 68 | @cask.postJson("/query") 69 | def postQuery(query: String)(isAuthorized: Boolean): Response[Obj] = { 70 | if (!isAuthorized) unauthorizedResponse 71 | else { 72 | val (uuid, resultFuture) = repl.queryAsync(query.linesIterator) 73 | logger.debug(s"query[uuid=$uuid, length=${query.length}]: submitted to queue") 74 | resultFuture.onComplete { 75 | case Success(output) => 76 | logger.debug(s"query[uuid=$uuid]: got result (length=${output.length})") 77 | returnResult(QueryResult(output, uuid, success = true)) 78 | case Failure(exception) => 79 | logger.info(s"query[uuid=$uuid] failed with $exception") 80 | returnResult(QueryResult(render(exception), uuid, success = false)) 81 | } 82 | Response(ujson.Obj("success" -> true, "uuid" -> uuid.toString), 200) 83 | } 84 | } 85 | 86 | @basicAuth() 87 | @cask.postJson("/query-sync") 88 | def postQuerySimple(query: String)(isAuthorized: Boolean): Response[Obj] = { 89 | if (!isAuthorized) unauthorizedResponse 90 | else { 91 | logger.debug(s"POST /query-sync query.length=${query.length}") 92 | val result = repl.query(query.linesIterator) 93 | logger.debug(s"query-sync: got result: length=${result.output.length}") 94 | Response(ujson.Obj("success" -> true, "stdout" -> result.output, "uuid" -> result.uuid.toString), 200) 95 | } 96 | } 97 | 98 | override def resultToJson(result: QueryResult, success: Boolean): Obj = { 99 | ujson.Obj("success" -> success, "uuid" -> result.uuid.toString, "stdout" -> result.output) 100 | } 101 | 102 | private def render(throwable: Throwable): String = { 103 | val sw = new StringWriter 104 | throwable.printStackTrace(new PrintWriter(sw)) 105 | throwable.getMessage() + System.lineSeparator() + sw.toString() 106 | } 107 | 108 | initialize() 109 | } 110 | -------------------------------------------------------------------------------- /server/src/main/scala/replpp/server/WebServiceWithWebSocket.scala: -------------------------------------------------------------------------------- 1 | package replpp.server 2 | 3 | import cask.model.Response.Raw 4 | import cask.model.{Request, Response} 5 | import cask.router.Result 6 | import org.slf4j.LoggerFactory 7 | import ujson.Obj 8 | 9 | import java.nio.charset.StandardCharsets 10 | import java.security.MessageDigest 11 | import java.util.concurrent.ConcurrentHashMap 12 | import java.util.{Base64, UUID} 13 | import scala.util.{Failure, Success, Try} 14 | 15 | trait HasUUID { def uuid: UUID } 16 | 17 | case class UsernamePasswordAuth(username: String, password: String) 18 | 19 | abstract class WebServiceWithWebSocket[T <: HasUUID]( 20 | override val host: String, 21 | override val port: Int, 22 | authenticationMaybe: Option[UsernamePasswordAuth] = None) extends cask.MainRoutes { 23 | protected val logger = LoggerFactory.getLogger(getClass) 24 | 25 | class basicAuth extends cask.RawDecorator { 26 | private lazy val utf8 = StandardCharsets.UTF_8 27 | def wrapFunction(request: Request, delegate: Delegate): Result[Raw] = { 28 | val isAuthorized = authenticationMaybe match { 29 | case None => true // no authorization required 30 | case Some(requiredAuth) => 31 | parseAuthentication(request) match { 32 | case None => false // no authentication provided 33 | case Some(providedAuth) => areEqual(providedAuth, requiredAuth) 34 | } 35 | } 36 | delegate(request, Map("isAuthorized" -> isAuthorized)) 37 | } 38 | 39 | private def parseAuthentication(request: Request): Option[UsernamePasswordAuth] = 40 | Try { 41 | val authHeader = request.exchange.getRequestHeaders.get("authorization").getFirst 42 | val strippedHeader = authHeader.replaceFirst("Basic ", "") 43 | val authString = new String(Base64.getDecoder.decode(strippedHeader)) 44 | authString.split(":", 2) match { 45 | case Array(username, password) => Some(UsernamePasswordAuth(username, password)) 46 | case _ => None 47 | } 48 | }.toOption.flatten 49 | 50 | /* constant-time comparison that prevents leaking the expected password through a timing side-channel */ 51 | private def areEqual(providedAuth: UsernamePasswordAuth, expectedAuth: UsernamePasswordAuth): Boolean = 52 | MessageDigest.isEqual(providedAuth.toString.getBytes(utf8), expectedAuth.toString.getBytes(utf8)) 53 | } 54 | 55 | private var openConnections = Set.empty[cask.WsChannelActor] 56 | private val resultMap = new ConcurrentHashMap[UUID, (T, Boolean)]() 57 | protected val unauthorizedResponse = Response(ujson.Obj(), 401, headers = Seq("WWW-Authenticate" -> "Basic")) 58 | 59 | def handler(): cask.WebsocketResult = { 60 | cask.WsHandler { connection => 61 | connection.send(cask.Ws.Text("connected")) 62 | openConnections += connection 63 | cask.WsActor { 64 | case cask.Ws.Error(e) => 65 | logger.error("Connection error", e) 66 | openConnections -= connection 67 | case cask.Ws.Close(_, _) | cask.Ws.ChannelClosed() => 68 | logger.debug("Connection closed.") 69 | openConnections -= connection 70 | } 71 | } 72 | } 73 | 74 | def getResult(uuidParam: String)(isAuthorized: Boolean): Response[Obj] = { 75 | if (!isAuthorized) { 76 | unauthorizedResponse 77 | } else { 78 | Try(UUID.fromString(uuidParam)) match { 79 | case Success(uuid) if !resultMap.containsKey(uuid) => 80 | Response(ujson.Obj("success" -> false, "err" -> "No result (yet?) found for specified UUID"), 200) 81 | case Success(uuid) => 82 | val (result, success) = resultMap.remove(uuid) 83 | Response(resultToJson(result, success), 200) 84 | case Failure(_) => 85 | Response(ujson.Obj("success" -> false, "err" -> "UUID parameter is incorrectly formatted"), 200) 86 | } 87 | } 88 | } 89 | 90 | def returnResult(result: T): Unit = { 91 | resultMap.put(result.uuid, (result, true)) 92 | openConnections.foreach { connection => 93 | connection.send(cask.Ws.Text(result.uuid.toString)) 94 | } 95 | Response(ujson.Obj("success" -> true, "uuid" -> result.uuid.toString), 200) 96 | } 97 | 98 | def resultToJson(result: T, success: Boolean): Obj 99 | 100 | initialize() 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /server/src/test/scala/replpp/server/ConfigTests.scala: -------------------------------------------------------------------------------- 1 | package replpp.server 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | import replpp.Colors 6 | 7 | import java.nio.file.Paths 8 | 9 | class ConfigTests extends AnyWordSpec with Matchers { 10 | val apacheRepo = "https://repository.apache.org/content/groups/public" 11 | 12 | "parse server and base config combined" in { 13 | val parsed = Config.parse(Array( 14 | "--server-host", "testHost", 15 | "--server-port", "42", 16 | "--server-auth-username", "test-user", 17 | "--server-auth-password", "test-pass", 18 | "--verbose", 19 | "--predef", "test-predef.sc", 20 | "--runBefore", "val foo = 42", 21 | "--runAfter", """println("goodbye")""", 22 | )) 23 | parsed.serverHost shouldBe "testHost" 24 | parsed.serverPort shouldBe 42 25 | parsed.serverAuthUsername shouldBe Some("test-user") 26 | parsed.serverAuthPassword shouldBe Some("test-pass") 27 | parsed.baseConfig.verbose shouldBe true 28 | parsed.baseConfig.predefFiles shouldBe Seq(Paths.get("test-predef.sc")) 29 | parsed.baseConfig.runBefore shouldBe Seq("val foo = 42") 30 | parsed.baseConfig.runAfter shouldBe Seq("""println("goodbye")""") 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/fansi/LICENSE: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2016 Li Haoyi (haoyi.sg@gmail.com) 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/geny/ByteData.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | package geny 3 | 4 | import java.nio.charset.StandardCharsets 5 | 6 | import scala.io.Codec 7 | 8 | /** 9 | * Encapsulates an `Array[Byte]` and provides convenience methods for 10 | * reading the data out of it. 11 | */ 12 | trait ByteData { 13 | 14 | def bytes: Array[Byte] 15 | 16 | def text(): String = text(StandardCharsets.UTF_8) 17 | def text(codec: Codec): String = new String(bytes, codec.charSet) 18 | 19 | def trim(): String = trim(StandardCharsets.UTF_8) 20 | def trim(codec: Codec): String = text(codec).trim 21 | 22 | def lines(): Vector[String] = lines(StandardCharsets.UTF_8) 23 | def lines(codec: Codec): Vector[String] = Predef.augmentString(text(codec)).lines.toVector 24 | } 25 | object ByteData{ 26 | case class Chunks(chunks: Seq[Bytes]) extends ByteData{ 27 | lazy val bytes = chunks.iterator.map(_.array).toArray.flatten 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/geny/Bytes.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | package geny 3 | 4 | /** 5 | * Trivial wrapper around `Array[Byte]` with sane equality and useful toString 6 | */ 7 | class Bytes(val array: Array[Byte]){ 8 | override def equals(other: Any) = other match{ 9 | case otherBytes: Bytes => java.util.Arrays.equals(array, otherBytes.array) 10 | case _ => false 11 | } 12 | override def toString = new String(array, java.nio.charset.StandardCharsets.UTF_8) 13 | } 14 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/geny/Internal.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | package geny 3 | 4 | import java.io.{InputStream, OutputStream} 5 | import java.lang.Character.MAX_SURROGATE 6 | import java.lang.Character.MIN_SURROGATE 7 | 8 | object Internal { 9 | val defaultMaxBufferStartSize: Int = 64 * 1024 10 | val defaultMinBufferStartSize: Int = 64 11 | val defaultBufferGrowthRatio: Int = 4 12 | 13 | /** 14 | * Transfers data from the given `InputStream` through a to a [[sink]] 15 | * function through a dynamically sized transfer buffer. 16 | * 17 | * The buffer is sized based on the `.available` number on the input stream, 18 | * clamped a [[minBufferSize]] and [[maxBufferSize]] size, and allowed to 19 | * grow in increments of 2x if the total amount of bytes transferred exceeds 20 | * the size of the buffer by [[bufferGrowthRatio]]. 21 | * 22 | * This should allow efficient processing of `InputStream`s of all sizes, 23 | * from the tiny to the very large, without over- or under- allocating the 24 | * size of the transfer buffer. 25 | */ 26 | def transfer0(src: InputStream, 27 | sink: (Array[Byte], Int) => Unit, 28 | minBufferSize: Int = defaultMinBufferStartSize, 29 | maxBufferSize: Int = defaultMaxBufferStartSize, 30 | bufferGrowthRatio: Int = defaultBufferGrowthRatio) = { 31 | def clampBufferSize(n: Int) = { 32 | if (n < minBufferSize) minBufferSize 33 | else if (n > maxBufferSize) maxBufferSize 34 | else n 35 | } 36 | 37 | var buffer = new Array[Byte](clampBufferSize(src.available())) 38 | var total = 0 39 | var read = 0 40 | while (read != -1) { 41 | read = src.read(buffer) 42 | if (read != -1) { 43 | sink(buffer, read) 44 | total += read 45 | if (total > buffer.length * bufferGrowthRatio && buffer.length < maxBufferSize) { 46 | buffer = new Array[Byte](clampBufferSize(buffer.length * 2)) 47 | } 48 | } 49 | } 50 | src.close() 51 | } 52 | 53 | def transfer(src: InputStream, 54 | dest: OutputStream, 55 | minBufferSize: Int = defaultMinBufferStartSize, 56 | maxBufferSize: Int = defaultMaxBufferStartSize, 57 | bufferGrowthRatio: Int = defaultBufferGrowthRatio) = transfer0( 58 | src, 59 | dest.write(_, 0, _), 60 | minBufferSize, 61 | maxBufferSize, 62 | bufferGrowthRatio 63 | ) 64 | 65 | // VENDORED FROM GUAVA 66 | def encodedLength(sequence: String) = { // Warning to maintainers: this implementation is highly optimized. 67 | val utf16Length = sequence.length 68 | var utf8Length = utf16Length 69 | var i = 0 70 | // This loop optimizes for pure ASCII. 71 | while (i < utf16Length && sequence.charAt(i) < 0x80) i += 1 72 | // This loop optimizes for chars less than 0x800. 73 | 74 | while (i < utf16Length) { 75 | val c = sequence.charAt(i) 76 | if (c < 0x800) utf8Length += ((0x7f - c) >>> 31) // branch free! 77 | else { 78 | utf8Length += encodedLengthGeneral(sequence, i) 79 | i = utf16Length // break is not supported, just set i to terminate the loop 80 | 81 | } 82 | 83 | i += 1 84 | } 85 | if (utf8Length < utf16Length) { // Necessary and sufficient condition for overflow because of maximum 3x expansion 86 | throw new IllegalArgumentException("UTF-8 length does not fit in int: " + (utf8Length + (1L << 32))) 87 | } 88 | utf8Length 89 | } 90 | 91 | private def encodedLengthGeneral(sequence: String, start: Int) = { 92 | val utf16Length = sequence.length 93 | var utf8Length = 0 94 | var i = start 95 | while (i < utf16Length) { 96 | val c = sequence.charAt(i) 97 | if (c < 0x800) utf8Length += (0x7f - c) >>> 31 98 | else { 99 | utf8Length += 2 100 | // jdk7+: if (Character.isSurrogate(c)) { 101 | if (MIN_SURROGATE <= c && c <= MAX_SURROGATE) { // Check that we have a well-formed surrogate pair. 102 | if (Character.codePointAt(sequence, i) == c) throw new IllegalArgumentException(unpairedSurrogateMsg(i)) 103 | i += 1 104 | } 105 | } 106 | 107 | i += 1 108 | } 109 | utf8Length 110 | } 111 | 112 | private def unpairedSurrogateMsg(i: Int) = "Unpaired surrogate at index " + i 113 | // END VENDORED FROM GUAVA 114 | } 115 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/geny/Writable.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | package geny 3 | import java.io.{ByteArrayInputStream, InputStream, OutputStream, OutputStreamWriter} 4 | import java.nio.charset.StandardCharsets 5 | 6 | import sun.nio.cs.StreamEncoder 7 | 8 | /** 9 | * A [[Writable]] is a source of bytes that can be written to an OutputStream. 10 | * 11 | * Essentially a push-based version of `java.io.InputStream`, that allows an 12 | * implementation to guarantee that cleanup logic runs after the bytes are 13 | * written. 14 | * 15 | * [[Writable]] is also much easier to implement than `java.io.InputStream`: any 16 | * code that previously wrote output to an `ByteArrayOutputStream` or 17 | * `StringBuilder` can trivially satisfy the [[Writable]] interface. That makes 18 | * [[Writable]] very convenient to use for allowing zero-friction zero-overhead 19 | * streaming data exchange between different libraries. 20 | * 21 | * [[Writable]] comes with implicit constructors from `Array[Byte]`, `String` 22 | * and `InputStream`, and is itself a tiny interface with minimal functionality. 23 | * Libraries using [[Writable]] are expected to extend it to provide additional 24 | * methods or additional implicit constructors that make sense in their context. 25 | */ 26 | trait Writable extends Any{ 27 | def writeBytesTo(out: OutputStream): Unit 28 | def httpContentType: Option[String] = None 29 | def contentLength: Option[Long] = None 30 | } 31 | object Writable extends LowPriWritable { 32 | implicit class StringWritable(s: String) extends Writable{ 33 | def writeBytesTo(out: OutputStream): Unit = { 34 | 35 | s.grouped(8192).foreach(ss => out.write(ss.getBytes(StandardCharsets.UTF_8))) 36 | } 37 | override def httpContentType = Some("text/plain; charset=utf-8") 38 | override def contentLength = Some(Internal.encodedLength(s)) 39 | } 40 | 41 | implicit class ByteArrayWritable(a: Array[Byte]) extends Writable{ 42 | def writeBytesTo(out: OutputStream): Unit = out.write(a) 43 | override def httpContentType = Some("application/octet-stream") 44 | override def contentLength = Some(a.length) 45 | } 46 | 47 | implicit class ByteBufferWritable(buffer: java.nio.ByteBuffer) extends Writable { 48 | def writeBytesTo(out: OutputStream): Unit = { 49 | // TODO: there is room for optimization here. We could match on the output 50 | // stream and in case it has an underlying NIO channel, write the buffer 51 | // directly to it. E.g. 52 | // 53 | // out match { 54 | // case fs: java.io.FileOutputStream => fs.getChannel().write(buffer) 55 | // case _ => 56 | // } 57 | // 58 | // This optimization however is not available on ScalaJS, and hence 59 | // requires a restructuring of this source file. 60 | val bb = buffer.duplicate().order(buffer.order()) 61 | var tmp = new Array[Byte](8192) 62 | val length = bb.remaining() 63 | var count = 0 64 | while (count < length) { 65 | val l = math.min(tmp.size, length - count) 66 | bb.get(tmp, 0, l) 67 | out.write(tmp, 0, l) 68 | count += l 69 | } 70 | } 71 | override def httpContentType = Some("application/octet-stream") 72 | override def contentLength = Some(buffer.remaining()) 73 | } 74 | 75 | } 76 | 77 | trait LowPriWritable{ 78 | implicit def readableWritable[T](t: T)(implicit f: T => Readable): Writable = f(t) 79 | } 80 | 81 | /** 82 | * A [[Readable]] is a source of bytes that can be read from an InputStream 83 | * 84 | * A subtype of [[Writable]], every [[Readable]] can be trivially used as a 85 | * [[Writable]] by transferring the bytes from the InputStream to the OutputStream, 86 | * but not every [[Writable]] is a [[Readable]]. 87 | * 88 | * Note that the InputStream is only available inside the `readBytesThrough`, and 89 | * may be closed and cleaned up (along with any associated resources) once the 90 | * callback returns. 91 | */ 92 | trait Readable extends Writable{ 93 | def readBytesThrough[T](f: InputStream => T): T 94 | def writeBytesTo(out: OutputStream): Unit = readBytesThrough(Internal.transfer(_, out)) 95 | } 96 | object Readable{ 97 | implicit class StringReadable(s: String) extends Readable{ 98 | def readBytesThrough[T](f: InputStream => T): T = { 99 | f(new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8))) 100 | } 101 | override def httpContentType = Some("text/plain; charset=utf-8") 102 | override def contentLength = Some(Internal.encodedLength(s)) 103 | } 104 | 105 | implicit class ByteArrayReadable(a: Array[Byte]) extends Readable{ 106 | def readBytesThrough[T](f: InputStream => T): T = f(new ByteArrayInputStream(a)) 107 | override def httpContentType = Some("application/octet-stream") 108 | override def contentLength = Some(a.length) 109 | } 110 | 111 | implicit class ByteBufferReadable(buffer: java.nio.ByteBuffer) extends Readable{ 112 | def readBytesThrough[T](f: InputStream => T): T = { 113 | val bb = buffer.duplicate().order(buffer.order()) 114 | 115 | val is = new InputStream { 116 | override def read(): Int = if (!bb.hasRemaining()) { 117 | -1 118 | } else { 119 | bb.get() & 0xff 120 | } 121 | override def read(bytes: Array[Byte], off: Int, len: Int) = if (!bb.hasRemaining()) { 122 | -1 123 | } else { 124 | val l = math.min(len, bb.remaining()) 125 | bb.get(bytes, off, l) 126 | l 127 | } 128 | } 129 | 130 | f(is) 131 | } 132 | override def httpContentType = Some("application/octet-stream") 133 | override def contentLength = Some(buffer.remaining()) 134 | } 135 | 136 | implicit class InputStreamReadable(i: InputStream) extends Readable{ 137 | def readBytesThrough[T](f: InputStream => T): T = f(i) 138 | override def httpContentType: Option[String] = Some("application/octet-stream") 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/Annotations.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.mainargs 2 | import scala.annotation.ClassfileAnnotation 3 | 4 | class arg( 5 | val name: String = null, 6 | val short: Char = 0, 7 | val doc: String = null, 8 | val noDefaultName: Boolean = false, 9 | val positional: Boolean = false, 10 | val hidden: Boolean = false 11 | ) extends ClassfileAnnotation 12 | 13 | class main(val name: String = null, val doc: String = null) extends ClassfileAnnotation 14 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/Compat.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.mainargs 2 | 3 | object Compat { 4 | def exit(n: Int) = sys.exit(n) 5 | } 6 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/Flag.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.mainargs 2 | case class Flag(value: Boolean = false) 3 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/Invoker.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.mainargs 2 | 3 | object Invoker { 4 | def construct[T]( 5 | cep: TokensReader.Class[T], 6 | args: Seq[String], 7 | allowPositional: Boolean, 8 | allowRepeats: Boolean 9 | ): Result[T] = { 10 | TokenGrouping 11 | .groupArgs( 12 | args, 13 | cep.main.flattenedArgSigs, 14 | allowPositional, 15 | allowRepeats, 16 | cep.main.argSigs0.exists(_.reader.isLeftover) 17 | ) 18 | .flatMap((group: TokenGrouping[Any]) => invoke(cep.companion(), cep.main, group)) 19 | } 20 | 21 | def invoke0[T, B]( 22 | base: B, 23 | mainData: MainData[T, B], 24 | kvs: Map[ArgSig, Seq[String]], 25 | extras: Seq[String] 26 | ): Result[T] = { 27 | val readArgValues: Seq[Either[Result[Any], ParamResult[_]]] = 28 | for (a <- mainData.argSigs0) yield { 29 | a.reader match { 30 | case r: TokensReader.Flag => 31 | Right(ParamResult.Success(Flag(kvs.contains(a)).asInstanceOf[T])) 32 | case r: TokensReader.Simple[T] => Right(makeReadCall(kvs, base, a, r)) 33 | case r: TokensReader.Constant[T] => Right(r.read() match { 34 | case Left(s) => ParamResult.Failure(Seq(Result.ParamError.Failed(a, Nil, s))) 35 | case Right(v) => ParamResult.Success(v) 36 | }) 37 | case r: TokensReader.Leftover[T, _] => Right(makeReadVarargsCall(a, extras, r)) 38 | case r: TokensReader.Class[T] => 39 | Left( 40 | invoke0[T, B]( 41 | r.companion().asInstanceOf[B], 42 | r.main.asInstanceOf[MainData[T, B]], 43 | kvs, 44 | extras 45 | ) 46 | ) 47 | 48 | } 49 | } 50 | 51 | val validated = { 52 | val lefts = readArgValues 53 | .collect { 54 | case Left(Result.Failure.InvalidArguments(lefts)) => lefts 55 | case Right(ParamResult.Failure(failure)) => failure 56 | } 57 | .flatten 58 | if (lefts.nonEmpty) Result.Failure.InvalidArguments(lefts) 59 | else Result.Success( 60 | readArgValues.collect { 61 | case Left(Result.Success(x)) => x 62 | case Right(ParamResult.Success(x)) => x 63 | } 64 | ) 65 | } 66 | 67 | val res = validated.flatMap { validated => 68 | Result.Success(mainData.invokeRaw(base, validated)) 69 | } 70 | res 71 | } 72 | def invoke[T, B](target: B, main: MainData[T, B], grouping: TokenGrouping[B]): Result[T] = { 73 | try invoke0( 74 | target, 75 | main, 76 | grouping.grouped, 77 | grouping.remaining 78 | ) 79 | catch { case e: Throwable => Result.Failure.Exception(e) } 80 | } 81 | def runMains[B]( 82 | mains: MethodMains[B], 83 | args: Seq[String], 84 | allowPositional: Boolean, 85 | allowRepeats: Boolean 86 | ): Either[Result.Failure.Early, (MainData[Any, B], Result[Any])] = { 87 | def groupArgs(main: MainData[Any, B], argsList: Seq[String]) = { 88 | def invokeLocal(group: TokenGrouping[Any]) = 89 | invoke(mains.base(), main.asInstanceOf[MainData[Any, Any]], group) 90 | Right( 91 | main, 92 | TokenGrouping 93 | .groupArgs( 94 | argsList, 95 | main.flattenedArgSigs, 96 | allowPositional, 97 | allowRepeats, 98 | main.argSigs0.exists { 99 | case x: ArgSig => x.reader.isLeftover 100 | case _ => false 101 | } 102 | ) 103 | .flatMap(invokeLocal) 104 | ) 105 | } 106 | mains.value match { 107 | case Seq() => Left(Result.Failure.Early.NoMainMethodsDetected()) 108 | case Seq(main) => groupArgs(main, args) 109 | case multiple => 110 | args.toList match { 111 | case List() => Left(Result.Failure.Early.SubcommandNotSpecified(multiple.map(_.name))) 112 | case head :: tail => 113 | if (head.startsWith("-")) { 114 | Left(Result.Failure.Early.SubcommandSelectionDashes(head)) 115 | } else { 116 | multiple.find(_.name == head) match { 117 | case None => 118 | Left(Result.Failure.Early.UnableToFindSubcommand(multiple.map(_.name), head)) 119 | case Some(main) => groupArgs(main, tail) 120 | } 121 | } 122 | } 123 | } 124 | } 125 | 126 | def tryEither[T](t: => T, error: Throwable => Result.ParamError): Either[Result.ParamError, T] = { 127 | try Right(t) 128 | catch { case e: Throwable => Left(error(e)) } 129 | } 130 | def makeReadCall[T]( 131 | dict: Map[ArgSig, Seq[String]], 132 | base: Any, 133 | arg: ArgSig, 134 | reader: TokensReader.Simple[_] 135 | ): ParamResult[T] = { 136 | def prioritizedDefault = tryEither( 137 | arg.default.map(_(base)), 138 | Result.ParamError.DefaultFailed(arg, _) 139 | ) match { 140 | case Left(ex) => ParamResult.Failure(Seq(ex)) 141 | case Right(v) => ParamResult.Success(v) 142 | } 143 | val tokens = dict.get(arg) match { 144 | case None => if (reader.allowEmpty) Some(Nil) else None 145 | case Some(tokens) => Some(tokens) 146 | } 147 | val optResult = tokens match { 148 | case None => prioritizedDefault 149 | case Some(tokens) => 150 | tryEither( 151 | reader.read(tokens), 152 | Result.ParamError.Exception(arg, tokens, _) 153 | ) match { 154 | case Left(ex) => ParamResult.Failure(Seq(ex)) 155 | case Right(Left(errMsg)) => 156 | ParamResult.Failure(Seq(Result.ParamError.Failed(arg, tokens, errMsg))) 157 | case Right(Right(v)) => ParamResult.Success(Some(v)) 158 | } 159 | } 160 | optResult.map(_.get.asInstanceOf[T]) 161 | } 162 | 163 | def makeReadVarargsCall[T]( 164 | arg: ArgSig, 165 | values: Seq[String], 166 | reader: TokensReader.Leftover[_, _] 167 | ): ParamResult[T] = { 168 | val eithers = 169 | tryEither( 170 | reader.read(values), 171 | Result.ParamError.Exception(arg, values, _) 172 | ) match { 173 | case Left(x) => Left(x) 174 | case Right(Left(errMsg)) => Left(Result.ParamError.Failed(arg, values, errMsg)) 175 | case Right(Right(v)) => Right(v) 176 | } 177 | 178 | eithers match { 179 | case Left(s) => ParamResult.Failure(Seq(s)) 180 | case Right(v) => ParamResult.Success(v.asInstanceOf[T]) 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/LICENSE: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2014 Li Haoyi (haoyi.sg@gmail.com) 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/Leftover.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.mainargs 2 | case class Leftover[T](value: T*) 3 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/ParserForClassCompanionVersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.mainargs 2 | 3 | import scala.language.experimental.macros 4 | 5 | private [mainargs] trait ParserForClassCompanionVersionSpecific { 6 | inline def apply[T]: ParserForClass[T] = ${ Macros.parserForClass[T] } 7 | } 8 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/ParserForMethodsCompanionVersionSpecific.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.mainargs 2 | 3 | private [mainargs] trait ParserForMethodsCompanionVersionSpecific { 4 | inline def apply[B](base: B): ParserForMethods[B] = ${ Macros.parserForMethods[B]('base) } 5 | } 6 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/Result.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.mainargs 2 | 3 | /** 4 | * Represents what comes out of an attempt to invoke an [[Main]]. 5 | * Could succeed with a value, but could fail in many different ways. 6 | */ 7 | sealed trait Result[+T] { 8 | def map[V](f: T => V): Result[V] = this match { 9 | case Result.Success(v) => Result.Success(f(v)) 10 | case e: Result.Failure => e 11 | } 12 | def flatMap[V](f: T => Result[V]): Result[V] = this match { 13 | case Result.Success(v) => f(v) 14 | case e: Result.Failure => e 15 | } 16 | } 17 | object Result { 18 | 19 | /** 20 | * Invoking the [[Main]] was totally successful, and returned a 21 | * result 22 | */ 23 | case class Success[T](value: T) extends Result[T] 24 | 25 | /** 26 | * Invoking the [[Main]] was not successful 27 | */ 28 | sealed trait Failure extends Result[Nothing] 29 | object Failure { 30 | sealed trait Early extends Failure 31 | object Early { 32 | 33 | case class NoMainMethodsDetected() extends Early 34 | case class SubcommandNotSpecified(options: Seq[String]) extends Early 35 | case class UnableToFindSubcommand(options: Seq[String], token: String) extends Early 36 | case class SubcommandSelectionDashes(token: String) extends Early 37 | } 38 | 39 | /** 40 | * Invoking the [[Main]] failed with an exception while executing 41 | * code within it. 42 | */ 43 | case class Exception(t: Throwable) extends Failure 44 | 45 | /** 46 | * Invoking the [[Main]] failed because the arguments provided 47 | * did not line up with the arguments expected 48 | */ 49 | case class MismatchedArguments( 50 | missing: Seq[ArgSig] = Nil, 51 | unknown: Seq[String] = Nil, 52 | duplicate: Seq[(ArgSig, Seq[String])] = Nil, 53 | incomplete: Option[ArgSig] = None 54 | ) extends Failure 55 | 56 | /** 57 | * Invoking the [[Main]] failed because there were problems 58 | * deserializing/parsing individual arguments 59 | */ 60 | case class InvalidArguments(values: Seq[ParamError]) extends Failure 61 | } 62 | 63 | sealed trait ParamError 64 | object ParamError { 65 | 66 | /** 67 | * Something went wrong trying to de-serialize the input parameter 68 | */ 69 | case class Failed(arg: ArgSig, tokens: Seq[String], errMsg: String) 70 | extends ParamError 71 | 72 | /** 73 | * Something went wrong trying to de-serialize the input parameter; 74 | * the thrown exception is stored in [[ex]] 75 | */ 76 | case class Exception(arg: ArgSig, tokens: Seq[String], ex: Throwable) 77 | extends ParamError 78 | 79 | /** 80 | * Something went wrong trying to evaluate the default value 81 | * for this input parameter 82 | */ 83 | case class DefaultFailed(arg: ArgSig, ex: Throwable) extends ParamError 84 | } 85 | } 86 | 87 | sealed trait ParamResult[+T] { 88 | def map[V](f: T => V): ParamResult[V] = this match { 89 | case ParamResult.Success(v) => ParamResult.Success(f(v)) 90 | case e: ParamResult.Failure => e 91 | } 92 | def flatMap[V](f: T => ParamResult[V]): ParamResult[V] = this match { 93 | case ParamResult.Success(v) => f(v) 94 | case e: ParamResult.Failure => e 95 | } 96 | } 97 | object ParamResult { 98 | case class Failure(errors: Seq[Result.ParamError]) extends ParamResult[Nothing] 99 | case class Success[T](value: T) extends ParamResult[T] 100 | } 101 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/TokenGrouping.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.mainargs 2 | 3 | import scala.annotation.tailrec 4 | 5 | case class TokenGrouping[B](remaining: List[String], grouped: Map[ArgSig, Seq[String]]) 6 | 7 | object TokenGrouping { 8 | def groupArgs[B]( 9 | flatArgs0: Seq[String], 10 | argSigs: Seq[(ArgSig, TokensReader.Terminal[_])], 11 | allowPositional: Boolean, 12 | allowRepeats: Boolean, 13 | allowLeftover: Boolean 14 | ): Result[TokenGrouping[B]] = { 15 | val positionalArgSigs = argSigs.collect { 16 | case (a, r: TokensReader.Simple[_]) if allowPositional | a.positional => 17 | a 18 | } 19 | 20 | val flatArgs = flatArgs0.toList 21 | val keywordArgMap = argSigs 22 | .collect { 23 | case (a, r: TokensReader.Simple[_]) if !a.positional => a 24 | case (a, r: TokensReader.Flag) => a 25 | } 26 | .flatMap { x => (x.name.map("--" + _) ++ x.shortName.map("-" + _)).map(_ -> x) } 27 | .toMap[String, ArgSig] 28 | 29 | @tailrec def rec( 30 | remaining: List[String], 31 | current: Map[ArgSig, Vector[String]] 32 | ): Result[TokenGrouping[B]] = { 33 | remaining match { 34 | case head :: rest => 35 | if (head.startsWith("-") && head.exists(_ != '-')) { 36 | keywordArgMap.get(head) match { 37 | case Some(cliArg: ArgSig) if cliArg.reader.isFlag => 38 | rec(rest, Util.appendMap(current, cliArg, "")) 39 | case Some(cliArg: ArgSig) if !cliArg.reader.isLeftover => 40 | rest match { 41 | case next :: rest2 => rec(rest2, Util.appendMap(current, cliArg, next)) 42 | case Nil => 43 | Result.Failure.MismatchedArguments(Nil, Nil, Nil, incomplete = Some(cliArg)) 44 | } 45 | 46 | case _ => complete(remaining, current) 47 | } 48 | } else { 49 | positionalArgSigs.find(!current.contains(_)) match { 50 | case Some(nextInLine) => rec(rest, Util.appendMap(current, nextInLine, head)) 51 | case None => complete(remaining, current) 52 | } 53 | } 54 | 55 | case _ => complete(remaining, current) 56 | } 57 | } 58 | 59 | def complete( 60 | remaining: List[String], 61 | current: Map[ArgSig, Vector[String]] 62 | ): Result[TokenGrouping[B]] = { 63 | 64 | val duplicates = current 65 | .filter { 66 | case (a: ArgSig, vs) => 67 | a.reader match { 68 | case r: TokensReader.Flag => vs.size > 1 && !allowRepeats 69 | case r: TokensReader.Simple[_] => vs.size > 1 && !r.alwaysRepeatable && !allowRepeats 70 | case r: TokensReader.Leftover[_, _] => false 71 | case r: TokensReader.Constant[_] => false 72 | } 73 | 74 | } 75 | .toSeq 76 | 77 | val missing = argSigs.collect { 78 | case (a, r: TokensReader.Simple[_]) 79 | if !r.allowEmpty 80 | && a.default.isEmpty 81 | && !current.contains(a) => 82 | a 83 | } 84 | 85 | val unknown = if (allowLeftover) Nil else remaining 86 | if (missing.nonEmpty || duplicates.nonEmpty || unknown.nonEmpty) { 87 | Result.Failure.MismatchedArguments( 88 | missing = missing, 89 | unknown = unknown, 90 | duplicate = duplicates, 91 | incomplete = None 92 | ) 93 | } else Result.Success(TokenGrouping(remaining, current)) 94 | 95 | } 96 | rec(flatArgs, Map()) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/Util.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.mainargs 2 | 3 | import scala.annotation.{switch, tailrec} 4 | 5 | object Util { 6 | def literalize(s: IndexedSeq[Char], unicode: Boolean = false) = { 7 | val sb = new StringBuilder 8 | sb.append('"') 9 | var i = 0 10 | val len = s.length 11 | while (i < len) { 12 | (s(i): @switch) match { 13 | case '"' => sb.append("\\\"") 14 | case '\\' => sb.append("\\\\") 15 | case '\b' => sb.append("\\b") 16 | case '\f' => sb.append("\\f") 17 | case '\n' => sb.append("\\n") 18 | case '\r' => sb.append("\\r") 19 | case '\t' => sb.append("\\t") 20 | case c => 21 | if (c < ' ' || (c > '~' && unicode)) sb.append("\\u%04x" format c.toInt) 22 | else sb.append(c) 23 | } 24 | i += 1 25 | } 26 | sb.append('"') 27 | 28 | sb.result() 29 | } 30 | 31 | def stripDashes(s: String) = { 32 | if (s.startsWith("--")) s.drop(2) 33 | else if (s.startsWith("-")) s.drop(1) 34 | else s 35 | } 36 | 37 | def appendMap[K, V](current: Map[K, Vector[V]], k: K, v: V): Map[K, Vector[V]] = { 38 | if (current.contains(k)) current + (k -> (current(k) :+ v)) 39 | else current + (k -> Vector(v)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/mainargs/acyclic.scala: -------------------------------------------------------------------------------- 1 | package acyclic 2 | 3 | def skipped = ??? 4 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/os/GlobInterpolator.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | package os 3 | 4 | object GlobInterpolator { 5 | class Interped(parts: Seq[String]) { 6 | def unapplySeq(s: String) = { 7 | val Seq(head, tail @ _*) = parts.map(java.util.regex.Pattern.quote) 8 | 9 | val regex = head + tail.map("(.*)" + _).mkString 10 | regex.r.unapplySeq(s) 11 | } 12 | } 13 | } 14 | 15 | /** 16 | * Lets you pattern match strings with interpolated glob-variables 17 | */ 18 | class GlobInterpolator(sc: StringContext) { 19 | def g(parts: Any*) = new StringContext(sc.parts: _*).s(parts: _*) 20 | def g = new GlobInterpolator.Interped(sc.parts) 21 | } 22 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/os/Internals.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | package os 3 | 4 | import java.io.{InputStream, OutputStream} 5 | import java.nio.file.Files 6 | 7 | object Internals { 8 | 9 | val emptyStringArray = Array.empty[String] 10 | 11 | def transfer0(src: InputStream, sink: (Array[Byte], Int) => Unit): Unit = { 12 | transfer0(src, sink, true) 13 | } 14 | def transfer0(src: InputStream, sink: (Array[Byte], Int) => Unit, close: Boolean = true): Unit = { 15 | val buffer = new Array[Byte](8192) 16 | var r = 0 17 | while (r != -1) { 18 | r = src.read(buffer) 19 | if (r != -1) sink(buffer, r) 20 | } 21 | if (close) src.close() 22 | } 23 | 24 | def transfer(src: InputStream, dest: OutputStream): Unit = transfer(src, dest, true) 25 | def transfer(src: InputStream, dest: OutputStream, close: Boolean = true): Unit = transfer0( 26 | src, 27 | dest.write(_, 0, _), 28 | close 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/os/LICENSE: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2019 Li Haoyi (haoyi.sg@gmail.com) 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/os/Macros.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | package os 3 | 4 | import os.PathChunk.{RelPathChunk, StringPathChunk, segmentsFromString, segmentsFromStringLiteralValidation} 5 | import os.RelPath.fromStringSegments 6 | 7 | import scala.quoted.{Expr, Quotes} 8 | import acyclic.skipped 9 | 10 | // StringPathChunkConversion is a fallback to non-macro String => PathChunk implicit conversion in case eta expansion is needed, this is required for ArrayPathChunk and SeqPathChunk 11 | trait PathChunkMacros extends StringPathChunkConversion { 12 | inline implicit def stringPathChunkValidated(s: String): PathChunk = 13 | ${ 14 | Macros.stringPathChunkValidatedImpl('s) 15 | } 16 | } 17 | 18 | object Macros { 19 | def stringPathChunkValidatedImpl(s: Expr[String])(using quotes: Quotes): Expr[PathChunk] = { 20 | import quotes.reflect.* 21 | 22 | s.asTerm match { 23 | case Inlined(_, _, Literal(StringConstant(literal))) => 24 | segmentsFromStringLiteralValidation(literal) 25 | '{ 26 | new RelPathChunk(fromStringSegments(segmentsFromString($s))) 27 | } 28 | case _ => 29 | '{ 30 | { 31 | new StringPathChunk($s) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/os/ResourcePath.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | package os 3 | 4 | import java.io.InputStream 5 | 6 | import scala.language.implicitConversions 7 | 8 | object ResourcePath { 9 | def resource(resRoot: ResourceRoot) = { 10 | new ResourcePath(resRoot, Array.empty[String]) 11 | } 12 | } 13 | 14 | /** 15 | * Represents path to a resource on the java classpath. 16 | * 17 | * Classloaders are tricky: http://stackoverflow.com/questions/12292926 18 | */ 19 | class ResourcePath private[os] (val resRoot: ResourceRoot, segments0: Array[String]) 20 | extends BasePathImpl with ReadablePath with SegmentedPath { 21 | def getInputStream = resRoot.getResourceAsStream(segments.mkString("/")) match { 22 | case null => throw ResourceNotFoundException(this) 23 | case stream => stream 24 | } 25 | def toSource = new Source.WritableSource(getInputStream) 26 | val segments: IndexedSeq[String] = segments0.toIndexedSeq 27 | type ThisType = ResourcePath 28 | def lastOpt = segments0.lastOption 29 | override def toString = resRoot.errorName + "/" + segments0.mkString("/") 30 | protected[this] def make(p: Seq[String], ups: Int) = { 31 | if (ups > 0) { 32 | throw PathError.AbsolutePathOutsideRoot 33 | } 34 | new ResourcePath(resRoot, p.toArray[String]) 35 | } 36 | 37 | def relativeTo(base: ResourcePath) = { 38 | var newUps = 0 39 | var s2 = base.segments 40 | 41 | while (!segments0.startsWith(s2)) { 42 | s2 = s2.dropRight(1) 43 | newUps += 1 44 | } 45 | new RelPath(segments0.drop(s2.length), newUps) 46 | } 47 | 48 | def startsWith(target: ResourcePath) = { 49 | segments0.startsWith(target.segments) 50 | } 51 | 52 | } 53 | 54 | /** 55 | * Thrown when you try to read from a resource that doesn't exist. 56 | * @param path 57 | */ 58 | case class ResourceNotFoundException(path: ResourcePath) extends Exception(path.toString) 59 | 60 | /** 61 | * Represents a possible root where classpath resources can be loaded from; 62 | * either a [[ResourceRoot.ClassLoader]] or a [[ResourceRoot.Class]]. Resources 63 | * loaded from classloaders are always loaded via their absolute path, while 64 | * resources loaded via classes are always loaded relatively. 65 | */ 66 | sealed trait ResourceRoot { 67 | def getResourceAsStream(s: String): InputStream 68 | def errorName: String 69 | } 70 | object ResourceRoot { 71 | private[this] def renderClassloader(cl: java.lang.ClassLoader) = { 72 | cl.getClass.getName + "@" + java.lang.Integer.toHexString(cl.hashCode()) 73 | } 74 | implicit def classResourceRoot(cls: java.lang.Class[_]): ResourceRoot = Class(cls) 75 | case class Class(cls: java.lang.Class[_]) extends ResourceRoot { 76 | def getResourceAsStream(s: String) = cls.getResourceAsStream(s) 77 | def errorName = renderClassloader(cls.getClassLoader) + ":" + cls.getName 78 | } 79 | implicit def classLoaderResourceRoot(cl: java.lang.ClassLoader): ResourceRoot = ClassLoader(cl) 80 | case class ClassLoader(cl: java.lang.ClassLoader) extends ResourceRoot { 81 | def getResourceAsStream(s: String) = cl.getResourceAsStream(s) 82 | def errorName = renderClassloader(cl) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/os/Source.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | package os 3 | 4 | import java.io.{ 5 | ByteArrayInputStream, 6 | InputStream, 7 | OutputStream, 8 | SequenceInputStream, 9 | BufferedOutputStream 10 | } 11 | import java.nio.channels.{ 12 | Channels, 13 | FileChannel, 14 | ReadableByteChannel, 15 | SeekableByteChannel, 16 | WritableByteChannel 17 | } 18 | 19 | import scala.language.implicitConversions 20 | 21 | /** 22 | * A source of bytes; must provide either an [[InputStream]] or a 23 | * [[SeekableByteChannel]] to read from. Can be constructed implicitly from 24 | * strings, byte arrays, inputstreams, channels or file paths 25 | */ 26 | trait Source extends geny.Writable { 27 | override def httpContentType = Some("application/octet-stream") 28 | def getHandle(): Either[geny.Writable, SeekableByteChannel] 29 | def writeBytesTo(out: java.io.OutputStream) = getHandle() match { 30 | case Left(bs) => bs.writeBytesTo(out) 31 | 32 | case Right(channel: FileChannel) => 33 | val outChannel = Channels.newChannel(out) 34 | channel.transferTo(0, Long.MaxValue, outChannel) 35 | 36 | case Right(channel) => 37 | val inChannel = Channels.newInputStream(channel) 38 | Internals.transfer(inChannel, out) 39 | } 40 | def writeBytesTo(out: WritableByteChannel) = getHandle() match { 41 | case Left(bs) => 42 | val os = new BufferedOutputStream(Channels.newOutputStream(out)) 43 | bs.writeBytesTo(os) 44 | os.flush() 45 | case Right(channel) => 46 | (channel, out) match { 47 | case (src: FileChannel, dest) => 48 | val size = src.size() 49 | var pos = 0L 50 | while (pos < size) { 51 | pos += src.transferTo(pos, size - pos, dest) 52 | } 53 | case (src, dest: FileChannel) => dest.transferFrom(src, 0, Long.MaxValue) 54 | case (src, dest) => 55 | val os = new BufferedOutputStream(Channels.newOutputStream(dest)) 56 | Internals.transfer(Channels.newInputStream(src), os) 57 | os.flush() 58 | } 59 | 60 | } 61 | } 62 | 63 | object Source extends WritableLowPri { 64 | implicit class ChannelSource(cn: SeekableByteChannel) extends Source { 65 | def getHandle() = Right(cn) 66 | } 67 | 68 | implicit class WritableSource[T](s: T)(implicit f: T => geny.Writable) extends Source { 69 | val writable = f(s) 70 | 71 | override def contentLength: Option[Long] = writable.contentLength 72 | def getHandle() = Left(writable) 73 | } 74 | } 75 | 76 | trait WritableLowPri { 77 | implicit def WritableGenerator[T](a: geny.Generator[T])(implicit 78 | f: T => geny.Writable 79 | ): Source = { 80 | val f0 = f 81 | new Source { 82 | def getHandle() = Left( 83 | new geny.Writable { 84 | def writeBytesTo(out: java.io.OutputStream) = { 85 | for (x <- a) f0(x).writeBytesTo(out) 86 | } 87 | } 88 | ) 89 | } 90 | } 91 | implicit def WritableTraversable[M[_], T](a: M[T])(implicit 92 | f: T => geny.Writable, 93 | g: M[T] => TraversableOnce[T] 94 | ): Source = { 95 | val traversable = g(a) 96 | val f0 = f 97 | new Source { 98 | def getHandle() = Left( 99 | new geny.Writable { 100 | def writeBytesTo(out: java.io.OutputStream) = { 101 | for (x <- traversable) f0(x).writeBytesTo(out) 102 | } 103 | } 104 | ) 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * A source which is guaranteeds to provide a [[SeekableByteChannel]] 111 | */ 112 | trait SeekableSource extends Source { 113 | def getHandle(): Right[geny.Writable, SeekableByteChannel] 114 | def getChannel() = getHandle().right.get 115 | } 116 | 117 | object SeekableSource { 118 | implicit class ChannelSource(cn: SeekableByteChannel) extends SeekableSource { 119 | def getHandle() = Right(cn) 120 | } 121 | class ChannelLengthSource(cn: SeekableByteChannel, length: Long) extends SeekableSource { 122 | def getHandle() = Right(cn) 123 | 124 | override def contentLength: Option[Long] = Some(length) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/os/package.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | import scala.language.implicitConversions 3 | import java.nio.file.FileSystem 4 | import java.nio.file.FileSystems 5 | import java.nio.file.Paths 6 | import scala.util.DynamicVariable 7 | 8 | package object os { 9 | type Generator[+T] = geny.Generator[T] 10 | val Generator = geny.Generator 11 | implicit def GlobSyntax(s: StringContext): GlobInterpolator = new GlobInterpolator(s) 12 | 13 | /** 14 | * The root of the filesystem 15 | */ 16 | val root: Path = Path(java.nio.file.Paths.get(".").toAbsolutePath.getRoot) 17 | 18 | def root(root: String, fileSystem: FileSystem = FileSystems.getDefault()): Path = { 19 | val path = Path(fileSystem.getPath(root)) 20 | assert(path.root == root || path.root == root.replace('/', '\\'), s"$root is not a root path") 21 | path 22 | } 23 | 24 | def resource(implicit resRoot: ResourceRoot = Thread.currentThread().getContextClassLoader) = { 25 | os.ResourcePath.resource(resRoot) 26 | } 27 | 28 | // See https://github.com/com-lihaoyi/os-lib/pull/239 29 | // and https://github.com/lightbend/mima/issues/794 30 | // why the need the inner object to preserve binary compatibility 31 | private object _home { 32 | lazy val value = Path(System.getProperty("user.home")) 33 | } 34 | 35 | /** 36 | * The user's home directory 37 | */ 38 | def home: Path = _home.value 39 | 40 | /** 41 | * The current working directory for this process. 42 | */ 43 | def pwd: Path = dynamicPwdFunction.value() 44 | 45 | private val pwd0 = os.Path(java.nio.file.Paths.get(".").toAbsolutePath) 46 | 47 | /** 48 | * Used to override `pwd` within a certain scope with a generated value 49 | */ 50 | val dynamicPwdFunction: DynamicVariable[() => Path] = new DynamicVariable(() => dynamicPwd.value) 51 | 52 | /** 53 | * Used to override `pwd` within a certain scope with a fixed value 54 | */ 55 | val dynamicPwd: DynamicVariable[Path] = new DynamicVariable(pwd0) 56 | 57 | val up: RelPath = RelPath.up 58 | 59 | val rel: RelPath = RelPath.rel 60 | 61 | val sub: SubPath = SubPath.sub 62 | 63 | /** 64 | * Extractor to let you easily pattern match on [[os.Path]]s. Lets you do 65 | * 66 | * {{{ 67 | * @ val base/segment/filename = pwd 68 | * base: Path = Path(Vector("Users", "haoyi", "Dropbox (Personal)")) 69 | * segment: String = "Workspace" 70 | * filename: String = "Ammonite" 71 | * }}} 72 | * 73 | * To break apart a path and extract various pieces of it. 74 | */ 75 | object / { 76 | def unapply(p: Path): Option[(Path, String)] = { 77 | if (p.segmentCount != 0) Some((p / up, p.last)) 78 | else None 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Li Haoyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/ProductSupport.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.pprint 2 | import replpp.shaded.fansi 3 | 4 | object ProductSupport { 5 | 6 | def treeifyProductElements(x: Product, 7 | walker: Walker, 8 | escapeUnicode: Boolean, 9 | showFieldNames: Boolean): Iterator[Tree] = { 10 | if (!showFieldNames) x.productIterator.map(x => walker.treeify(x, escapeUnicode, showFieldNames)) 11 | else x.productElementNames 12 | .zipWithIndex 13 | .map { 14 | case (name, i) => 15 | val elem = x.productElement(i) 16 | Tree.KeyValue(name, walker.treeify(elem, escapeUnicode, showFieldNames)) 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/Renderer.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.pprint 2 | import replpp.shaded.fansi 3 | object Renderer{ 4 | /** 5 | * Basically like mkString, but for nested iterators. Used whenever 6 | * you want to put stuff "in between" the elements of the larger 7 | * iterator 8 | */ 9 | def joinIter[T](it0: Iterator[Iterator[T]], joiner: => Iterator[T]) = { 10 | new Util.ConcatIterator(it0, () => joiner) 11 | } 12 | 13 | val openParen = fansi.Str("(") 14 | val closeParen = fansi.Str(")") 15 | val commaSpace = fansi.Str(", ") 16 | val newLine = fansi.Str("\n") 17 | val commaNewLine = fansi.Str(",\n") 18 | private[this] val cachedIndents = Array.tabulate(64)(n => fansi.Str(" " * n)) 19 | def indent(n: Int) = if (n < 64) cachedIndents(n) else fansi.Str(" " * n) 20 | } 21 | class Renderer(maxWidth: Int, 22 | colorApplyPrefix: fansi.Attrs, 23 | colorLiteral: fansi.Attrs, 24 | indentStep: Int){ 25 | 26 | 27 | 28 | def rec(x: Tree, leftOffset: Int, indentCount: Int): Result = x match{ 29 | case Tree.Apply(prefix, body) => 30 | val nonEmpty = body.hasNext 31 | // Render children and buffer them until you fill up a single line, 32 | // or you run out of children. 33 | // 34 | // Even before rendering any children, the indentation, prefix 35 | // and the two open/close parens already take up a few characters 36 | var totalHorizontalWidth = leftOffset + prefix.length + 2 37 | val buffer = collection.mutable.Buffer.empty[collection.Seq[fansi.Str]] 38 | var lastChildIter = Iterator[fansi.Str]() 39 | var childCompletedLineCount = 0 40 | while(body.hasNext && totalHorizontalWidth <= maxWidth && childCompletedLineCount == 0){ 41 | 42 | val child = body.next() 43 | val childRes = rec(child, (indentCount + 1) * indentStep, indentCount + 1) 44 | 45 | val childBuffer = collection.mutable.Buffer.empty[fansi.Str] 46 | while(childRes.iter.hasNext && totalHorizontalWidth < maxWidth){ 47 | val next = childRes.iter.next() 48 | childBuffer += next 49 | totalHorizontalWidth += next.length 50 | } 51 | 52 | 53 | if (body.hasNext) { 54 | totalHorizontalWidth += 2 55 | } 56 | 57 | if (!childRes.iter.hasNext){ 58 | childCompletedLineCount = childCompletedLineCount + childRes.completedLineCount 59 | }else{ 60 | lastChildIter = childRes.iter 61 | 62 | } 63 | 64 | buffer += childBuffer.toSeq 65 | } 66 | 67 | def applyHeader = Iterator(colorApplyPrefix(prefix), Renderer.openParen) 68 | 69 | val indentPlusOne = Renderer.indent((indentCount + 1) * indentStep) 70 | 71 | def separator = Iterator(Renderer.commaNewLine, indentPlusOne) 72 | 73 | if ( 74 | totalHorizontalWidth <= maxWidth && 75 | childCompletedLineCount == 0 && 76 | !lastChildIter.hasNext 77 | ) { 78 | val iter = Util.concat( 79 | () => applyHeader, 80 | () => Renderer.joinIter( 81 | buffer.iterator.map(_.iterator), 82 | Iterator(Renderer.commaSpace) 83 | ), 84 | () => Iterator(Renderer.closeParen) 85 | ) 86 | 87 | val length: Int = buffer.iterator.map(_.iterator.map(_.length).sum).sum 88 | new Result(iter, 0, length) 89 | }else if (!nonEmpty && totalHorizontalWidth > maxWidth) { 90 | val iter = Util.concat( 91 | () => applyHeader, 92 | () => Iterator( 93 | Renderer.newLine, 94 | Renderer.indent(indentCount * indentStep), 95 | Renderer.closeParen 96 | ) 97 | ) 98 | 99 | val length: Int = buffer.iterator.map(_.iterator.map(_.length).sum).sum 100 | new Result(iter, 0, length) 101 | } else { 102 | def bufferedFragments = Renderer.joinIter( 103 | for((v, i) <- buffer.iterator.zipWithIndex) yield{ 104 | if (i < buffer.length-1) v.iterator 105 | else v.iterator ++ lastChildIter 106 | }, 107 | separator 108 | ) 109 | 110 | def nonBufferedFragments = Renderer.joinIter( 111 | body.map(c => rec(c, (indentCount + 1) * indentStep, indentCount + 1).iter), 112 | separator 113 | ) 114 | 115 | def allFragments = 116 | if (buffer.isEmpty) nonBufferedFragments 117 | else if (!body.hasNext) bufferedFragments 118 | else Renderer.joinIter(Iterator(bufferedFragments, nonBufferedFragments), separator) 119 | 120 | def iter = Util.concat( 121 | () => applyHeader, 122 | () => Iterator(Renderer.newLine, indentPlusOne), 123 | () => allFragments, 124 | () => Iterator( 125 | Renderer.newLine, 126 | Renderer.indent(indentCount * indentStep), 127 | Renderer.closeParen 128 | ) 129 | ) 130 | 131 | 132 | new Result(iter, childCompletedLineCount + 2, indentCount * indentStep + 1) 133 | } 134 | 135 | case Tree.Infix(lhs, op, rhs) => 136 | rec(lhs, leftOffset, indentCount).flatMap{ (lhsNewline, lhsLastLineLength) => 137 | Result.fromString(" " + op + " ").flatMap((_, _) => 138 | rec(rhs, lhsLastLineLength, indentCount) 139 | ) 140 | } 141 | 142 | case t: Tree.Lazy => 143 | 144 | lazy val str = t.body0(Tree.Ctx( 145 | maxWidth, leftOffset, indentCount, 146 | indentStep, colorLiteral, colorApplyPrefix 147 | )) 148 | new Truncated(str.map(fansi.Str(_)), maxWidth, height = 99999999).toResult 149 | 150 | case t: Tree.Literal => Result.fromString(colorLiteral(t.body)) 151 | 152 | case Tree.KeyValue(k, v) => 153 | val prefix = s"$k = " 154 | Result.fromString(prefix) 155 | .flatMap((_, _) => rec(v, leftOffset + prefix.length, indentCount)) 156 | 157 | } 158 | } 159 | 160 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/Result.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.pprint 2 | import replpp.shaded.fansi 3 | 4 | import scala.annotation.tailrec 5 | 6 | 7 | /** 8 | * The intermediate return type of the pretty-print system: provides an 9 | * iterator which produces the actual string output, as well as metadata 10 | * around that output that is only available after the iterator is exhausted 11 | */ 12 | class Result(val iter: Iterator[fansi.Str], 13 | completedLineCount0: => Int, 14 | lastLineLength0: => Int){ 15 | lazy val completedLineCount = { 16 | require(iter.isEmpty) 17 | completedLineCount0 18 | } 19 | lazy val lastLineLength = { 20 | require(iter.isEmpty) 21 | lastLineLength0 22 | } 23 | def flatMap(f: (Int, Int) => Result): Result = { 24 | var newCompletedLineCount = 0 25 | var newLastLineLength = 0 26 | 27 | val mergedIterator = Util.concat( 28 | () => iter, 29 | () => { 30 | require(!iter.hasNext) 31 | val newResult = f(completedLineCount, lastLineLength0) 32 | newResult.iter.map{ x => 33 | if (!newResult.iter.hasNext){ 34 | newCompletedLineCount = newResult.completedLineCount 35 | newLastLineLength = newResult.lastLineLength 36 | } 37 | x 38 | } 39 | } 40 | ) 41 | new Result( 42 | mergedIterator, 43 | newCompletedLineCount + completedLineCount, 44 | if (newCompletedLineCount > 0) newLastLineLength 45 | else newLastLineLength + lastLineLength 46 | ) 47 | 48 | } 49 | } 50 | 51 | object Result{ 52 | def fromString(s: => fansi.Str) = { 53 | lazy val lines = s.plainText.linesIterator.toArray 54 | new Result(Iterator(s), lines.length - 1, lines.last.length) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/StringPrefix.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.pprint 2 | import replpp.shaded.fansi 3 | 4 | object StringPrefix{ 5 | def apply(i: Iterable[_]) = 6 | scala.collection.internal.pprint.CollectionName.get(i) 7 | } 8 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/TPrint.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.pprint 2 | import replpp.shaded.fansi 3 | 4 | /** 5 | * Summoning an implicit `TPrint[T]` provides a pretty-printed 6 | * string representation of the type `T`, much better than is 7 | * provided by the default `Type#toString`. In particular 8 | * 9 | * - More forms are properly supported and printed 10 | * - Prefixed Types are printed un-qualified, according to 11 | * what's currently in scope 12 | */ 13 | trait TPrint[T] { 14 | def render(implicit tpc: TPrintColors): fansi.Str 15 | 16 | } 17 | 18 | object TPrint extends TPrintLowPri{ 19 | def recolor[T](s: fansi.Str): TPrint[T] = { 20 | new TPrint[T]{ 21 | def render(implicit tpc: TPrintColors) = { 22 | val colors = s.getColors 23 | val updatedColors = colors.map{ 24 | c => if (c == fansi.Color.Green.applyMask) tpc.typeColor.applyMask else 0L 25 | } 26 | fansi.Str.fromArrays(s.getChars, updatedColors) 27 | } 28 | } 29 | } 30 | def implicitly[T](implicit t: TPrint[T]): TPrint[T] = t 31 | implicit val NothingTPrint: TPrint[Nothing] = 32 | recolor[Nothing](fansi.Color.Green("Nothing")) 33 | 34 | } 35 | 36 | case class TPrintColors(typeColor: fansi.Attrs) 37 | 38 | object TPrintColors { 39 | implicit object BlackWhite extends TPrintColors(fansi.Attrs()) 40 | object Colors extends TPrintColors(fansi.Color.Green){ 41 | implicit val Colored: TPrintColors = this 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/TPrintImpl.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.pprint 2 | import replpp.shaded.fansi 3 | 4 | trait TPrintLowPri{ 5 | inline given default[T]: TPrint[T] = ${ TPrintLowPri.typePrintImpl[T] } 6 | } 7 | 8 | object TPrintLowPri{ 9 | 10 | import scala.quoted._ 11 | 12 | sealed trait WrapType 13 | object WrapType{ 14 | case object NoWrap extends WrapType 15 | case object Infix extends WrapType 16 | case object Tuple extends WrapType 17 | } 18 | 19 | val functionTypes = Range.inclusive(0, 22).map(i => s"scala.Function$i").toSet 20 | val tupleTypes = Range.inclusive(0, 22).map(i => s"scala.Tuple$i").toSet 21 | 22 | def typePrintImpl[T](using Quotes, Type[T]): Expr[TPrint[T]] = { 23 | 24 | import quotes.reflect._ 25 | import util._ 26 | 27 | def literalColor(s: fansi.Str): fansi.Str = { 28 | fansi.Color.Green(s) 29 | } 30 | 31 | def printSymString(s: String) = 32 | if (s.toString.startsWith("_$")) "_" 33 | else s.toString.stripSuffix(".type") 34 | 35 | def printBounds(lo: TypeRepr, hi: TypeRepr): fansi.Str = { 36 | val loTree = 37 | if (lo =:= TypeRepr.of[Nothing]) None else Some(fansi.Str(" >: ") ++ rec(lo) ) 38 | val hiTree = 39 | if (hi =:= TypeRepr.of[Any]) None else Some(fansi.Str(" <: ") ++ rec(hi) ) 40 | val underscore = fansi.Str("_") 41 | loTree.orElse(hiTree).map(underscore ++ _).getOrElse(underscore) 42 | } 43 | 44 | def printSym(s: String): fansi.Str = literalColor(fansi.Str(s)) 45 | 46 | //TODO: We don't currently use this method 47 | def prefixFor(pre: TypeTree, sym: String): fansi.Str = { 48 | // Depending on what the prefix is, you may use `#`, `.` 49 | // or even need to wrap the prefix in parentheses 50 | val sep = pre match { 51 | case x if x.toString.endsWith(".type") => 52 | rec(pre.tpe) ++ "." 53 | } 54 | sep ++ printSym(sym) 55 | } 56 | 57 | 58 | def printArgs(args: List[TypeRepr]): fansi.Str = { 59 | fansi.Str("[") ++ printArgs0(args) ++ "]" 60 | } 61 | 62 | def printArgs0(args: List[TypeRepr]): fansi.Str = { 63 | val added = fansi.Str.join( 64 | args.map { 65 | case TypeBounds(lo, hi) => 66 | printBounds(lo, hi) 67 | case tpe: TypeRepr => 68 | rec(tpe, false) 69 | }, 70 | sep = ", " 71 | ) 72 | added 73 | } 74 | 75 | 76 | object RefinedType { 77 | def unapply(tpe: TypeRepr): Option[(TypeRepr, List[(String, TypeRepr)])] = tpe match { 78 | case Refinement(p, i, b) => 79 | unapply(p).map { 80 | case (pp, bs) => (pp, (i -> b) :: bs) 81 | }.orElse(Some((p, (i -> b) :: Nil))) 82 | case _ => None 83 | } 84 | } 85 | 86 | def rec(tpe: TypeRepr, end: Boolean = false): fansi.Str = rec0(tpe)._1 87 | def rec0(tpe: TypeRepr, end: Boolean = false): (fansi.Str, WrapType) = tpe match { 88 | case TypeRef(NoPrefix(), sym) => 89 | (printSym(sym), WrapType.NoWrap) 90 | // TODO: Add prefix handling back in once it works! 91 | case TypeRef(_, sym) => 92 | (printSym(sym), WrapType.NoWrap) 93 | case AppliedType(tpe, args) => 94 | if (functionTypes.contains(tpe.typeSymbol.fullName)) { 95 | ( 96 | if(args.size == 1 ) fansi.Str("() => ") ++ rec(args.last) 97 | else{ 98 | val (left, wrap) = rec0(args(0)) 99 | if(args.size == 2 && wrap == WrapType.NoWrap){ 100 | left ++ fansi.Str(" => ") ++ rec(args.last) 101 | } 102 | else fansi.Str("(") ++ printArgs0(args.dropRight(1)) ++ fansi.Str(") => ") ++ rec(args.last) 103 | 104 | }, 105 | WrapType.Infix 106 | ) 107 | 108 | } else if (tupleTypes.contains(tpe.typeSymbol.fullName)) 109 | (fansi.Str("(") ++ printArgs0(args) ++ fansi.Str(")"), WrapType.Tuple) 110 | else (printSym(tpe.typeSymbol.name) ++ printArgs(args), WrapType.NoWrap) 111 | case RefinedType(tpe, refinements) => 112 | val pre = rec(tpe) 113 | lazy val defs = fansi.Str.join( 114 | refinements.collect { 115 | case (name, tpe: TypeRepr) => 116 | fansi.Str("type " + name + " = ") ++ rec(tpe) 117 | case (name, TypeBounds(lo, hi)) => 118 | fansi.Str("type " + name) ++ printBounds(lo, hi) ++ rec(tpe) 119 | }, 120 | sep = "; " 121 | ) 122 | (pre ++ (if(refinements.isEmpty) fansi.Str("") else fansi.Str("{") ++ defs ++ "}"), WrapType.NoWrap) 123 | case AnnotatedType(parent, annot) => 124 | (rec(parent, end), WrapType.NoWrap) 125 | case _ => 126 | (fansi.Str(Type.show[T]), WrapType.NoWrap) 127 | } 128 | val value: fansi.Str = rec(TypeRepr.of[T]) 129 | 130 | '{TPrint.recolor(fansi.Str(${Expr(value.render)}))} 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/Truncated.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.pprint 2 | import replpp.shaded.fansi 3 | 4 | import scala.collection.mutable 5 | 6 | 7 | /** 8 | * Wraps an input iterator of colored [[fansi.Str]]s, and produces the same 9 | * [[fansi.Str]]s but truncated once the wrapped-at-[[width]] text reaches 10 | * beyond a certain [[height]] 11 | */ 12 | class Truncated(chunks0: Iterator[fansi.Str], 13 | width: Int, 14 | height: Int, 15 | truncationMarker: String = "...") 16 | extends Iterator[fansi.Str]{ 17 | val lineLengths = collection.mutable.Buffer(0) 18 | 19 | private[this] object Internal { 20 | 21 | val chunks = chunks0.filter(_.length > 0) 22 | 23 | var previousSlashN = false 24 | var previousSlashR = false 25 | 26 | def handleNormalChar(char: Char) = { 27 | previousSlashN = false 28 | previousSlashR = false 29 | if (char == '\n' && previousSlashR || char == '\r' && previousSlashN) { 30 | // do nothing 31 | } else if (char == '\n') { 32 | previousSlashN = true 33 | lineLengths.append(0) 34 | } else if (char == '\r') { 35 | previousSlashR = true 36 | lineLengths.append(0) 37 | } 38 | else if (lineLengths.last == width) lineLengths.append(1) 39 | else lineLengths(lineLengths.length - 1) += 1 40 | 41 | } 42 | 43 | def completedLines = lineLengths.length - 1 44 | 45 | var finishedChunk = false 46 | 47 | var lastLineFinished = false 48 | var lastChunkLeftover = fansi.Str("") 49 | 50 | def consumeChunkUntilLine(chunk: fansi.Str, lineLimit: Int) ={ 51 | var i = 0 52 | val chars = chunk.getChars 53 | while (i < chars.length && completedLines < lineLimit) { 54 | val char = chars(i) 55 | handleNormalChar(char) 56 | i += 1 57 | } 58 | if (i == chunk.length) None else Some(i) 59 | } 60 | 61 | var isTruncated0 = false 62 | } 63 | 64 | import Internal._ 65 | 66 | def completedLineCount = { 67 | assert(!hasNext) 68 | lineLengths.length - 1 69 | } 70 | def lastLineLength = { 71 | assert(!hasNext) 72 | lineLengths(lineLengths.length-1) 73 | } 74 | def isTruncated = { 75 | assert(!hasNext) 76 | isTruncated0 77 | } 78 | 79 | def toResult = new Result(this, completedLineCount, lastLineLength) 80 | 81 | def hasNext = (chunks.hasNext && completedLines < height - 1) || !lastLineFinished 82 | 83 | 84 | /** 85 | * [[Truncated]] streams the chunks one by one until it reaches the height 86 | * limit; then, it buffers up to one entire row worth of chunks to check 87 | * whether it overshoots. If it overshoots, it discards the chunks and prints 88 | * "..." instead. If not, the buffered chunks get printed all at once. 89 | */ 90 | def next() = if (chunks.hasNext && completedLines < height - 1) { 91 | val chunk = chunks.next() 92 | consumeChunkUntilLine(chunk, height - 1) match{ 93 | case None => 94 | if (!chunks.hasNext) lastLineFinished = true 95 | chunk 96 | case Some(i) => 97 | // chunk was partially consumed. This should only happen if the chunk 98 | // is overshooting the vertical limit 99 | 100 | // If the last line is not empty, it means there is a character 101 | // on that last line. In such a case 102 | val splitPoint = if (lineLengths.last != 0) i - 1 else i 103 | lastChunkLeftover = chunk.substring(splitPoint, chunk.length) 104 | chunk.substring(0, splitPoint) 105 | } 106 | 107 | }else if (!lastLineFinished) { 108 | val buffer = mutable.Buffer.empty[fansi.Str] 109 | var charsLeftOver = false 110 | consumeChunkUntilLine(lastChunkLeftover, height) match{ 111 | case None => buffer.append(lastChunkLeftover) 112 | case Some(i) => 113 | charsLeftOver = true 114 | buffer.append(lastChunkLeftover.substring(0, i - 1)) 115 | } 116 | while(chunks.hasNext && completedLines < height){ 117 | val chunk = chunks.next() 118 | 119 | consumeChunkUntilLine(chunk, height) match{ 120 | case None => buffer.append(chunk) 121 | case Some(i) => 122 | charsLeftOver = true 123 | buffer.append(chunk.substring(0, i)) 124 | } 125 | 126 | } 127 | 128 | lastLineFinished = true 129 | 130 | if (charsLeftOver || chunks.hasNext) { 131 | isTruncated0 = true 132 | fansi.Str(truncationMarker) 133 | } 134 | else buffer.map(_.render).mkString 135 | }else{ 136 | throw new java.util.NoSuchElementException("next on empty iterator") 137 | } 138 | } 139 | 140 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/Util.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.pprint 2 | import replpp.shaded.fansi 3 | 4 | import scala.annotation.{switch, tailrec} 5 | 6 | object Util{ 7 | 8 | 9 | def concat[T](is: (() => Iterator[T])*) = new ConcatIterator(is.iterator.map(_()), () => Iterator.empty) 10 | /** 11 | * Basically a fast, efficient `.flatten` or `mkString` for nested iterators 12 | * 13 | * For some reason, the default way of concatenation e.g. 14 | * 15 | * val middle = first ++ lastChildIter ++ sep ++ remaining 16 | * 17 | * Was throwing weird NullPointerExceptions I couldn't figure out =( 18 | * 19 | * Also, ++ didn't seem to be sufficiently lazy, so it was forcing 20 | * things earlier than it really needed to. It isn't documented anywhere 21 | * how lazy it's meant to be, whereas `ConcatIterator` here is obviously 22 | * lazy and won't even evaluate each iterator until you ask it to 23 | */ 24 | class ConcatIterator[T](it0: Iterator[Iterator[T]], joiner: () => Iterator[T]) extends Iterator[T]{ 25 | var head: Iterator[T] = null 26 | var count = 0 27 | 28 | @tailrec private[this] def check(): Boolean = { 29 | 30 | if (head != null && head.hasNext) true 31 | else if (!it0.hasNext) false 32 | else { 33 | if (count % 2 == 0) head = it0.next() 34 | else head = joiner() 35 | count += 1 36 | 37 | check() 38 | } 39 | } 40 | 41 | def hasNext = check() 42 | 43 | 44 | def next() = { 45 | check() 46 | head.next() 47 | } 48 | } 49 | 50 | def isOperator(ident: String) = { 51 | (ident.size > 0) && (ident(0) match{ 52 | case '<' | '~' | '!' | '@' | '#' | '%' | 53 | '^' | '*' | '+' | '-' | /*'<' | */ 54 | '>' | '?' | ':' | '=' | '&' | 55 | '|' | '/' | '\\' => true 56 | case _ => false 57 | }) 58 | } 59 | def escapeChar(c: Char, 60 | sb: StringBuilder, 61 | unicode: Boolean = true) = (c: @switch) match { 62 | case '"' => sb.append("\\\"") 63 | case '\\' => sb.append("\\\\") 64 | case '\b' => sb.append("\\b") 65 | case '\f' => sb.append("\\f") 66 | case '\n' => sb.append("\\n") 67 | case '\r' => sb.append("\\r") 68 | case '\t' => sb.append("\\t") 69 | case c => 70 | if (c < ' ' || (c > '~' && unicode)) sb.append("\\u%04x" format c.toInt) 71 | else sb.append(c) 72 | } 73 | 74 | 75 | /** 76 | * Convert a string to a C&P-able literal. Basically 77 | * copied verbatim from the uPickle source code. 78 | */ 79 | def literalize(s: IndexedSeq[Char], unicode: Boolean = true) = { 80 | val sb = new StringBuilder 81 | sb.append('"') 82 | var i = 0 83 | val len = s.length 84 | while (i < len) { 85 | Util.escapeChar(s(i), sb, unicode) 86 | i += 1 87 | } 88 | sb.append('"') 89 | 90 | sb.result() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/Walker.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.pprint 2 | import replpp.shaded.fansi 3 | 4 | /** 5 | * A lazy AST representing pretty-printable text. Models `foo(a, b)` 6 | * `foo op bar`, and terminals `foo` in both lazy and eager forms 7 | */ 8 | sealed trait Tree 9 | object Tree{ 10 | 11 | /** 12 | * Foo(aa, bbb, cccc) 13 | */ 14 | case class Apply(prefix: String, 15 | body: Iterator[Tree]) extends Tree 16 | 17 | /** 18 | * LHS op RHS 19 | */ 20 | case class Infix(lhs: Tree, op: String, rhs: Tree) extends Tree 21 | 22 | /** 23 | * "xyz" 24 | */ 25 | case class Literal(body: String) extends Tree{ 26 | val hasNewLine = body.exists(c => c == '\n' || c == '\r') 27 | } 28 | 29 | /** 30 | * x = y 31 | */ 32 | case class KeyValue(key: String, value: Tree) extends Tree 33 | 34 | /** 35 | * xyz 36 | */ 37 | case class Lazy(body0: Ctx => Iterator[String]) extends Tree 38 | 39 | case class Ctx(width: Int, 40 | leftOffset: Int, 41 | indentCount: Int, 42 | indentStep: Int, 43 | literalColor: fansi.Attrs, 44 | applyPrefixColor: fansi.Attrs) 45 | } 46 | 47 | abstract class Walker{ 48 | val tuplePrefix = "scala.Tuple" 49 | 50 | def additionalHandlers: PartialFunction[Any, Tree] 51 | def treeify(x: Any, escapeUnicode: Boolean, showFieldNames: Boolean): Tree = additionalHandlers.lift(x).getOrElse{ 52 | x match{ 53 | 54 | case null => Tree.Literal("null") 55 | case x: Boolean => Tree.Literal(x.toString) 56 | case x: Char => 57 | val sb = new StringBuilder 58 | sb.append('\'') 59 | Util.escapeChar(x, sb, escapeUnicode) 60 | sb.append('\'') 61 | Tree.Literal(sb.toString) 62 | 63 | case x: Byte => Tree.Literal(x.toString) 64 | case x: Short => Tree.Literal(x.toString) 65 | case x: Int => Tree.Literal(x.toString) 66 | case x: Long => Tree.Literal(x.toString + "L") 67 | case x: Float => Tree.Literal(x.toString + "F") 68 | case x: Double => Tree.Literal(x.toString) 69 | case x: String => 70 | // MP: adapted here: added ` || c == '\\'` 71 | // MP: upstream PR: https://github.com/com-lihaoyi/PPrint/pull/110 72 | if (x.exists(c => c == '\n' || c == '\r' || c == '\\')) Tree.Literal("\"\"\"" + x + "\"\"\"") 73 | else Tree.Literal(Util.literalize(x, escapeUnicode)) 74 | 75 | case x: Symbol => Tree.Literal("'" + x.name) 76 | 77 | case x: scala.collection.Map[_, _] => 78 | Tree.Apply( 79 | StringPrefix(x), 80 | x.iterator.flatMap { case (k, v) => 81 | Seq(Tree.Infix(treeify(k, escapeUnicode, showFieldNames), "->", treeify(v, escapeUnicode, showFieldNames))) 82 | } 83 | ) 84 | 85 | case x: Iterable[_] => Tree.Apply(StringPrefix(x), x.iterator.map(x => treeify(x, escapeUnicode, showFieldNames))) 86 | 87 | case None => Tree.Literal("None") 88 | 89 | case it: Iterator[_] => 90 | // required since 2.13 91 | if (it.isEmpty) 92 | Tree.Literal("empty iterator") 93 | else 94 | Tree.Literal("non-empty iterator") 95 | 96 | case x: Array[_] => Tree.Apply("Array", x.iterator.map(x => treeify(x, escapeUnicode, showFieldNames))) 97 | 98 | case x: Product => 99 | val className = x.getClass.getName 100 | if (x.productArity == 0) Tree.Lazy(ctx => Iterator(x.toString)) 101 | else if(x.productArity == 2 && Util.isOperator(x.productPrefix)){ 102 | Tree.Infix( 103 | treeify(x.productElement(0), escapeUnicode, showFieldNames), 104 | 105 | x.productPrefix, 106 | treeify(x.productElement(1), escapeUnicode, showFieldNames) 107 | ) 108 | } else (className.startsWith(tuplePrefix), className.lift(tuplePrefix.length)) match{ 109 | // leave out tuple1, so it gets printed as Tuple1(foo) instead of (foo) 110 | // Don't check the whole suffix, because of specialization there may be 111 | // funny characters after the digit 112 | case (true, Some('2' | '3' | '4' | '5' | '6' | '7' | '8' | '9')) => 113 | Tree.Apply("", x.productIterator.map(x => treeify(x, escapeUnicode, showFieldNames))) 114 | 115 | case _ => 116 | Tree.Apply(x.productPrefix, ProductSupport.treeifyProductElements(x, this, escapeUnicode, showFieldNames)) 117 | } 118 | 119 | case x => Tree.Lazy(ctx => 120 | Iterator( 121 | x.toString match{ 122 | case null => "null" 123 | case s =>s 124 | } 125 | ) 126 | ) 127 | } 128 | } 129 | 130 | 131 | } 132 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/package.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded 2 | import replpp.shaded.fansi 3 | /** 4 | * Contains a convenient default pre-configured PPrinter. 5 | * 6 | * Hard-coded and inflexible, but feel free to instantiate your own 7 | * PPrint if you want to customize it. 8 | */ 9 | package object pprint extends PPrinter{ 10 | def tprint[T: TPrint](implicit config: TPrintColors) = { 11 | implicitly[TPrint[T]].render 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/pprint/scala/collection/internal/pprint/CollectionName.scala: -------------------------------------------------------------------------------- 1 | package scala.collection.internal.pprint 2 | 3 | // needs to be in a scala.* package to call Iterable.collectionClassName (which is private[scala]) 4 | object CollectionName { 5 | def get(iterable: scala.collection.Iterable[_]): String = 6 | iterable.collectionClassName 7 | } 8 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/scopt/LICENSE.md: -------------------------------------------------------------------------------- 1 | This project is licensed under the [MIT license](https://en.wikipedia.org/wiki/MIT_License). 2 | 3 | Copyright (c) scopt contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/scopt/OEffect.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.scopt 2 | 3 | sealed trait OEffect 4 | object OEffect { 5 | case class DisplayToOut(msg: String) extends OEffect 6 | case class DisplayToErr(msg: String) extends OEffect 7 | case class ReportError(msg: String) extends OEffect 8 | case class ReportWarning(msg: String) extends OEffect 9 | case class Terminate(exitState: Either[String, Unit]) extends OEffect 10 | } 11 | 12 | trait OEffectSetup { 13 | def displayToOut(msg: String): Unit 14 | def displayToErr(msg: String): Unit 15 | def reportError(msg: String): Unit 16 | def reportWarning(msg: String): Unit 17 | def terminate(exitState: Either[String, Unit]): Unit 18 | } 19 | 20 | abstract class DefaultOEffectSetup extends OEffectSetup { 21 | override def displayToOut(msg: String): Unit = { 22 | Console.out.println(msg) 23 | } 24 | override def displayToErr(msg: String): Unit = { 25 | Console.err.println(msg) 26 | } 27 | override def reportError(msg: String): Unit = { 28 | displayToErr("Error: " + msg) 29 | } 30 | override def reportWarning(msg: String): Unit = { 31 | displayToErr("Warning: " + msg) 32 | } 33 | override def terminate(exitState: Either[String, Unit]): Unit = 34 | exitState match { 35 | case Left(_) => platform.exit(1) 36 | case Right(_) => platform.exit(0) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/scopt/OParserSetup.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.scopt 2 | 3 | trait OParserSetup { 4 | def renderingMode: RenderingMode 5 | def errorOnUnknownArgument: Boolean 6 | 7 | /** 8 | * Show usage text on parse error. 9 | * Defaults to None, which displays the usage text if 10 | * --help option is not defined. 11 | */ 12 | def showUsageOnError: Option[Boolean] 13 | 14 | } 15 | 16 | abstract class DefaultOParserSetup extends OParserSetup { 17 | override def renderingMode: RenderingMode = RenderingMode.TwoColumns 18 | override def errorOnUnknownArgument: Boolean = true 19 | override def showUsageOnError: Option[Boolean] = None 20 | } 21 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/scopt/PlatformReadInstances.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.scopt 2 | 3 | import java.net.{ URL, UnknownHostException } 4 | import collection.{ Seq => CSeq } 5 | import scala.io.Source 6 | 7 | private[scopt] object platform { 8 | val _NL = System.getProperty("line.separator") 9 | 10 | import java.util.{ Locale, Calendar, GregorianCalendar } 11 | import java.text.SimpleDateFormat 12 | import java.io.File 13 | import java.nio.file.{ Path, Paths } 14 | import java.net.{ InetAddress, URI } 15 | 16 | type ParseException = java.text.ParseException 17 | def mkParseEx(s: String, p: Int) = new java.text.ParseException(s, p) 18 | 19 | trait PlatformReadInstances { 20 | def calendarRead(pattern: String): Read[Calendar] = calendarRead(pattern, Locale.getDefault) 21 | def calendarRead(pattern: String, locale: Locale): Read[Calendar] = 22 | Read.reads { s => 23 | val fmt = new SimpleDateFormat(pattern, locale) 24 | val c = new GregorianCalendar(locale) 25 | c.setTime(fmt.parse(s)) 26 | c 27 | } 28 | 29 | implicit val yyyymmdddRead: Read[Calendar] = calendarRead("yyyy-MM-dd") 30 | implicit val fileRead: Read[File] = Read.reads { new File(_) } 31 | implicit val pathRead: Read[Path] = Read.reads { Paths.get(_) } 32 | implicit val sourceRead: Read[Source] = Read.reads { Source.fromFile(_) } 33 | implicit val inetAddress: Read[InetAddress] = Read.reads { InetAddress.getByName(_) } 34 | implicit val urlRead: Read[URL] = Read.reads { new URL(_) } 35 | } 36 | 37 | def applyArgumentExHandler[C]( 38 | desc: String, 39 | arg: String 40 | ): PartialFunction[Throwable, Either[CSeq[String], C]] = { 41 | case e: NumberFormatException => 42 | Left(List(desc + " expects a number but was given '" + arg + "'")) 43 | case e: UnknownHostException => 44 | Left( 45 | List( 46 | desc + " expects a host name or an IP address but was given '" + arg + "' which is invalid" 47 | ) 48 | ) 49 | case e: ParseException if e.getMessage.contains("Unparseable date") => 50 | Left( 51 | List( 52 | s"$desc date format ('$arg') couldn't be parsed using implicit instance of `Read[Date]`." 53 | ) 54 | ) 55 | case _: ParseException => 56 | Left(List(s"$desc expects a Scala duration but was given '$arg'")) 57 | case e: Throwable => Left(List(desc + " failed when given '" + arg + "'. " + e.getMessage)) 58 | } 59 | 60 | def exit(status: Int): Nothing = sys.exit(status) 61 | } 62 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/scopt/Read.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.scopt 2 | 3 | import java.net.URI 4 | import scala.collection.{ Seq => CSeq } 5 | import scala.collection.immutable.{ Seq => ISeq } 6 | 7 | trait Read[A] { self => 8 | def arity: Int 9 | def tokensToRead: Int = if (arity == 0) 0 else 1 10 | def reads: String => A 11 | def map[B](f: A => B): Read[B] = new Read[B] { 12 | val arity = self.arity 13 | val reads = self.reads andThen f 14 | } 15 | } 16 | 17 | object Read extends platform.PlatformReadInstances { 18 | 19 | import scala.concurrent.duration.{ Duration, FiniteDuration } 20 | 21 | def reads[A](f: String => A): Read[A] = new Read[A] { 22 | val arity = 1 23 | val reads = f 24 | } 25 | implicit val stringRead: Read[String] = reads { identity } 26 | implicit val charRead: Read[Char] = 27 | reads { 28 | _.toCharArray match { 29 | case Array(char) => char 30 | case s => 31 | throw new IllegalArgumentException("'" + s + "' is not a char.") 32 | } 33 | } 34 | implicit val doubleRead: Read[Double] = reads { _.toDouble } 35 | implicit val booleanRead: Read[Boolean] = 36 | reads { 37 | _.toLowerCase match { 38 | case "true" => true 39 | case "false" => false 40 | case "yes" => true 41 | case "no" => false 42 | case "1" => true 43 | case "0" => false 44 | case s => 45 | throw new IllegalArgumentException("'" + s + "' is not a boolean.") 46 | } 47 | } 48 | 49 | private def fixedPointWithRadix(str: String): (String, Int) = str.toLowerCase match { 50 | case s if s.startsWith("0x") => (s.stripPrefix("0x"), 16) 51 | case s => (s, 10) 52 | } 53 | implicit val intRead: Read[Int] = reads { str => 54 | val (s, radix) = fixedPointWithRadix(str) 55 | Integer.parseInt(s, radix) 56 | } 57 | implicit val longRead: Read[Long] = reads { str => 58 | val (s, radix) = fixedPointWithRadix(str) 59 | java.lang.Long.parseLong(s, radix) 60 | } 61 | implicit val shortRead: Read[Short] = reads { str => 62 | val (s, radix) = fixedPointWithRadix(str) 63 | java.lang.Short.parseShort(s, radix) 64 | } 65 | implicit val bigIntRead: Read[BigInt] = reads { str => 66 | val (s, radix) = fixedPointWithRadix(str) 67 | BigInt(s, radix) 68 | } 69 | 70 | implicit val bigDecimalRead: Read[BigDecimal] = reads { BigDecimal(_) } 71 | 72 | implicit val durationRead: Read[Duration] = 73 | reads { 74 | try { 75 | Duration(_) 76 | } catch { 77 | case e: NumberFormatException => throw platform.mkParseEx(e.getMessage, -1) 78 | } 79 | } 80 | 81 | implicit val finiteDurationRead: Read[FiniteDuration] = 82 | durationRead.map { 83 | case d: FiniteDuration => d 84 | case d => throw new IllegalArgumentException("'" + d + "' is not a finite duration.") 85 | } 86 | 87 | implicit def tupleRead[A1: Read, A2: Read]: Read[(A1, A2)] = new Read[(A1, A2)] { 88 | val arity = 2 89 | val reads = { (s: String) => 90 | splitKeyValue(s) match { 91 | case (k, v) => implicitly[Read[A1]].reads(k) -> implicitly[Read[A2]].reads(v) 92 | } 93 | } 94 | } 95 | private def splitKeyValue(s: String): (String, String) = 96 | s.indexOf('=') match { 97 | case -1 => throw new IllegalArgumentException("Expected a key=value pair") 98 | case n: Int => (s.slice(0, n), s.slice(n + 1, s.length)) 99 | } 100 | implicit val unitRead: Read[Unit] = new Read[Unit] { 101 | val arity = 0 102 | val reads = { (s: String) => 103 | () 104 | } 105 | } 106 | 107 | val sep = "," 108 | 109 | // reads("1,2,3,4,5") == Seq(1,2,3,4,5) 110 | implicit def seqRead[A: Read]: Read[CSeq[A]] = reads { (s: String) => 111 | s.split(sep).toList.map(implicitly[Read[A]].reads) 112 | } 113 | // reads("1,2,3,4,5") == List(1,2,3,4,5) 114 | implicit def immutableSeqRead[A: Read]: Read[ISeq[A]] = reads { (s: String) => 115 | s.split(sep).toList.map(implicitly[Read[A]].reads) 116 | } 117 | 118 | // reads("1=false,2=true") == Map(1 -> false, 2 -> true) 119 | implicit def mapRead[K: Read, V: Read]: Read[Map[K, V]] = reads { (s: String) => 120 | s.split(sep).map(implicitly[Read[(K, V)]].reads).toMap 121 | } 122 | 123 | // reads("1=false,1=true") == List((1 -> false), (1 -> true)) 124 | implicit def seqTupleRead[K: Read, V: Read]: Read[CSeq[(K, V)]] = reads { (s: String) => 125 | s.split(sep).map(implicitly[Read[(K, V)]].reads).toList 126 | } 127 | // reads("1=false,1=true") == List((1 -> false), (1 -> true)) 128 | implicit def immutableSeqTupleRead[K: Read, V: Read]: Read[ISeq[(K, V)]] = reads { (s: String) => 129 | s.split(sep).map(implicitly[Read[(K, V)]].reads).toList 130 | } 131 | 132 | implicit def optionRead[A: Read]: Read[Option[A]] = reads { 133 | case "" => None 134 | case str => Some(implicitly[Read[A]].reads(str)) 135 | } 136 | 137 | implicit val uriRead: Read[URI] = Read.reads { new URI(_) } 138 | } 139 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/scopt/RenderingMode.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.scopt 2 | 3 | trait RenderingMode 4 | object RenderingMode { 5 | case object OneColumn extends RenderingMode 6 | case object TwoColumns extends RenderingMode 7 | } 8 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/scopt/Validation.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.scopt 2 | 3 | import scala.collection.{ Seq => CSeq } 4 | 5 | object Validation { 6 | def validateValue[A]( 7 | vs: CSeq[A => Either[String, Unit]] 8 | )(value: A): Either[CSeq[String], Unit] = { 9 | val results = vs map { _.apply(value) } 10 | results.foldLeft(OptionDef.makeSuccess[CSeq[String]]) { (acc, r) => 11 | (acc match { 12 | case Right(_) => List[String]() 13 | case Left(xs) => xs 14 | }) ++ (r match { 15 | case Right(_) => List[String]() 16 | case Left(x) => List[String](x) 17 | }) match { 18 | case CSeq() => acc 19 | case xs => Left(xs) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/sourcecode/LICENSE: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2014 Li Haoyi (haoyi.sg@gmail.com) 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /shaded-libs/src/main/scala/replpp/shaded/sourcecode/SourceContext.scala: -------------------------------------------------------------------------------- 1 | package replpp.shaded.sourcecode 2 | 3 | 4 | abstract class SourceValue[T]{ 5 | def value: T 6 | } 7 | abstract class SourceCompanion[T, V <: SourceValue[T]](build: T => V){ 8 | def apply()(implicit s: V): T = s.value 9 | implicit def wrap(s: T): V = build(s) 10 | } 11 | case class Name(value: String) extends SourceValue[String] 12 | object Name extends SourceCompanion[String, Name](new Name(_)) with NameMacros { 13 | case class Machine(value: String) extends SourceValue[String] 14 | object Machine extends SourceCompanion[String, Machine](new Machine(_)) with NameMachineMacros 15 | } 16 | case class FullName(value: String) extends SourceValue[String] 17 | object FullName extends SourceCompanion[String, FullName](new FullName(_)) with FullNameMacros { 18 | case class Machine(value: String) extends SourceValue[String] 19 | object Machine extends SourceCompanion[String, Machine](new Machine(_)) with FullNameMachineMacros 20 | } 21 | 22 | case class File(value: String) extends SourceValue[String] 23 | object File extends SourceCompanion[String, File](new File(_)) with FileMacros 24 | 25 | case class FileName(value: String) extends SourceValue[String] 26 | object FileName extends SourceCompanion[String, FileName](new FileName(_)) with FileNameMacros 27 | 28 | case class Line(value: Int) extends SourceValue[Int] 29 | object Line extends SourceCompanion[Int, Line](new Line(_)) with LineMacros 30 | case class Enclosing(value: String) extends SourceValue[String] 31 | 32 | object Enclosing extends SourceCompanion[String, Enclosing](new Enclosing(_)) with EnclosingMacros { 33 | case class Machine(value: String) extends SourceValue[String] 34 | object Machine extends SourceCompanion[String, Machine](new Machine(_)) with EnclosingMachineMacros 35 | } 36 | 37 | 38 | case class Pkg(value: String) extends SourceValue[String] 39 | object Pkg extends SourceCompanion[String, Pkg](new Pkg(_)) with PkgMacros 40 | 41 | case class Text[T](value: T, source: String) 42 | object Text extends TextMacros 43 | 44 | case class Args(value: Seq[Seq[Text[_]]]) extends SourceValue[Seq[Seq[Text[_]]]] 45 | object Args extends SourceCompanion[Seq[Seq[Text[_]]], Args](new Args(_)) with ArgsMacros 46 | -------------------------------------------------------------------------------- /srp: -------------------------------------------------------------------------------- 1 | core/target/3.7.0/universal/stage/bin/srp -------------------------------------------------------------------------------- /srp-server: -------------------------------------------------------------------------------- 1 | server/target/3.7.0/universal/stage/bin/srp-server -------------------------------------------------------------------------------- /srp-server.bat: -------------------------------------------------------------------------------- 1 | server/target/3.7.0/universal/stage/bin/srp-server.bat -------------------------------------------------------------------------------- /srp.bat: -------------------------------------------------------------------------------- 1 | core/target/3.7.0/universal/stage/bin/srp.bat -------------------------------------------------------------------------------- /test-scripts/main-requiring-import.sc: -------------------------------------------------------------------------------- 1 | // ./srp --script test-scripts/main-requiring-import.sc --predef test-scripts/sayHello-function.sc 2 | println(sayHello("Michael")) 3 | -------------------------------------------------------------------------------- /test-scripts/main-using-import.sc: -------------------------------------------------------------------------------- 1 | // ./srp --script test-scripts/main-using-import.sc 2 | 3 | //> using file sayHello-function.sc 4 | println(sayHello("Michael")) 5 | -------------------------------------------------------------------------------- /test-scripts/multiple-mains.sc: -------------------------------------------------------------------------------- 1 | // ./srp --script test-scripts/multiple-mains.sc --command foo 2 | 3 | @main def foo() = { 4 | println("in foo") 5 | } 6 | 7 | @main def bar() = { 8 | println("in bar") 9 | } 10 | 11 | -------------------------------------------------------------------------------- /test-scripts/runBeforeCode-main.sc: -------------------------------------------------------------------------------- 1 | // ./srp --script test-scripts/runBeforeCode-main.sc --runBefore 'import Byte.MinValue' 2 | 3 | @main def foo() = { 4 | // val x: Int = "a string" // uncomment to check line number reporting 5 | println(s"test-main2 $MinValue") 6 | } 7 | 8 | -------------------------------------------------------------------------------- /test-scripts/runBeforeCode.sc: -------------------------------------------------------------------------------- 1 | // ./srp --script test-scripts/runBeforeCode.sc --runBefore 'import Byte.MinValue' 2 | // val x: Int = "a string" // uncomment to check line number reporting 3 | println(s"test-main1 $MinValue") 4 | -------------------------------------------------------------------------------- /test-scripts/sayHello-function.sc: -------------------------------------------------------------------------------- 1 | def sayHello(to: String) = s"Hello, $to" 2 | --------------------------------------------------------------------------------