diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c7afba2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Continuous Integration + +env: + JAVA_VERSION: '11' + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'zulu' + cache: 'sbt' + + - name: Setup SBT + uses: sbt/setup-sbt@v1 + + - name: Run tests + run: sbt clean test diff --git a/build.sbt b/build.sbt index f38ee07..dfa46ca 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,12 @@ lazy val replDependencies = Seq( lazy val lspDependencies = Seq( "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.23.1", - "com.google.code.gson" % "gson" % "2.8.2" + "com.google.code.gson" % "gson" % "2.11.0" +) + +lazy val testingDependencies = Seq( + "org.scala-sbt" %% "io" % "1.6.0" % Test, + "org.scalameta" %% "munit" % "0.7.29" % Test ) lazy val kiama: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file(".")) @@ -32,7 +37,7 @@ lazy val kiama: CrossProject = crossProject(JSPlatform, JVMPlatform).in(file("." name := "kiama" ) .jvmSettings( - libraryDependencies ++= (replDependencies ++ lspDependencies), - libraryDependencies += "com.lihaoyi" %% "utest" % "0.8.2" % "test", - testFrameworks += new TestFramework("utest.runner.Framework") + libraryDependencies ++= (replDependencies ++ lspDependencies ++ testingDependencies), + + Test / testOptions += Tests.Argument(TestFrameworks.MUnit, "-oD"), ) diff --git a/jvm/src/main/scala/kiama/util/Compiler.scala b/jvm/src/main/scala/kiama/util/Compiler.scala index f3b88e6..9e1f3f1 100644 --- a/jvm/src/main/scala/kiama/util/Compiler.scala +++ b/jvm/src/main/scala/kiama/util/Compiler.scala @@ -88,7 +88,7 @@ trait Compiler[C <: Config, M <: Message] { } /** - * Run the compiler given a configuration. Overriden in Server to + * Run the compiler given a configuration. Overwritten in Server to * also provide LSP services. */ def run(config: C): Unit = () diff --git a/jvm/src/main/scala/kiama/util/Server.scala b/jvm/src/main/scala/kiama/util/Server.scala index babde45..07fd975 100644 --- a/jvm/src/main/scala/kiama/util/Server.scala +++ b/jvm/src/main/scala/kiama/util/Server.scala @@ -21,7 +21,7 @@ import scala.jdk.CollectionConverters._ * A language server that is mixed with a compiler that provide the basis * for its services. Allows specialisation of configuration via `C`. */ -trait Server[N, C <: Config, M <: Message] extends Compiler[C, M] with LanguageService[N] { +trait Server[N, C <: Config, M <: Message] extends Compiler[C, M] with LanguageService[N, C] { import com.google.gson.{ JsonArray, JsonElement, JsonObject } import java.util.Collections @@ -215,7 +215,7 @@ trait Server[N, C <: Config, M <: Message] extends Compiler[C, M] with LanguageS val after = cells.drop(start + deleteCount) // create if not already exist (this can be the case if cells are re-ordered=delete+insert) - insertedUris.foreach { uri => sources.getOrElseUpdate(uri, RopeSource(Rope.empty, uri)) } + insertedUris.foreach { uri => sources.getOrElseUpdate(uri, BufferSource(GapBuffer.empty, uri)) } val inserted = insertedUris.map(NotebookCell.apply) val updated = (before ++ inserted ++ after).map(c => c.uri -> c).toMap @@ -229,7 +229,8 @@ trait Server[N, C <: Config, M <: Message] extends Compiler[C, M] with LanguageS def onNotebookContentChange( notebookUri: String, cellUri: String, - changes: Seq[TextDocumentContentChangeEvent] + changes: Seq[TextDocumentContentChangeEvent], + config: C ): Unit = { val bs = sources.get(cellUri) match { case Some(buffer: BufferSource) => buffer @@ -261,7 +262,7 @@ trait Server[N, C <: Config, M <: Message] extends Compiler[C, M] with LanguageS // Update and process notebook accordingly notebooks.get(notebookUri).foreach { notebook => notebook.cells.get(cellUri).foreach { cell => - processCell(cell, notebook) + processCell(cell, notebook, config) } } } @@ -269,10 +270,10 @@ trait Server[N, C <: Config, M <: Message] extends Compiler[C, M] with LanguageS /** * Called when a notebook is saved. **/ - def onNotebookSave(notebookUri: String): Unit = { + def onNotebookSave(notebookUri: String, config: C): Unit = { notebooks.get(notebookUri).foreach { notebook => notebook.cells.values.foreach { cell => - processCell(cell, notebook) + processCell(cell, notebook, config) // comment this in to see state of the notebook on save (in server debug mode) // println(sources.get(cell.uri)) } @@ -317,9 +318,19 @@ trait Server[N, C <: Config, M <: Message] extends Compiler[C, M] with LanguageS } } + def toURI(filename: String): String = filename match { + case _ if filename startsWith "file:" => + filename + case _ if filename startsWith "vscode-notebook-cell:" => + filename + case _ if filename startsWith "./" => + s"file://${Filenames.cwd()}/${Filenames.dropPrefix(filename, ".")}" + case _ => + s"file://$filename" + } + def publishDiagnostics(name: String, diagnostics: Vector[Diagnostic]): Unit = { - val uri = if (name startsWith "file://") name else s"file://$name" - val params = new PublishDiagnosticsParams(uri, seqToJavaList(diagnostics)) + val params = new PublishDiagnosticsParams(toURI(name), seqToJavaList(diagnostics)) client.publishDiagnostics(params) } @@ -459,18 +470,12 @@ trait Server[N, C <: Config, M <: Message] extends Compiler[C, M] with LanguageS } // Support for services - def locationOfNode(node: N): Location = { (positions.getStart(node), positions.getFinish(node)) match { case (start @ Some(st), finish @ Some(_)) => - st.source match { - case StringSource(_, name) => - val s = convertPosition(start) - val f = convertPosition(finish) - new Location(name, new LSPRange(s, f)) - case _ => - null - } + val s = convertPosition(start) + val f = convertPosition(finish) + new Location(toURI(st.source.name), new LSPRange(s, f)) case _ => null } @@ -478,10 +483,9 @@ trait Server[N, C <: Config, M <: Message] extends Compiler[C, M] with LanguageS def rangeOfNode(node: N): LSPRange = convertRange(positions.getStart(node), positions.getFinish(node)) - } -trait LanguageService[N] { +trait LanguageService[N, C] { /** * A representation of a simple named code action that replaces @@ -547,7 +551,7 @@ trait LanguageService[N] { * Process a cell in the given notebook. Default is to do nothing, * meaning cells won't be processed until a compiler overrides this. */ - def processCell(cell: NotebookCell, notebook: Notebook): Unit = () + def processCell(cell: NotebookCell, notebook: Notebook, config: C): Option[Any] = None } class Services[N, C <: Config, M <: Message]( @@ -702,7 +706,8 @@ class Services[N, C <: Config, M <: Message]( server.onNotebookContentChange( notebookUri, change.getDocument.getUri, - change.getChanges.asScala.toSeq + change.getChanges.asScala.toSeq, + config ) } } @@ -716,7 +721,7 @@ class Services[N, C <: Config, M <: Message]( @JsonNotification("notebookDocument/didSave") def notebookDidSave(params: DidSaveNotebookDocumentParams): Unit = { - server.onNotebookSave(params.getNotebookDocument.getUri) + server.onNotebookSave(params.getNotebookDocument.getUri, config) } @JsonNotification("notebookDocument/didClose") diff --git a/jvm/src/test/scala/kiama/util/GapBufferTests.scala b/jvm/src/test/scala/kiama/util/GapBufferTests.scala index 008c5d3..ca7e45f 100644 --- a/jvm/src/test/scala/kiama/util/GapBufferTests.scala +++ b/jvm/src/test/scala/kiama/util/GapBufferTests.scala @@ -1,240 +1,236 @@ package kiama package util -import utest._ +class GapBufferTests extends munit.FunSuite { -object GapBufferTests extends TestSuite { - val tests = Tests { - test("empty buffer") { - val buffer = GapBuffer("") - assert(buffer.lineCount == 0) - assert(buffer.content == "") - } + test("empty buffer") { + val buffer = GapBuffer("") + assert(buffer.lineCount == 0) + assert(buffer.content == "") + } - test("single line") { - val buffer = GapBuffer("test") - assert(buffer.lineCount == 1) - assert(buffer.line(0).contains("test")) - assert(buffer.content == "test") + test("single line") { + val buffer = GapBuffer("test") + assert(buffer.lineCount == 1) + assert(buffer.line(0).contains("test")) + assert(buffer.content == "test") + } + + test("multi line") { + val buffer = GapBuffer("line1\nline2\nline3") + assert(buffer.lineCount == 3) + assert(buffer.line(0).contains("line1")) + assert(buffer.line(1).contains("line2")) + assert(buffer.line(2).contains("line3")) + } + + test("replaceRange - empty replacement") { + val buffer = GapBuffer("test") + val modified = buffer.replaceRange(0, 0, 0, 0, "") + assert(modified.content == "test") + assert(modified.lineCount == 1) + } + + test("replaceRange - single line insert") { + val buffer = GapBuffer("test") + val modified = buffer.replaceRange(0, 0, 0, 0, "Hello, ") + assert(modified.content == "Hello, test") + assert(modified.lineCount == 1) + } + + test("replaceRange - multi line insert") { + val buffer = GapBuffer("test") + val modified = buffer.replaceRange(0, 0, 0, 0, "Hello!\n\n\n") + assert(modified.lineCount == 4) + assert(modified.line(0).contains("Hello!")) + assert(modified.line(1).contains("")) + assert(modified.line(2).contains("")) + assert(modified.line(3).contains("test")) + } + + test("replaceRange - replace entire line") { + val buffer = GapBuffer("test") + val modified = buffer.replaceRange(0, 0, 0, 4, "replaced") + assert(modified.content == "replaced") + assert(modified.lineCount == 1) + } + + test("replaceRange - replace across lines") { + val buffer = GapBuffer("line1\nline2\nline3") + val modified = buffer.replaceRange(0, 2, 1, 2, "ne1-li") + assert(modified.content == "line1-line2\nline3") + assert(modified.lineCount == 2) + } + + test("edge cases") { + val buffer = GapBuffer("test") + test("invalid line access") { + assert(buffer.line(-1).isEmpty) + assert(buffer.line(1).isEmpty) } - test("multi line") { - val buffer = GapBuffer("line1\nline2\nline3") - assert(buffer.lineCount == 3) - assert(buffer.line(0).contains("line1")) - assert(buffer.line(1).contains("line2")) - assert(buffer.line(2).contains("line3")) + test("line length") { + assert(buffer.lineLength(0) == 4) + assert(buffer.lineLength(1) == 0) } + } - test("replaceRange - empty replacement") { - val buffer = GapBuffer("test") - val modified = buffer.replaceRange(0, 0, 0, 0, "") - assert(modified.content == "test") - assert(modified.lineCount == 1) + test("compound edits") { + test("incrementally building content") { + var buffer = GapBuffer("") + assert(buffer.content == "") + + buffer = buffer.replaceRange(0, 0, 0, 0, "def") + assert(buffer.content == "def") + + buffer = buffer.replaceRange(0, 3, 0, 3, " main() {\n ") + assert(buffer.content == "def main() {\n ") + + buffer = buffer.replaceRange(1, 2, 1, 2, "println(") + assert(buffer.content == "def main() {\n println(") + + buffer = buffer.replaceRange(1, 10, 1, 10, "\"Hello!\"") + assert(buffer.content == "def main() {\n println(\"Hello!\"") + + buffer = buffer.replaceRange(1, 18, 1, 18, ")") + assert(buffer.content == "def main() {\n println(\"Hello!\")") + + buffer = buffer.replaceRange(1, 19, 1, 19, "\n}") + assert(buffer.content == "def main() {\n println(\"Hello!\")\n}") } - test("replaceRange - single line insert") { - val buffer = GapBuffer("test") - val modified = buffer.replaceRange(0, 0, 0, 0, "Hello, ") - assert(modified.content == "Hello, test") - assert(modified.lineCount == 1) + test("multiple line replacements") { + var buffer = GapBuffer("line1\nline2\nline3\nline4\nline5") + assert(buffer.lineCount == 5) + + // Replace lines 2-4 with new content + buffer = buffer.replaceRange(1, 0, 3, 5, "newline2\ninserted\nnewline4") + assert(buffer.lineCount == 5) + assert(buffer.line(0).contains("line1")) + assert(buffer.line(1).contains("newline2")) + assert(buffer.line(2).contains("inserted")) + assert(buffer.line(3).contains("newline4")) + assert(buffer.line(4).contains("line5")) + + // Now replace part of first line and add content + buffer = buffer.replaceRange(0, 2, 1, 3, "REPLACED\nmore") + assert(buffer.content == "liREPLACED\nmoreline2\ninserted\nnewline4\nline5") + + // Delete some lines + buffer = buffer.replaceRange(1, 0, 3, 0, "") + assert(buffer.lineCount == 3) + assert(buffer.line(0).contains("liREPLACED")) + assert(buffer.line(1).contains("newline4")) + assert(buffer.line(2).contains("line5")) } - test("replaceRange - multi line insert") { - val buffer = GapBuffer("test") - val modified = buffer.replaceRange(0, 0, 0, 0, "Hello!\n\n\n") - assert(modified.lineCount == 4) - assert(modified.line(0).contains("Hello!")) - assert(modified.line(1).contains("")) - assert(modified.line(2).contains("")) - assert(modified.line(3).contains("test")) + test("editing with empty lines") { + var buffer = GapBuffer("first\n\n\nlast") + assert(buffer.lineCount == 4) + + // Insert into empty line + buffer = buffer.replaceRange(1, 0, 1, 0, "middle") + assert(buffer.lineCount == 4) + assert(buffer.line(1).contains("middle")) + + // Replace empty line + buffer = buffer.replaceRange(2, 0, 2, 0, "also here") + assert(buffer.lineCount == 4) + assert(buffer.line(0).contains("first")) + assert(buffer.line(1).contains("middle")) + assert(buffer.line(2).contains("also here")) + assert(buffer.line(3).contains("last")) + + // Create more empty lines + buffer = buffer.replaceRange(2, 4, 2, 9, "\n\n\nhere") + assert(buffer.lineCount == 7) + assert(buffer.line(2).contains("also")) + assert(buffer.line(3).contains("")) + assert(buffer.line(4).contains("")) + assert(buffer.line(5).contains("here")) + assert(buffer.line(6).contains("last")) } - test("replaceRange - replace entire line") { - val buffer = GapBuffer("test") - val modified = buffer.replaceRange(0, 0, 0, 4, "replaced") - assert(modified.content == "replaced") - assert(modified.lineCount == 1) + test("replacing multiple lines with single line") { + var buffer = GapBuffer("one\ntwo\nthree\nfour\nfive") + + // Replace 3 lines with single line + buffer = buffer.replaceRange(1, 0, 3, 4, "REPLACED") + assert(buffer.lineCount == 3) + assert(buffer.line(0).contains("one")) + assert(buffer.line(1).contains("REPLACED")) + assert(buffer.line(2).contains("five")) + + // Replace 2 lines with single line leaving partial content + buffer = buffer.replaceRange(0, 2, 1, 3, "NEW") + assert(buffer.lineCount == 2) + assert(buffer.line(0).contains("onNEWLACED")) + assert(buffer.line(1).contains("five")) } - test("replaceRange - replace across lines") { - val buffer = GapBuffer("line1\nline2\nline3") - val modified = buffer.replaceRange(0, 2, 1, 2, "ne1-li") - assert(modified.content == "line1-line2\nline3") - assert(modified.lineCount == 2) + test("replacing lines with more lines") { + var buffer = GapBuffer("1\n2\n3\n4\n5") + + // Replace 2 lines with 3 lines + buffer = buffer.replaceRange(1, 0, 2, 1, "two\nextra\nthree") + assert(buffer.lineCount == 6) + assert(buffer.line(0).contains("1")) + assert(buffer.line(1).contains("two")) + assert(buffer.line(2).contains("extra")) + assert(buffer.line(3).contains("three")) + assert(buffer.line(4).contains("4")) + assert(buffer.line(5).contains("5")) + + // Replace 3 lines with 2 lines + buffer = buffer.replaceRange(2, 0, 4, 1, "merged1\nmerged2") + assert(buffer.lineCount == 5) + assert(buffer.line(0).contains("1")) + assert(buffer.line(1).contains("two")) + assert(buffer.line(2).contains("merged1")) + assert(buffer.line(3).contains("merged2")) + assert(buffer.line(4).contains("5")) } - test("edge cases") { - val buffer = GapBuffer("test") - test("invalid line access") { - assert(buffer.line(-1).isEmpty) - assert(buffer.line(1).isEmpty) - } - - test("line length") { - assert(buffer.lineLength(0) == 4) - assert(buffer.lineLength(1) == 0) - } + test("incremental line removal") { + var buffer = GapBuffer("a\nb\nc\nd\ne") + assert(buffer.content == "a\nb\nc\nd\ne") + + // Remove 2 lines + buffer = buffer.replaceRange(1, 0, 2, 1, "") + assert(buffer.content == "a\n\nd\ne") + + // Remove another 2 lines but preserve part of last line + buffer = buffer.replaceRange(0, 1, 3, 0, "") + assert(buffer.content == "ae") + + // Remove all content + buffer = buffer.replaceRange(0, 0, 0, 2, "") + assert(buffer.content == "") } - test("compound edits") { - test("incrementally building content") { - var buffer = GapBuffer("") - assert(buffer.content == "") - - buffer = buffer.replaceRange(0, 0, 0, 0, "def") - assert(buffer.content == "def") - - buffer = buffer.replaceRange(0, 3, 0, 3, " main() {\n ") - assert(buffer.content == "def main() {\n ") - - buffer = buffer.replaceRange(1, 2, 1, 2, "println(") - assert(buffer.content == "def main() {\n println(") - - buffer = buffer.replaceRange(1, 10, 1, 10, "\"Hello!\"") - assert(buffer.content == "def main() {\n println(\"Hello!\"") - - buffer = buffer.replaceRange(1, 18, 1, 18, ")") - assert(buffer.content == "def main() {\n println(\"Hello!\")") - - buffer = buffer.replaceRange(1, 19, 1, 19, "\n}") - assert(buffer.content == "def main() {\n println(\"Hello!\")\n}") - } - - test("multiple line replacements") { - var buffer = GapBuffer("line1\nline2\nline3\nline4\nline5") - assert(buffer.lineCount == 5) - - // Replace lines 2-4 with new content - buffer = buffer.replaceRange(1, 0, 3, 5, "newline2\ninserted\nnewline4") - assert(buffer.lineCount == 5) - assert(buffer.line(0).contains("line1")) - assert(buffer.line(1).contains("newline2")) - assert(buffer.line(2).contains("inserted")) - assert(buffer.line(3).contains("newline4")) - assert(buffer.line(4).contains("line5")) - - // Now replace part of first line and add content - buffer = buffer.replaceRange(0, 2, 1, 3, "REPLACED\nmore") - assert(buffer.content == "liREPLACED\nmoreline2\ninserted\nnewline4\nline5") - - // Delete some lines - buffer = buffer.replaceRange(1, 0, 3, 0, "") - assert(buffer.lineCount == 3) - assert(buffer.line(0).contains("liREPLACED")) - assert(buffer.line(1).contains("newline4")) - assert(buffer.line(2).contains("line5")) - } - - test("editing with empty lines") { - var buffer = GapBuffer("first\n\n\nlast") - assert(buffer.lineCount == 4) - - // Insert into empty line - buffer = buffer.replaceRange(1, 0, 1, 0, "middle") - assert(buffer.lineCount == 4) - assert(buffer.line(1).contains("middle")) - - // Replace empty line - buffer = buffer.replaceRange(2, 0, 2, 0, "also here") - assert(buffer.lineCount == 4) - assert(buffer.line(0).contains("first")) - assert(buffer.line(1).contains("middle")) - assert(buffer.line(2).contains("also here")) - assert(buffer.line(3).contains("last")) - - // Create more empty lines - buffer = buffer.replaceRange(2, 4, 2, 9, "\n\n\nhere") - assert(buffer.lineCount == 7) - assert(buffer.line(2).contains("also")) - assert(buffer.line(3).contains("")) - assert(buffer.line(4).contains("")) - assert(buffer.line(5).contains("here")) - assert(buffer.line(6).contains("last")) - } - - test("line count transformations") { - test("replacing multiple lines with single line") { - var buffer = GapBuffer("one\ntwo\nthree\nfour\nfive") - - // Replace 3 lines with single line - buffer = buffer.replaceRange(1, 0, 3, 4, "REPLACED") - assert(buffer.lineCount == 3) - assert(buffer.line(0).contains("one")) - assert(buffer.line(1).contains("REPLACED")) - assert(buffer.line(2).contains("five")) - - // Replace 2 lines with single line leaving partial content - buffer = buffer.replaceRange(0, 2, 1, 3, "NEW") - assert(buffer.lineCount == 2) - assert(buffer.line(0).contains("onNEWLACED")) - assert(buffer.line(1).contains("five")) - } - - test("replacing lines with more lines") { - var buffer = GapBuffer("1\n2\n3\n4\n5") - - // Replace 2 lines with 3 lines - buffer = buffer.replaceRange(1, 0, 2, 1, "two\nextra\nthree") - assert(buffer.lineCount == 6) - assert(buffer.line(0).contains("1")) - assert(buffer.line(1).contains("two")) - assert(buffer.line(2).contains("extra")) - assert(buffer.line(3).contains("three")) - assert(buffer.line(4).contains("4")) - assert(buffer.line(5).contains("5")) - - // Replace 3 lines with 2 lines - buffer = buffer.replaceRange(2, 0, 4, 1, "merged1\nmerged2") - assert(buffer.lineCount == 5) - assert(buffer.line(0).contains("1")) - assert(buffer.line(1).contains("two")) - assert(buffer.line(2).contains("merged1")) - assert(buffer.line(3).contains("merged2")) - assert(buffer.line(4).contains("5")) - } - - test("incremental line removal") { - var buffer = GapBuffer("a\nb\nc\nd\ne") - assert(buffer.content == "a\nb\nc\nd\ne") - - // Remove 2 lines - buffer = buffer.replaceRange(1, 0, 2, 1, "") - assert(buffer.content == "a\n\nd\ne") - - // Remove another 2 lines but preserve part of last line - buffer = buffer.replaceRange(0, 1, 3, 0, "") - assert(buffer.content == "ae") - - // Remove all content - buffer = buffer.replaceRange(0, 0, 0, 2, "") - assert(buffer.content == "") - } - - test("mixed transformations") { - var buffer = GapBuffer("1\n2\n3\n4\n5") - - // First replace multiple lines with single - buffer = buffer.replaceRange(1, 0, 3, 1, "merged") - assert(buffer.lineCount == 3) - assert(buffer.line(1).contains("merged")) - assert(buffer.content == "1\nmerged\n5") - - // Then replace single line with multiple - buffer = buffer.replaceRange(1, 0, 1, 6, "a\nb\nc") - assert(buffer.content == "1\na\nb\nc\n5") - assert(buffer.lineCount == 5) - assert(buffer.line(1).contains("a")) - assert(buffer.line(2).contains("b")) - assert(buffer.line(3).contains("c")) - - // Replace all with single line - buffer = buffer.replaceRange(0, 0, 4, 1, "only line") - assert(buffer.content == "only line") - assert(buffer.lineCount == 1) - assert(buffer.line(0).contains("only line")) - } - } + test("mixed transformations") { + var buffer = GapBuffer("1\n2\n3\n4\n5") + + // First replace multiple lines with single + buffer = buffer.replaceRange(1, 0, 3, 1, "merged") + assert(buffer.lineCount == 3) + assert(buffer.line(1).contains("merged")) + assert(buffer.content == "1\nmerged\n5") + + // Then replace single line with multiple + buffer = buffer.replaceRange(1, 0, 1, 6, "a\nb\nc") + assert(buffer.content == "1\na\nb\nc\n5") + assert(buffer.lineCount == 5) + assert(buffer.line(1).contains("a")) + assert(buffer.line(2).contains("b")) + assert(buffer.line(3).contains("c")) + + // Replace all with single line + buffer = buffer.replaceRange(0, 0, 4, 1, "only line") + assert(buffer.content == "only line") + assert(buffer.lineCount == 1) + assert(buffer.line(0).contains("only line")) } + } } diff --git a/jvm/src/test/scala/kiama/util/RopeTests.scala b/jvm/src/test/scala/kiama/util/RopeTests.scala index 2e51b7d..769818a 100644 --- a/jvm/src/test/scala/kiama/util/RopeTests.scala +++ b/jvm/src/test/scala/kiama/util/RopeTests.scala @@ -1,192 +1,190 @@ package kiama package util -import utest._ - -object RopeTests extends TestSuite { - val tests = Tests { - test("empty rope") { - val rope = Rope("") - assert(rope.length == 0) - assert(rope.toString == "") - assert(rope.iterator.toList.isEmpty) - } - test("single character operations") { - val rope = Rope("hello") - assert(rope.length == 5) - assert(rope.charAt(0) == 'h') - assert(rope.charAt(4) == 'o') - - intercept[IllegalArgumentException] { - rope.charAt(-1) - } - intercept[IllegalArgumentException] { - rope.charAt(5) - } - } +class RopeTests extends munit.FunSuite { + + test("empty rope") { + val rope = Rope("") + assert(rope.length == 0) + assert(rope.toString == "") + assert(rope.iterator.toList.isEmpty) + } - test("concatenation") { - val rope1 = Rope("Hello") - val rope2 = Rope(" World") - val combined = rope1.concat(rope2) - assert(combined.toString == "Hello World") + test("single character operations") { + val rope = Rope("hello") + assert(rope.length == 5) + assert(rope.charAt(0) == 'h') + assert(rope.charAt(4) == 'o') - // test empty concatenations - assert(rope1.concat(Rope("")).toString == "Hello") - assert(Rope("").concat(rope2).toString == " World") - assert(Rope("").concat(Rope("")).toString == "") + intercept[IllegalArgumentException] { + rope.charAt(-1) + } + intercept[IllegalArgumentException] { + rope.charAt(5) } + } - test("substring") { - val rope = Rope("Hello World") + test("concatenation") { + val rope1 = Rope("Hello") + val rope2 = Rope(" World") + val combined = rope1.concat(rope2) + assert(combined.toString == "Hello World") - // full string - assert(rope.substring(0, 11).toString == "Hello World") + // test empty concatenations + assert(rope1.concat(Rope("")).toString == "Hello") + assert(Rope("").concat(rope2).toString == " World") + assert(Rope("").concat(Rope("")).toString == "") + } + + test("substring") { + val rope = Rope("Hello World") + + // full string + assert(rope.substring(0, 11).toString == "Hello World") - // partial string - assert(rope.substring(0, 5).toString == "Hello") - assert(rope.substring(6, 11).toString == "World") + // partial string + assert(rope.substring(0, 5).toString == "Hello") + assert(rope.substring(6, 11).toString == "World") - // empty substring - assert(rope.substring(0, 0).toString == "") - assert(rope.substring(5, 5).toString == "") + // empty substring + assert(rope.substring(0, 0).toString == "") + assert(rope.substring(5, 5).toString == "") - // single character - assert(rope.substring(0, 1).toString == "H") + // single character + assert(rope.substring(0, 1).toString == "H") - intercept[IllegalArgumentException] { - rope.substring(-1, 5) - } - intercept[IllegalArgumentException] { - rope.substring(5, 12) - } - intercept[IllegalArgumentException] { - rope.substring(6, 5) - } + intercept[IllegalArgumentException] { + rope.substring(-1, 5) } + intercept[IllegalArgumentException] { + rope.substring(5, 12) + } + intercept[IllegalArgumentException] { + rope.substring(6, 5) + } + } - test("delete") { - val rope = Rope("Hello World") + test("delete") { + val rope = Rope("Hello World") - // delete from middle - assert(rope.delete(5, 6).toString == "HelloWorld") + // delete from middle + assert(rope.delete(5, 6).toString == "HelloWorld") - // delete multiple characters - assert(rope.delete(5, 11).toString == "Hello") + // delete multiple characters + assert(rope.delete(5, 11).toString == "Hello") - // delete from start - assert(rope.delete(0, 6).toString == "World") + // delete from start + assert(rope.delete(0, 6).toString == "World") - // delete nothing - assert(rope.delete(5, 5).toString == "Hello World") + // delete nothing + assert(rope.delete(5, 5).toString == "Hello World") - // delete everything - assert(rope.delete(0, 11).toString == "") + // delete everything + assert(rope.delete(0, 11).toString == "") - intercept[IllegalArgumentException] { - rope.delete(-1, 5) - } - intercept[IllegalArgumentException] { - rope.delete(5, 12) - } - intercept[IllegalArgumentException] { - rope.delete(6, 5) - } + intercept[IllegalArgumentException] { + rope.delete(-1, 5) } + intercept[IllegalArgumentException] { + rope.delete(5, 12) + } + intercept[IllegalArgumentException] { + rope.delete(6, 5) + } + } - test("insert") { - val rope = Rope("Hello World") + test("insert") { + val rope = Rope("Hello World") - // insert at start - assert(rope.insert(0, "Start: ").toString == "Start: Hello World") + // insert at start + assert(rope.insert(0, "Start: ").toString == "Start: Hello World") - // insert in middle - assert(rope.insert(5, " there").toString == "Hello there World") + // insert in middle + assert(rope.insert(5, " there").toString == "Hello there World") - // insert at end - assert(rope.insert(11, "!").toString == "Hello World!") + // insert at end + assert(rope.insert(11, "!").toString == "Hello World!") - // insert empty string - assert(rope.insert(5, "").toString == "Hello World") + // insert empty string + assert(rope.insert(5, "").toString == "Hello World") - // insert with multiple characters - assert(rope.insert(6, "beautiful ").toString == "Hello beautiful World") + // insert with multiple characters + assert(rope.insert(6, "beautiful ").toString == "Hello beautiful World") - intercept[IllegalArgumentException] { - rope.insert(-1, "test") - } - intercept[IllegalArgumentException] { - rope.insert(12, "test") - } + intercept[IllegalArgumentException] { + rope.insert(-1, "test") } - - test("compound operations") { - var rope = Rope("Hello World") - - // insert then delete - rope = rope.insert(5, " beautiful") - assert(rope.toString == "Hello beautiful World") - rope = rope.delete(5, 15) - assert(rope.toString == "Hello World") - - // substring then insert - rope = rope.substring(0, 5) - assert(rope.toString == "Hello") - rope = rope.insert(5, " there") - assert(rope.toString == "Hello there") - - // multiple operations - rope = Rope("base") - .insert(0, "pre") - .insert(7, "post") - .delete(3, 7) - .insert(3, "middle") - assert(rope.toString == "premiddlepost") + intercept[IllegalArgumentException] { + rope.insert(12, "test") } + } + + test("compound operations") { + var rope = Rope("Hello World") + + // insert then delete + rope = rope.insert(5, " beautiful") + assert(rope.toString == "Hello beautiful World") + rope = rope.delete(5, 15) + assert(rope.toString == "Hello World") + + // substring then insert + rope = rope.substring(0, 5) + assert(rope.toString == "Hello") + rope = rope.insert(5, " there") + assert(rope.toString == "Hello there") + + // multiple operations + rope = Rope("base") + .insert(0, "pre") + .insert(7, "post") + .delete(3, 7) + .insert(3, "middle") + assert(rope.toString == "premiddlepost") + } - test("long string handling") { - val longStr = "a" * 1000 - val rope = Rope(longStr) - assert(rope.length == 1000) - assert(rope.toString == longStr) + test("long string handling") { + val longStr = "a" * 1000 + val rope = Rope(longStr) + assert(rope.length == 1000) + assert(rope.toString == longStr) - // test splitting into chunks - assert(rope.charAt(500) == 'a') + // test splitting into chunks + assert(rope.charAt(500) == 'a') - // substring on long string - val sub = rope.substring(100, 200) - assert(sub.length == 100) - assert(sub.toString == "a" * 100) - } + // substring on long string + val sub = rope.substring(100, 200) + assert(sub.length == 100) + assert(sub.toString == "a" * 100) + } - test("iterator") { - val rope = Rope("Hello") - val chars = rope.iterator.toList - assert(chars == List('H', 'e', 'l', 'l', 'o')) + test("iterator") { + val rope = Rope("Hello") + val chars = rope.iterator.toList + assert(chars == List('H', 'e', 'l', 'l', 'o')) - // empty rope iterator - assert(Rope("").iterator.toList.isEmpty) + // empty rope iterator + assert(Rope("").iterator.toList.isEmpty) - // iterator after operations - val modified = rope.insert(5, " World") - assert(modified.iterator.toList == "Hello World".toList) - } + // iterator after operations + val modified = rope.insert(5, " World") + assert(modified.iterator.toList == "Hello World".toList) + } - test("edge cases") { - // very short strings - assert(Rope("a").toString == "a") - assert(Rope("a").length == 1) + test("edge cases") { + // very short strings + assert(Rope("a").toString == "a") + assert(Rope("a").length == 1) - // strings around chunk threshold - val threshold = Rope.LeafThreshold - val nearThreshold = "a" * (threshold - 1) - val atThreshold = "a" * threshold - val overThreshold = "a" * (threshold + 1) + // strings around chunk threshold + val threshold = Rope.LeafThreshold + val nearThreshold = "a" * (threshold - 1) + val atThreshold = "a" * threshold + val overThreshold = "a" * (threshold + 1) - assert(Rope(nearThreshold).toString == nearThreshold) - assert(Rope(atThreshold).toString == atThreshold) - assert(Rope(overThreshold).toString == overThreshold) - } + assert(Rope(nearThreshold).toString == nearThreshold) + assert(Rope(atThreshold).toString == atThreshold) + assert(Rope(overThreshold).toString == overThreshold) } } diff --git a/project/plugins.sbt b/project/plugins.sbt index 1e0fa96..80dcc6a 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,2 +1,2 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") diff --git a/shared/src/main/scala/kiama/util/GapBuffer.scala b/shared/src/main/scala/kiama/util/GapBuffer.scala index 2260792..a8c0564 100644 --- a/shared/src/main/scala/kiama/util/GapBuffer.scala +++ b/shared/src/main/scala/kiama/util/GapBuffer.scala @@ -157,4 +157,5 @@ object GapBuffer { GapBuffer(leftLines = lines) } } + val empty = GapBuffer("") } diff --git a/shared/src/main/scala/kiama/util/Source.scala b/shared/src/main/scala/kiama/util/Source.scala index 929c493..f86bee2 100644 --- a/shared/src/main/scala/kiama/util/Source.scala +++ b/shared/src/main/scala/kiama/util/Source.scala @@ -99,7 +99,7 @@ case class StringSource(content: String, name: String = "") extends Source * A source that is a gap buffer (for easier incremental update). */ case class BufferSource(contents: GapBuffer, name: String = "") extends Source { - lazy val content: String = contents.toString + lazy val content: String = contents.content } /**