From 3432c5bf355063dcaa1855ad7cbfc718e235e38e Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 9 Oct 2020 09:24:36 -0700 Subject: [PATCH 01/54] Initial rough draft of hover functionality --- .gitignore | 3 + .scalafmt.conf | 1 + .vscode/launch.json | 18 +++ .vscode/settings.json | 5 + build.sbt | 42 +++--- project/build.properties | 2 +- project/metals.sbt | 4 + project/project/metals.sbt | 4 + project/project/project/metals.sbt | 4 + src/main/scala/millfork/Main.scala | 24 ++++ .../millfork/language/MfLanguageClient.scala | 69 ++++++++++ .../millfork/language/MfLanguageServer.scala | 123 ++++++++++++++++++ 12 files changed, 283 insertions(+), 16 deletions(-) create mode 100644 .scalafmt.conf create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 project/metals.sbt create mode 100644 project/project/metals.sbt create mode 100644 project/project/project/metals.sbt create mode 100644 src/main/scala/millfork/language/MfLanguageClient.scala create mode 100644 src/main/scala/millfork/language/MfLanguageServer.scala diff --git a/.gitignore b/.gitignore index 081c513a..3e2098b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # various directories target/ +.bloop/ .idea/ +.metals/ project/target project/project/target/ stuff @@ -14,6 +16,7 @@ include-*/ # hidden files *.~ +.DS_Store #tools *.bat diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 00000000..ba14fb70 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1 @@ +version = "2.6.4" diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..33d99e8d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "scala", + "name": "Debug", + "request": "launch", + "mainClass": "millfork.Main", + // optional jvm properties to use + "jvmOptions": [], + "args": [] + }, + + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e72490fb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.watcherExclude": { + "**/target": true + } +} \ No newline at end of file diff --git a/build.sbt b/build.sbt index 891447eb..e7b8c6c5 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ name := "millfork" version := "0.3.23-SNAPSHOT" -scalaVersion := "2.12.11" +scalaVersion := "2.12.12" resolvers += Resolver.mavenLocal @@ -10,12 +10,14 @@ libraryDependencies += "com.lihaoyi" %% "fastparse" % "1.0.0" libraryDependencies += "org.apache.commons" % "commons-configuration2" % "2.2" -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test" +libraryDependencies += "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.9.0" -libraryDependencies += "com.codingrodent.microprocessor" % "Z80Processor" % "2.0.2" % "test" +// libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test" + +// libraryDependencies += "com.codingrodent.microprocessor" % "Z80Processor" % "2.0.2" % "test" // see: https://github.com/NeatMonster/Intel8086 -libraryDependencies += "NeatMonster" % "Intel8086" % "1.0" % "test" from "https://github.com/NeatMonster/Intel8086/raw/master/IBMPC.jar" +// libraryDependencies += "NeatMonster" % "Intel8086" % "1.0" % "test" from "https://github.com/NeatMonster/Intel8086/raw/master/IBMPC.jar" // these three are not in Maven Central or any other public repo // get them from the following links or just build millfork without tests: @@ -24,13 +26,13 @@ libraryDependencies += "NeatMonster" % "Intel8086" % "1.0" % "test" from "https: // https://github.com/trekawek/coffee-gb/tree/coffee-gb-1.0.0 // https://github.com/sorenroug/osnine-java/tree/b77349a6c314e1362e69b7158c385ac6f89b7ab8 -libraryDependencies += "com.loomcom.symon" % "symon" % "1.3.0-SNAPSHOT" % "test" +// libraryDependencies += "com.loomcom.symon" % "symon" % "1.3.0-SNAPSHOT" % "test" -libraryDependencies += "com.grapeshot" % "halfnes" % "061" % "test" +// libraryDependencies += "com.grapeshot" % "halfnes" % "061" % "test" -libraryDependencies += "eu.rekawek.coffeegb" % "coffee-gb" % "1.0.0" % "test" +// libraryDependencies += "eu.rekawek.coffeegb" % "coffee-gb" % "1.0.0" % "test" -libraryDependencies += "roug.org.osnine" % "osnine-core" % "2.0-SNAPSHOT" % "test" +// libraryDependencies += "roug.org.osnine" % "osnine-core" % "2.0-SNAPSHOT" % "test" libraryDependencies += "org.graalvm.sdk" % "graal-sdk" % "20.2.0" % "test" @@ -42,16 +44,17 @@ mainClass in Compile := Some("millfork.Main") assemblyJarName := "millfork.jar" -lazy val root = (project in file(".")). - enablePlugins(BuildInfoPlugin). - settings( +lazy val root = (project in file(".")) + .enablePlugins(BuildInfoPlugin) + .settings( buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion), buildInfoPackage := "millfork.buildinfo" ) import sbtassembly.AssemblyKeys -val releaseDist = TaskKey[File]("release-dist", "Creates a distributable zip file.") +val releaseDist = + TaskKey[File]("release-dist", "Creates a distributable zip file.") releaseDist := { val jar = AssemblyKeys.assembly.value @@ -68,7 +71,10 @@ releaseDist := { IO.createDirectory(distDir) IO.copyFile(jar, distDir / jar.name) IO.copyFile(base / "LICENSE", distDir / "LICENSE") - IO.copyFile(base / "src/3rd-party-licenses.txt", distDir / "3rd-party-licenses.txt") + IO.copyFile( + base / "src/3rd-party-licenses.txt", + distDir / "3rd-party-licenses.txt" + ) IO.copyFile(base / "CHANGELOG.md", distDir / "CHANGELOG.md") IO.copyFile(base / "README.md", distDir / "README.md") IO.copyFile(base / "COMPILING.md", distDir / "COMPILING.md") @@ -78,8 +84,14 @@ releaseDist := { } copyDir("include") copyDir("docs") - def entries(f: File): List[File] = f :: (if (f.isDirectory) IO.listFiles(f).toList.flatMap(entries) else Nil) - IO.zip(entries(distDir).map(d => (d, d.getAbsolutePath.substring(distDir.getParent.length + 1))), zipFile) + def entries(f: File): List[File] = + f :: (if (f.isDirectory) IO.listFiles(f).toList.flatMap(entries) else Nil) + IO.zip( + entries(distDir).map(d => + (d, d.getAbsolutePath.substring(distDir.getParent.length + 1)) + ), + zipFile + ) IO.delete(distDir) zipFile } diff --git a/project/build.properties b/project/build.properties index 5a1071cb..2ec6cd91 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 0.13.18 +sbt.version = 1.4.0 diff --git a/project/metals.sbt b/project/metals.sbt new file mode 100644 index 00000000..aa04ddb5 --- /dev/null +++ b/project/metals.sbt @@ -0,0 +1,4 @@ +// DO NOT EDIT! This file is auto-generated. +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.4-13-408f4d80") diff --git a/project/project/metals.sbt b/project/project/metals.sbt new file mode 100644 index 00000000..aa04ddb5 --- /dev/null +++ b/project/project/metals.sbt @@ -0,0 +1,4 @@ +// DO NOT EDIT! This file is auto-generated. +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.4-13-408f4d80") diff --git a/project/project/project/metals.sbt b/project/project/project/metals.sbt new file mode 100644 index 00000000..aa04ddb5 --- /dev/null +++ b/project/project/project/metals.sbt @@ -0,0 +1,4 @@ +// DO NOT EDIT! This file is auto-generated. +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.4-13-408f4d80") diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index bf2c0017..db6e8d89 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -19,6 +19,11 @@ import millfork.node.StandardCallGraph import millfork.output._ import millfork.parser.{MSourceLoadingQueue, MosSourceLoadingQueue, TextCodecRepository, ZSourceLoadingQueue} +import millfork.language.{MfLanguageServer,MfLanguageClient} +import org.eclipse.lsp4j.services.LanguageServer +import org.eclipse.lsp4j.jsonrpc.Launcher +import java.util.concurrent.Executors +import java.io.PrintWriter object Main { @@ -28,6 +33,8 @@ object Main { val errorReporting = new ConsoleLogger implicit val __implicitLogger: Logger = errorReporting + // Console.printf("Starting server") + if (args.isEmpty) { errorReporting.info("For help, use --help") } @@ -67,6 +74,23 @@ object Main { case (f, b) => errorReporting.debug(f" $f%-30s : $b%s") } + val server = new MfLanguageServer(c, options) + + val exec = Executors.newCachedThreadPool() + + val tracePrinter = new PrintWriter(new File("/users/adam/millforklsp.log")) + + val launcher = new Launcher.Builder[MfLanguageClient]() + .traceMessages(tracePrinter) + .setExecutorService(exec) + .setInput(System.in) + .setOutput(System.out) + .setRemoteInterface(classOf[MfLanguageClient]) + .setLocalService(server) + .create() + val clientProxy = launcher.getRemoteProxy + launcher.startListening().get() + val output = c.outputFileName match { case Some(ofn) => ofn case None => c.inputFileNames match { diff --git a/src/main/scala/millfork/language/MfLanguageClient.scala b/src/main/scala/millfork/language/MfLanguageClient.scala new file mode 100644 index 00000000..2f27749e --- /dev/null +++ b/src/main/scala/millfork/language/MfLanguageClient.scala @@ -0,0 +1,69 @@ +package millfork.language + +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.MessageType +import org.eclipse.lsp4j.MessageParams + +trait MfLanguageClient extends LanguageClient { + + /** + * Display message in the editor "status bar", which should be displayed somewhere alongside the buffer. + * + * The status bar should always be visible to the user. + * + * - VS Code: https://code.visualstudio.com/docs/extensionAPI/vscode-api#StatusBarItem + */ + // @JsonNotification("metals/status") + // def metalsStatus(params: MetalsStatusParams): Unit + + /** + * Starts a long running task with no estimate for how long it will take to complete. + * + * - request cancellation from the server indicates that the task has completed + * - response with cancel=true indicates the client wishes to cancel the slow task + */ + // @JsonRequest("metals/slowTask") + // def metalsSlowTask( + // params: MetalsSlowTaskParams + // ): CompletableFuture[MetalsSlowTaskResult] + + // @JsonNotification("metals/executeClientCommand") + // def metalsExecuteClientCommand(params: ExecuteCommandParams): Unit + + final def refreshModel(): Unit = { + // val command = ClientCommands.RefreshModel.id + // val params = new ExecuteCommandParams(command, Nil.asJava) + // metalsExecuteClientCommand(params) + } + + /** + * Opens an input box to ask the user for input. + * + * @return the user provided input. The future can be cancelled, meaning + * the input box should be dismissed in the editor. + */ + // @JsonRequest("metals/inputBox") + // def metalsInputBox( + // params: MetalsInputBoxParams + // ): CompletableFuture[MetalsInputBoxResult] + + /** + * Opens an menu to ask the user to pick one of the suggested options. + * + * @return the user provided pick. The future can be cancelled, meaning + * the input box should be dismissed in the editor. + */ + // @JsonRequest("metals/quickPick") + // def metalsQuickPick( + // params: MetalsQuickPickParams + // ): CompletableFuture[MetalsQuickPickResult] + + final def showMessage(messageType: MessageType, message: String): Unit = { + val params = new MessageParams(messageType, message) + showMessage(params) + } + + def shutdown(): Unit = {} + +} diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala new file mode 100644 index 00000000..f2656ad8 --- /dev/null +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -0,0 +1,123 @@ +package millfork.language +import millfork.CompilationOptions +import millfork.parser.MosSourceLoadingQueue +import millfork.Context + +import org.eclipse.lsp4j.services.{ + LanguageServer, + TextDocumentService, + WorkspaceService +} +import org.eclipse.lsp4j.{ + InitializeParams, + InitializeResult, + ServerCapabilities +} +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest + +import java.util.concurrent.CompletableFuture +import org.eclipse.lsp4j.TextDocumentPositionParams +import org.eclipse.lsp4j.Hover +import org.eclipse.lsp4j.jsonrpc.messages.Either +import java.{util => ju} +import org.eclipse.lsp4j.MarkedString +import org.eclipse.lsp4j.jsonrpc.services.JsonNotification +import org.eclipse.lsp4j.InitializedParams +import org.eclipse.lsp4j.MarkupContent +import millfork.node.FunctionDeclarationStatement +import millfork.node.ParameterDeclaration + +class MfLanguageServer(context: Context, options: CompilationOptions) { + @JsonRequest("initialize") + def initialize( + params: InitializeParams + ): CompletableFuture[ + InitializeResult + ] = + CompletableFuture.completedFuture { + val capabilities = new ServerCapabilities() + capabilities.setHoverProvider(true) + + // Console.printf("Initializing Millfork language server") + + new InitializeResult(capabilities) + } + // ] = { + // val completableFuture = new CompletableFuture[InitializeResult]() + + // val capabilities = new ServerCapabilities() + // capabilities.setHoverProvider(true) + + // Console.printf("Initializing Millfork language server") + + // completableFuture.complete(new InitializeResult(capabilities)) + // completableFuture + // } + + @JsonNotification("initialized") + def initialized(params: InitializedParams): CompletableFuture[Unit] = { + val completableFuture = new CompletableFuture[Unit]() + + // Console.printf("Millfork language server Initialization Finished") + + completableFuture.complete() + completableFuture + } + + // @JsonRequest("getTextDocumentService") + // def getTextDocumentService(): CompletableFuture[TextDocumentService] = { + // val completableFuture = new CompletableFuture[InitializeResult]() + // completableFuture.complete(new TextDocumentService()) + // completableFuture + // } + + // @JsonRequest("getWorkspaceService") + // def getWorkspaceService(): CompletableFuture[WorkspaceService] = ??? + + @JsonRequest("exit") + def exit(): CompletableFuture[Unit] = ??? + + @JsonRequest("shutdown") + def shutdown(): CompletableFuture[Object] = ??? + + @JsonRequest("textDocument/hover") + def textDocumentHover( + params: TextDocumentPositionParams + ): CompletableFuture[Hover] = + CompletableFuture.completedFuture { + val hoverPosition = params.getPosition() + + val unoptimized = new MosSourceLoadingQueue( + initialFilenames = + List(params.getTextDocument().getUri().stripPrefix("file:")), + includePath = context.includePath, + options = options + ).run() + + val declaration = unoptimized.declarations.find(s => { + if (s.position.isDefined) { + val position = s.position.get + position.line - 1 == hoverPosition.getLine() + // .getLine() && position.column == hoverPosition.getCharacter() + } else false + }) + + // Console.printf("Hovering") + if (declaration.isDefined) { + val declarationContent = declaration.get + var formattedHover = declaration.get.name + + if (declarationContent.isInstanceOf[FunctionDeclarationStatement]) { + val funcDeclaration: FunctionDeclarationStatement = + declarationContent.asInstanceOf[FunctionDeclarationStatement] + formattedHover += " Params: " + funcDeclaration.params + .map(p => p.typ) + .mkString + } + + new Hover( + new MarkupContent("plaintext", formattedHover) + ) + } else null + } +} From 8c57afe2a90d4e2b2e29fa9a1b22ae6bbf2669b6 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 9 Oct 2020 12:37:17 -0700 Subject: [PATCH 02/54] Node traversal attempting to find closest to cursor --- .../millfork/language/MfLanguageServer.scala | 71 +++++----- .../scala/millfork/language/NodeFinder.scala | 131 ++++++++++++++++++ 2 files changed, 165 insertions(+), 37 deletions(-) create mode 100644 src/main/scala/millfork/language/NodeFinder.scala diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index f2656ad8..4688bb6b 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -3,6 +3,13 @@ import millfork.CompilationOptions import millfork.parser.MosSourceLoadingQueue import millfork.Context +import millfork.node.{ + FunctionDeclarationStatement, + ParameterDeclaration, + Position, + Node +} + import org.eclipse.lsp4j.services.{ LanguageServer, TextDocumentService, @@ -24,8 +31,6 @@ import org.eclipse.lsp4j.MarkedString import org.eclipse.lsp4j.jsonrpc.services.JsonNotification import org.eclipse.lsp4j.InitializedParams import org.eclipse.lsp4j.MarkupContent -import millfork.node.FunctionDeclarationStatement -import millfork.node.ParameterDeclaration class MfLanguageServer(context: Context, options: CompilationOptions) { @JsonRequest("initialize") @@ -38,28 +43,13 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { val capabilities = new ServerCapabilities() capabilities.setHoverProvider(true) - // Console.printf("Initializing Millfork language server") - new InitializeResult(capabilities) } - // ] = { - // val completableFuture = new CompletableFuture[InitializeResult]() - - // val capabilities = new ServerCapabilities() - // capabilities.setHoverProvider(true) - - // Console.printf("Initializing Millfork language server") - - // completableFuture.complete(new InitializeResult(capabilities)) - // completableFuture - // } @JsonNotification("initialized") def initialized(params: InitializedParams): CompletableFuture[Unit] = { val completableFuture = new CompletableFuture[Unit]() - // Console.printf("Millfork language server Initialization Finished") - completableFuture.complete() completableFuture } @@ -87,25 +77,19 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { CompletableFuture.completedFuture { val hoverPosition = params.getPosition() - val unoptimized = new MosSourceLoadingQueue( - initialFilenames = - List(params.getTextDocument().getUri().stripPrefix("file:")), - includePath = context.includePath, - options = options - ).run() - - val declaration = unoptimized.declarations.find(s => { - if (s.position.isDefined) { - val position = s.position.get - position.line - 1 == hoverPosition.getLine() - // .getLine() && position.column == hoverPosition.getCharacter() - } else false - }) - - // Console.printf("Hovering") - if (declaration.isDefined) { - val declarationContent = declaration.get - var formattedHover = declaration.get.name + val statement = findExpressionAtPosition( + params.getTextDocument().getUri().stripPrefix("file:"), + new Position( + "", + params.getPosition().getLine() + 1, + params.getPosition().getCharacter(), + 0 + ) + ) + + if (statement.isDefined) { + val declarationContent = statement.get + var formattedHover = declarationContent.toString() if (declarationContent.isInstanceOf[FunctionDeclarationStatement]) { val funcDeclaration: FunctionDeclarationStatement = @@ -118,6 +102,19 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { new Hover( new MarkupContent("plaintext", formattedHover) ) - } else null + } else new Hover(new MarkupContent("plaintext", "No statement found")) } + + private def findExpressionAtPosition( + documentPath: String, + position: Position + ): Option[Node] = { + val unoptimizedProgram = new MosSourceLoadingQueue( + initialFilenames = List(documentPath), + includePath = context.includePath, + options = options + ).run() + + NodeFinder.findNodeAtPosition(unoptimizedProgram, position) + } } diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala new file mode 100644 index 00000000..54ac4ce7 --- /dev/null +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -0,0 +1,131 @@ +package millfork.language + +import millfork.node.{ + DeclarationStatement, + Expression, + FunctionCallExpression, + Node, + Program, + Position +} +import scala.collection.mutable + +object NodeFinder { + def findNodeAtPosition(program: Program, position: Position): Option[Node] = { + val line = position.line + val column = position.column + + val declarations = + findDeclarationsAtLine(program.declarations, line) + + if (declarations.isEmpty) { + return None + } + + if (lineOrNegOne(declarations.get.head.position) == line) { + // All declarations are current line, find matching node for column + findNodeAtColumn( + declarations.get.flatMap(d => List(d) ++ d.getAllExpressions), + line, + column + ) + } else { + // Declaration is a function or similar wrapper + // Find inner expressions + val matchingExpressions = + declarations.get.head.getAllExpressions.filter(e => + lineOrNegOne(e.position) == line + ) + + findNodeAtColumn(matchingExpressions, line, column) + } + } + + private def findDeclarationsAtLine( + declarations: List[DeclarationStatement], + line: Int + ): Option[List[DeclarationStatement]] = { + var lastDeclarations: Option[List[DeclarationStatement]] = None + + for ((nextDeclaration, i) <- declarations.view.zipWithIndex) { + if (lastDeclarations.isEmpty) { + // Populate with first item, no matter what + lastDeclarations = Option(List(nextDeclaration)) + } else { + val nextLine = lineOrNegOne(nextDeclaration.position) + + if (nextLine == line) { + // Declaration is on this line + // Check for additional declarations on this line + val newDeclarations = mutable.MutableList(nextDeclaration) + + for (declarationIndex <- i to declarations.length - 1) { + val checkDeclaration = declarations(declarationIndex) + + if (checkDeclaration.position.isDefined) { + if (checkDeclaration.position.get.line == line) { + newDeclarations += checkDeclaration + } else { + // Line doesn't match, done with this line + return Option(newDeclarations.toList) + } + } + } + + return Option(newDeclarations.toList) + } else if (nextLine < line) { + // Closer to desired line + lastDeclarations = Option(List(nextDeclaration)) + } + } + } + + lastDeclarations + } + + private def findNodeAtColumn( + nodes: List[Node], + line: Int, + column: Int + ): Option[Node] = { + var lastNode: Option[Node] = None + + // Only consider nodes on this line (if we're opening a declaration, it could span multiple lines) + for (nextNode <- nodes) if (lineOrNegOne(nextNode.position) == line) { + val innerExpressions = extractNestedExpressions(nextNode) + + if (innerExpressions.isDefined) { + for (innerExpression <- innerExpressions.get) { + if (colOrNegOne(innerExpression.position) < column) { + lastNode = Option(innerExpression) + } + } + } else if (colOrNegOne(nextNode.position) < column) { + lastNode = Option(nextNode) + } + } + + lastNode + } + + private def lineOrNegOne(position: Option[Position]): Int = + position match { + case Some(pos) => pos.line + case None => -1 + } + + private def colOrNegOne(position: Option[Position]): Int = + position match { + case Some(pos) => pos.column + case None => -1 + } + + private def extractNestedExpressions( + node: Node + ): Option[List[Expression]] = { + node match { + case FunctionCallExpression(_, expressions) => Option(expressions) + case default => None + } + } +} From 6976b00fc0bdf84e8862a36631adffd619bf56bc Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 9 Oct 2020 13:51:39 -0700 Subject: [PATCH 03/54] Added basic hover symbol formatting --- .../millfork/language/MfLanguageServer.scala | 29 ++-- .../millfork/language/NodeFormatter.scala | 147 ++++++++++++++++++ 2 files changed, 162 insertions(+), 14 deletions(-) create mode 100644 src/main/scala/millfork/language/NodeFormatter.scala diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 4688bb6b..3cd7aaef 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -89,20 +89,21 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { if (statement.isDefined) { val declarationContent = statement.get - var formattedHover = declarationContent.toString() - - if (declarationContent.isInstanceOf[FunctionDeclarationStatement]) { - val funcDeclaration: FunctionDeclarationStatement = - declarationContent.asInstanceOf[FunctionDeclarationStatement] - formattedHover += " Params: " + funcDeclaration.params - .map(p => p.typ) - .mkString - } - - new Hover( - new MarkupContent("plaintext", formattedHover) - ) - } else new Hover(new MarkupContent("plaintext", "No statement found")) + val formatting = NodeFormatter.symbol(declarationContent) + + if (formatting.isDefined) + new Hover( + new MarkupContent( + "markdown", + NodeFormatter.hover( + "", + formatting.get, + "" + ) + ) + ) + else null + } else null } private def findExpressionAtPosition( diff --git a/src/main/scala/millfork/language/NodeFormatter.scala b/src/main/scala/millfork/language/NodeFormatter.scala new file mode 100644 index 00000000..02934d55 --- /dev/null +++ b/src/main/scala/millfork/language/NodeFormatter.scala @@ -0,0 +1,147 @@ +package millfork.language + +import millfork.node.Node +import millfork.node.DeclarationStatement +import millfork.node.FunctionDeclarationStatement +import millfork.node.ParameterDeclaration +import millfork.node.Expression +import millfork.node.VariableExpression +import millfork.node.VariableDeclarationStatement +import millfork.node.LiteralExpression +import millfork.env.ParamPassingConvention +import millfork.env.ByConstant +import millfork.env.ByVariable +import millfork.env.ByReference + +object NodeFormatter { + // TODO: Remove Option + def symbol(node: Node): Option[String] = + node match { + case statement: DeclarationStatement => + statement match { + case functionStatement: FunctionDeclarationStatement => { + val builder = new StringBuilder() + + if (functionStatement.constPure) { + builder.append("const ") + } + + if (functionStatement.interrupt) { + builder.append("interrupt ") + } + + if (functionStatement.kernalInterrupt) { + builder.append("kernal_interrupt ") + } + + if (functionStatement.assembly) { + builder.append("asm ") + } + + // Cannot have both "macro" and "inline" + if (functionStatement.isMacro) { + builder.append("macro ") + } else if ( + functionStatement.inlinable.isDefined && functionStatement.inlinable.get + ) { + builder.append("inline ") + } + + builder.append( + s"""${functionStatement.resultType} ${functionStatement.name}(${functionStatement.params + .map(symbol) + .filter(n => n.isDefined) + .map(n => n.get) + .mkString(", ")})""" + ) + + Some(builder.toString()) + } + case variableStatement: VariableDeclarationStatement => { + val builder = new StringBuilder() + + if (variableStatement.constant) { + builder.append("const ") + } + + if (variableStatement.volatile) { + builder.append("volatile ") + } + + builder.append( + s"""${variableStatement.typ} ${variableStatement.name}""" + ) + + if (variableStatement.initialValue.isDefined) { + val formattedInitialValue = symbol( + variableStatement.initialValue.get + ) + + if (formattedInitialValue.isDefined) { + builder.append(s""" = ${formattedInitialValue.get}""") + } + } + + Some(builder.toString()) + } + case default => None + } + case ParameterDeclaration(typ, assemblyParamPassingConvention) => + Some(s"""${typ} ${symbol(assemblyParamPassingConvention)}""") + case expression: Expression => + expression match { + case LiteralExpression(value, _) => Some(s"""${value}""") + case VariableExpression(name) => Some(s"""${name}""") + case default => None + } + case default => None + } + + def symbol(paramConvention: ParamPassingConvention): String = + paramConvention match { + case ByConstant(name) => name + case ByVariable(name) => name + case ByReference(name) => name + // TODO: Remove default + case default => "" + } + + /** + * TODO: This function is nearly the same as https://github.com/scalameta/metals/blob/main/mtags/src/main/scala/scala/meta/internal/pc/HoverMarkup.scala + * It is unclear what "Expression type" and "Symbol signature" do + * + * Render the textDocument/hover result into markdown. + * + * @param expressionType The type of the expression over the cursor, for example "List[Int]". + * @param symbolSignature The signature of the symbol over the cursor, for example + * "def map[B](fn: A => B): Option[B]" + * @param docstring The Scaladoc/Javadoc string for the symbol. + */ + def hover( + expressionType: String, + symbolSignature: String, + docstring: String + ): String = { + val markdown = new StringBuilder() + val needsExpressionType = !symbolSignature.endsWith(expressionType) + if (needsExpressionType) { + markdown + .append("**Expression type**:\n") + .append("```mfk\n") + .append(expressionType) + .append("\n```\n") + } + if (symbolSignature.nonEmpty) { + markdown + .append(if (needsExpressionType) "**Symbol signature**:\n" else "") + .append("```mfk\n") + .append(symbolSignature) + .append("\n```") + } + if (docstring.nonEmpty) + markdown + .append("\n") + .append(docstring) + markdown.toString() + } +} From 34576abc411b5fecd71f23224ef4fbf1bc0cf93b Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 9 Oct 2020 18:35:49 -0700 Subject: [PATCH 04/54] Add declaration finding --- include/platform/nes_small_4screen.ini | 42 ++++++ src/main/scala/millfork/Main.scala | 8 +- .../millfork/language/MfLanguageServer.scala | 19 ++- .../scala/millfork/language/NodeFinder.scala | 123 ++++++++++++++---- .../millfork/language/NodeFormatter.scala | 14 +- .../parser/AbstractSourceLoadingQueue.scala | 12 +- 6 files changed, 171 insertions(+), 47 deletions(-) create mode 100644 include/platform/nes_small_4screen.ini diff --git a/include/platform/nes_small_4screen.ini b/include/platform/nes_small_4screen.ini new file mode 100644 index 00000000..8683f0f3 --- /dev/null +++ b/include/platform/nes_small_4screen.ini @@ -0,0 +1,42 @@ +; a very simple NES cartridge format +; uses mapper 0 and no bankswitching, so it's only good for very simple games +; assumes CHRROM is at chrrom:$0000-$1fff and PRGROM is at prgrom:$8000-$ffff +; uses horizontal mirroring; to use vertical mirroring, change byte #6 of the header from 0 to 1 +; output file size: 40976 bytes + +[compilation] +arch=ricoh +modules=nes_hardware,nes_routines,default_panic,stdlib + +[allocation] +zp_bytes=all + +segments=default,prgrom,chrrom +default_code_segment=prgrom +ram_init_segment=prgrom + +segment_default_start=$200 +segment_default_end=$7ff +segment_default_bank=$ff + +segment_prgrom_start=$8000 +segment_prgrom_end=$ffff + +segment_chrrom_start=$0000 +segment_chrrom_end=$3fff + +[define] +NES=1 +WIDESCREEN=0 +KEYBOARD=0 +JOYSTICKS=2 +HAS_BITMAP_MODE=0 + +[output] +style=single +; Byte 5 sets bit 3 to disable mirroring control +format=$4E,$45,$53,$1A, 2,1,8,0, 0,0,0,0, 0,0,0,0, prgrom:$8000:$ffff, chrrom:$0000:$1fff +extension=nes +labels=nesasm + + diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index db6e8d89..90d35d2e 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -276,7 +276,7 @@ object Main { val unoptimized = new MosSourceLoadingQueue( initialFilenames = c.inputFileNames, includePath = c.includePath, - options = options).run() + options = options).run().compilationOrderProgram val program = if (optLevel > 0) { OptimizationPresets.NodeOpt.foldLeft(unoptimized)((p, opt) => p.applyNodeOptimization(opt, options)) @@ -330,7 +330,7 @@ object Main { val unoptimized = new ZSourceLoadingQueue( initialFilenames = c.inputFileNames, includePath = c.includePath, - options = options).run() + options = options).run().compilationOrderProgram val program = if (optLevel > 0) { OptimizationPresets.NodeOpt.foldLeft(unoptimized)((p, opt) => p.applyNodeOptimization(opt, options)) @@ -370,7 +370,7 @@ object Main { val unoptimized = new MSourceLoadingQueue( initialFilenames = c.inputFileNames, includePath = c.includePath, - options = options).run() + options = options).run().compilationOrderProgram val program = if (optLevel > 0) { OptimizationPresets.NodeOpt.foldLeft(unoptimized)((p, opt) => p.applyNodeOptimization(opt, options)) @@ -400,7 +400,7 @@ object Main { val unoptimized = new ZSourceLoadingQueue( initialFilenames = c.inputFileNames, includePath = c.includePath, - options = options).run() + options = options).run().compilationOrderProgram val program = if (optLevel > 0) { OptimizationPresets.NodeOpt.foldLeft(unoptimized)((p, opt) => p.applyNodeOptimization(opt, options)) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 3cd7aaef..859c54d1 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -96,7 +96,6 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { new MarkupContent( "markdown", NodeFormatter.hover( - "", formatting.get, "" ) @@ -110,12 +109,24 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { documentPath: String, position: Position ): Option[Node] = { - val unoptimizedProgram = new MosSourceLoadingQueue( + val queue = new MosSourceLoadingQueue( initialFilenames = List(documentPath), includePath = context.includePath, options = options - ).run() + ) + val unoptimizedProgram = queue.run() - NodeFinder.findNodeAtPosition(unoptimizedProgram, position) + val node = NodeFinder.findNodeAtPosition( + queue.extractName(documentPath), + unoptimizedProgram, + position + ) + + if (node.isDefined) { + val usage = + NodeFinder.findDeclarationForUsage(unoptimizedProgram, node.get) + + if (usage.isDefined) usage else node + } else None } } diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 54ac4ce7..68eae276 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -4,19 +4,65 @@ import millfork.node.{ DeclarationStatement, Expression, FunctionCallExpression, + FunctionDeclarationStatement, Node, Program, - Position + Position, + VariableDeclarationStatement, + VariableExpression } +import millfork.parser.ParsedProgram + import scala.collection.mutable +import millfork.node.ExpressionStatement object NodeFinder { - def findNodeAtPosition(program: Program, position: Position): Option[Node] = { + def findDeclarationForUsage( + program: ParsedProgram, + node: Node + ): Option[DeclarationStatement] = { + node match { + case expression: Expression => + matchingDeclarationForExpression( + expression, + program.compilationOrderProgram.declarations + ) + case default => None + } + } + + private def matchingDeclarationForExpression( + expression: Expression, + declarations: List[DeclarationStatement] + ): Option[DeclarationStatement] = + expression match { + case FunctionCallExpression(name, expressions) => + declarations + .filter(d => d.isInstanceOf[FunctionDeclarationStatement]) + .find(d => d.name == name) + case VariableExpression(name) => + declarations + .filter(d => d.isInstanceOf[VariableDeclarationStatement]) + .find(d => d.name == name) + case default => None + } + + def findNodeAtPosition( + module: String, + program: ParsedProgram, + position: Position + ): Option[Node] = { val line = position.line val column = position.column + val activeProgram = program.parsedModules.get(module) + + if (activeProgram.isEmpty) { + return None + } + val declarations = - findDeclarationsAtLine(program.declarations, line) + findDeclarationsAtLine(activeProgram.get.declarations, line) if (declarations.isEmpty) { return None @@ -32,12 +78,11 @@ object NodeFinder { } else { // Declaration is a function or similar wrapper // Find inner expressions - val matchingExpressions = - declarations.get.head.getAllExpressions.filter(e => - lineOrNegOne(e.position) == line - ) + if (declarations.get.length > 1) { + throw new Exception("Unexpected number of declarations") + } - findNodeAtColumn(matchingExpressions, line, column) + findNodeAtColumn(declarations.get.head.getAllExpressions, line, column) } } @@ -89,21 +134,27 @@ object NodeFinder { column: Int ): Option[Node] = { var lastNode: Option[Node] = None + var lastPosition: Option[Position] = None // Only consider nodes on this line (if we're opening a declaration, it could span multiple lines) - for (nextNode <- nodes) if (lineOrNegOne(nextNode.position) == line) { - val innerExpressions = extractNestedExpressions(nextNode) + var flattenedNodes = nodes.flatMap(flattenNestedExpressions) - if (innerExpressions.isDefined) { - for (innerExpression <- innerExpressions.get) { - if (colOrNegOne(innerExpression.position) < column) { - lastNode = Option(innerExpression) - } + for (nextNode <- flattenedNodes) + if (lineOrNegOne(nextNode.position) == line) { + if (nextNode.position.isEmpty) { + throw new Error("Missing position for node " + nextNode.toString()) + } + + if ( + colOrNegOne(nextNode.position) < column && colOrNegOne( + nextNode.position + // Allow equality, because later nodes are of higher specificity + ) >= colOrNegOne(lastPosition) + ) { + lastNode = Some(nextNode) + lastPosition = nextNode.position } - } else if (colOrNegOne(nextNode.position) < column) { - lastNode = Option(nextNode) } - } lastNode } @@ -120,12 +171,36 @@ object NodeFinder { case None => -1 } - private def extractNestedExpressions( - node: Node - ): Option[List[Expression]] = { + private def flattenNestedExpressions(node: Node): List[Node] = node match { - case FunctionCallExpression(_, expressions) => Option(expressions) - case default => None + case statement: ExpressionStatement => { + val innerExpressions = flattenNestedExpressions( + statement.expression + ) + + List(statement.expression) ++ innerExpressions + } + case functionExpression: FunctionCallExpression => + List(functionExpression) ++ + functionExpression.expressions + .flatMap(flattenNestedExpressions) + case default => List(default) } - } + + private def sortNodes(nodes: List[Node]) = + nodes.sortWith((a, b) => { + if (a.position.isEmpty && b.position.isEmpty) { + false + } else if (a.position.isEmpty) { + true + } else if (b.position.isEmpty) { + false + } else { + val aPos = a.position.get + val bPos = b.position.get + + // aPos.line < bPos.line && aPos.column < bPos.column + aPos.column < bPos.column + } + }) } diff --git a/src/main/scala/millfork/language/NodeFormatter.scala b/src/main/scala/millfork/language/NodeFormatter.scala index 02934d55..da264a1d 100644 --- a/src/main/scala/millfork/language/NodeFormatter.scala +++ b/src/main/scala/millfork/language/NodeFormatter.scala @@ -108,32 +108,20 @@ object NodeFormatter { /** * TODO: This function is nearly the same as https://github.com/scalameta/metals/blob/main/mtags/src/main/scala/scala/meta/internal/pc/HoverMarkup.scala - * It is unclear what "Expression type" and "Symbol signature" do * * Render the textDocument/hover result into markdown. * - * @param expressionType The type of the expression over the cursor, for example "List[Int]". * @param symbolSignature The signature of the symbol over the cursor, for example * "def map[B](fn: A => B): Option[B]" - * @param docstring The Scaladoc/Javadoc string for the symbol. + * @param docstring The Markdown documentation string for the symbol. */ def hover( - expressionType: String, symbolSignature: String, docstring: String ): String = { val markdown = new StringBuilder() - val needsExpressionType = !symbolSignature.endsWith(expressionType) - if (needsExpressionType) { - markdown - .append("**Expression type**:\n") - .append("```mfk\n") - .append(expressionType) - .append("\n```\n") - } if (symbolSignature.nonEmpty) { markdown - .append(if (needsExpressionType) "**Symbol signature**:\n" else "") .append("```mfk\n") .append(symbolSignature) .append("\n```") diff --git a/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala b/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala index 66c1b0bf..8baccafe 100644 --- a/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala +++ b/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala @@ -10,6 +10,8 @@ import millfork.node.{AliasDefinitionStatement, DeclarationStatement, ImportStat import scala.collection.mutable import scala.collection.convert.ImplicitConversionsToScala._ +case class ParsedProgram(compilationOrderProgram: Program, parsedModules: Map[String, Program]) + abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], val includePath: List[String], val options: CompilationOptions) { @@ -41,7 +43,12 @@ abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], encodingConversionAliases } - def run(): Program = { + /** + * Tokenizes and parses the configured source file and modules + * + * @return A ParsedProgram containing an ordered set of statements in order of compilation dependencies, and each individual parsed module + */ + def run(): ParsedProgram = { for { initialFilename <- initialFilenames startingModule <- options.platform.startingModules @@ -76,7 +83,8 @@ abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], options.log.assertNoErrors("Parse failed") val compilationOrder = Tarjan.sort(parsedModules.keys, moduleDependecies) options.log.debug("Compilation order: " + compilationOrder.mkString(", ")) - compilationOrder.filter(parsedModules.contains).map(parsedModules).reduce(_ + _).applyImportantAliases + + ParsedProgram(compilationOrder.filter(parsedModules.contains).map(parsedModules).reduce(_ + _).applyImportantAliases, parsedModules.toMap) } def lookupModuleFile(includePath: List[String], moduleName: String, position: Option[Position]): String = { From 86198abb28d95dbba46efa48aa49b66cb7f20687 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 9 Oct 2020 19:13:36 -0700 Subject: [PATCH 05/54] Go to definition with weird positioning issues --- .../millfork/language/MfLanguageServer.scala | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 859c54d1..c76114bd 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -18,7 +18,8 @@ import org.eclipse.lsp4j.services.{ import org.eclipse.lsp4j.{ InitializeParams, InitializeResult, - ServerCapabilities + ServerCapabilities, + Range } import org.eclipse.lsp4j.jsonrpc.services.JsonRequest @@ -31,6 +32,8 @@ import org.eclipse.lsp4j.MarkedString import org.eclipse.lsp4j.jsonrpc.services.JsonNotification import org.eclipse.lsp4j.InitializedParams import org.eclipse.lsp4j.MarkupContent +import org.eclipse.lsp4j.DefinitionParams +import org.eclipse.lsp4j.Location class MfLanguageServer(context: Context, options: CompilationOptions) { @JsonRequest("initialize") @@ -42,6 +45,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { CompletableFuture.completedFuture { val capabilities = new ServerCapabilities() capabilities.setHoverProvider(true) + capabilities.setDefinitionProvider(true) new InitializeResult(capabilities) } @@ -70,6 +74,39 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { @JsonRequest("shutdown") def shutdown(): CompletableFuture[Object] = ??? + @JsonRequest("textDocument/definition") + def textDocumentDefinition( + params: DefinitionParams + ): CompletableFuture[Location] = + CompletableFuture.completedFuture { + val activePosition = params.getPosition() + + val statement = findExpressionAtPosition( + params.getTextDocument().getUri().stripPrefix("file:"), + new Position( + "", + activePosition.getLine() + 1, + activePosition.getCharacter(), + 0 + ) + ) + + if (statement.isDefined) { + val declarationContent = statement.get + val formatting = NodeFormatter.symbol(declarationContent) + + if (declarationContent.position.isDefined) + new Location( + params.getTextDocument().getUri(), + new Range( + mfPositionToLSP4j(declarationContent.position.get), + mfPositionToLSP4j(declarationContent.position.get) + ) + ) + else null + } else null + } + @JsonRequest("textDocument/hover") def textDocumentHover( params: TextDocumentPositionParams @@ -81,8 +118,8 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { params.getTextDocument().getUri().stripPrefix("file:"), new Position( "", - params.getPosition().getLine() + 1, - params.getPosition().getCharacter(), + hoverPosition.getLine() + 1, + hoverPosition.getCharacter(), 0 ) ) @@ -129,4 +166,9 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { if (usage.isDefined) usage else node } else None } + + private def mfPositionToLSP4j( + position: Position + ): org.eclipse.lsp4j.Position = + new org.eclipse.lsp4j.Position(position.line, position.column) } From 6c8a490347b5b6a2a7805fb22055297f48c88153 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 10 Oct 2020 08:22:13 -0700 Subject: [PATCH 06/54] Fixed column indexes being off --- src/main/scala/millfork/language/MfLanguageServer.scala | 9 +++++---- src/main/scala/millfork/language/NodeFinder.scala | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index c76114bd..60ad8332 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -83,10 +83,10 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { val statement = findExpressionAtPosition( params.getTextDocument().getUri().stripPrefix("file:"), - new Position( + Position( "", activePosition.getLine() + 1, - activePosition.getCharacter(), + activePosition.getCharacter() + 2, 0 ) ) @@ -116,10 +116,11 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { val statement = findExpressionAtPosition( params.getTextDocument().getUri().stripPrefix("file:"), - new Position( + Position( "", + // Millfork positions start at 1,2, rather than 0,0, so add to each coord hoverPosition.getLine() + 1, - hoverPosition.getCharacter(), + hoverPosition.getCharacter() + 2, 0 ) ) diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 68eae276..b04ce1b3 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -95,7 +95,7 @@ object NodeFinder { for ((nextDeclaration, i) <- declarations.view.zipWithIndex) { if (lastDeclarations.isEmpty) { // Populate with first item, no matter what - lastDeclarations = Option(List(nextDeclaration)) + lastDeclarations = Some(List(nextDeclaration)) } else { val nextLine = lineOrNegOne(nextDeclaration.position) @@ -112,15 +112,15 @@ object NodeFinder { newDeclarations += checkDeclaration } else { // Line doesn't match, done with this line - return Option(newDeclarations.toList) + return Some(newDeclarations.toList) } } } - return Option(newDeclarations.toList) + return Some(newDeclarations.toList) } else if (nextLine < line) { // Closer to desired line - lastDeclarations = Option(List(nextDeclaration)) + lastDeclarations = Some(List(nextDeclaration)) } } } From bb641feaf9694d6560f3eb84930e5f16be7fc848 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 10 Oct 2020 08:40:01 -0700 Subject: [PATCH 07/54] Proper positioning for go to def --- src/main/scala/millfork/language/MfLanguageServer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 60ad8332..21a6477a 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -171,5 +171,5 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { private def mfPositionToLSP4j( position: Position ): org.eclipse.lsp4j.Position = - new org.eclipse.lsp4j.Position(position.line, position.column) + new org.eclipse.lsp4j.Position(position.line - 1, position.column - 1) } From 0afbf4d691ccb9c50c37c4360ba687a7ea8d46c7 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 10 Oct 2020 09:02:24 -0700 Subject: [PATCH 08/54] Logger stub --- src/main/scala/millfork/Main.scala | 4 +-- .../language/LanguageServerLogger.scala | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/millfork/language/LanguageServerLogger.scala diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index 90d35d2e..bc7bb7e3 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -19,7 +19,7 @@ import millfork.node.StandardCallGraph import millfork.output._ import millfork.parser.{MSourceLoadingQueue, MosSourceLoadingQueue, TextCodecRepository, ZSourceLoadingQueue} -import millfork.language.{MfLanguageServer,MfLanguageClient} +import millfork.language.{MfLanguageServer,MfLanguageClient,LanguageServerLogger} import org.eclipse.lsp4j.services.LanguageServer import org.eclipse.lsp4j.jsonrpc.Launcher import java.util.concurrent.Executors @@ -68,7 +68,7 @@ object Main { errorReporting.info("No platform selected, defaulting to `c64`") "c64" }, textCodecRepository) - val options = CompilationOptions(platform, c.flags, c.outputFileName, c.zpRegisterSize.getOrElse(platform.zpRegisterSize), c.features, textCodecRepository, JobContext(errorReporting, new LabelGenerator)) + val options = CompilationOptions(platform, c.flags, c.outputFileName, c.zpRegisterSize.getOrElse(platform.zpRegisterSize), c.features, textCodecRepository, JobContext(new LanguageServerLogger(), new LabelGenerator)) errorReporting.debug("Effective flags: ") options.flags.toSeq.sortBy(_._1).foreach{ case (f, b) => errorReporting.debug(f" $f%-30s : $b%s") diff --git a/src/main/scala/millfork/language/LanguageServerLogger.scala b/src/main/scala/millfork/language/LanguageServerLogger.scala new file mode 100644 index 00000000..8255a379 --- /dev/null +++ b/src/main/scala/millfork/language/LanguageServerLogger.scala @@ -0,0 +1,35 @@ +package millfork.language + +import millfork.error.Logger +import millfork.node.Position +import millfork.assembly.SourceLine + +class LanguageServerLogger extends Logger { + // TODO: Complete stub to send diagnostics to client + override def setFatalWarnings(fatalWarnings: Boolean): Unit = {} + + override def info(msg: String, position: Option[Position]): Unit = {} + + override def debug(msg: String, position: Option[Position]): Unit = {} + + override def trace(msg: String, position: Option[Position]): Unit = {} + + override def traceEnabled: Boolean = false + + override def debugEnabled: Boolean = false + + override def warn(msg: String, position: Option[Position]): Unit = {} + + override def error(msg: String, position: Option[Position]): Unit = {} + + override def fatal(msg: String, position: Option[Position]): Nothing = ??? + + override def fatalQuit(msg: String, position: Option[Position]): Nothing = ??? + + override def assertNoErrors(msg: String): Unit = {} + + override def addSource(filename: String, lines: IndexedSeq[String]): Unit = {} + + override def getLine(line: SourceLine): Option[String] = None + +} From cc3d58a8f2d80248bb95ca7cef6d111a2774b668 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 23 Oct 2020 19:40:31 -0700 Subject: [PATCH 09/54] Removed accidentally committed config changes --- build.sbt | 16 +++++----- include/platform/nes_small_4screen.ini | 42 -------------------------- 2 files changed, 8 insertions(+), 50 deletions(-) delete mode 100644 include/platform/nes_small_4screen.ini diff --git a/build.sbt b/build.sbt index e7b8c6c5..0ca08dbd 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ name := "millfork" version := "0.3.23-SNAPSHOT" -scalaVersion := "2.12.12" +scalaVersion := "2.12.11" resolvers += Resolver.mavenLocal @@ -12,12 +12,12 @@ libraryDependencies += "org.apache.commons" % "commons-configuration2" % "2.2" libraryDependencies += "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.9.0" -// libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test" +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test" -// libraryDependencies += "com.codingrodent.microprocessor" % "Z80Processor" % "2.0.2" % "test" +libraryDependencies += "com.codingrodent.microprocessor" % "Z80Processor" % "2.0.2" % "test" // see: https://github.com/NeatMonster/Intel8086 -// libraryDependencies += "NeatMonster" % "Intel8086" % "1.0" % "test" from "https://github.com/NeatMonster/Intel8086/raw/master/IBMPC.jar" +libraryDependencies += "NeatMonster" % "Intel8086" % "1.0" % "test" from "https://github.com/NeatMonster/Intel8086/raw/master/IBMPC.jar" // these three are not in Maven Central or any other public repo // get them from the following links or just build millfork without tests: @@ -26,13 +26,13 @@ libraryDependencies += "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.9.0" // https://github.com/trekawek/coffee-gb/tree/coffee-gb-1.0.0 // https://github.com/sorenroug/osnine-java/tree/b77349a6c314e1362e69b7158c385ac6f89b7ab8 -// libraryDependencies += "com.loomcom.symon" % "symon" % "1.3.0-SNAPSHOT" % "test" +libraryDependencies += "com.loomcom.symon" % "symon" % "1.3.0-SNAPSHOT" % "test" -// libraryDependencies += "com.grapeshot" % "halfnes" % "061" % "test" +libraryDependencies += "com.grapeshot" % "halfnes" % "061" % "test" -// libraryDependencies += "eu.rekawek.coffeegb" % "coffee-gb" % "1.0.0" % "test" +libraryDependencies += "eu.rekawek.coffeegb" % "coffee-gb" % "1.0.0" % "test" -// libraryDependencies += "roug.org.osnine" % "osnine-core" % "2.0-SNAPSHOT" % "test" +libraryDependencies += "roug.org.osnine" % "osnine-core" % "2.0-SNAPSHOT" % "test" libraryDependencies += "org.graalvm.sdk" % "graal-sdk" % "20.2.0" % "test" diff --git a/include/platform/nes_small_4screen.ini b/include/platform/nes_small_4screen.ini deleted file mode 100644 index 8683f0f3..00000000 --- a/include/platform/nes_small_4screen.ini +++ /dev/null @@ -1,42 +0,0 @@ -; a very simple NES cartridge format -; uses mapper 0 and no bankswitching, so it's only good for very simple games -; assumes CHRROM is at chrrom:$0000-$1fff and PRGROM is at prgrom:$8000-$ffff -; uses horizontal mirroring; to use vertical mirroring, change byte #6 of the header from 0 to 1 -; output file size: 40976 bytes - -[compilation] -arch=ricoh -modules=nes_hardware,nes_routines,default_panic,stdlib - -[allocation] -zp_bytes=all - -segments=default,prgrom,chrrom -default_code_segment=prgrom -ram_init_segment=prgrom - -segment_default_start=$200 -segment_default_end=$7ff -segment_default_bank=$ff - -segment_prgrom_start=$8000 -segment_prgrom_end=$ffff - -segment_chrrom_start=$0000 -segment_chrrom_end=$3fff - -[define] -NES=1 -WIDESCREEN=0 -KEYBOARD=0 -JOYSTICKS=2 -HAS_BITMAP_MODE=0 - -[output] -style=single -; Byte 5 sets bit 3 to disable mirroring control -format=$4E,$45,$53,$1A, 2,1,8,0, 0,0,0,0, 0,0,0,0, prgrom:$8000:$ffff, chrrom:$0000:$1fff -extension=nes -labels=nesasm - - From 3b76bb9cc899e748e02b189a836bb49302c2cee5 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 23 Oct 2020 20:06:02 -0700 Subject: [PATCH 10/54] Add -lsp flag to launch language server --- src/main/scala/millfork/Context.scala | 3 ++- src/main/scala/millfork/Main.scala | 35 ++++++++++++++++----------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/main/scala/millfork/Context.scala b/src/main/scala/millfork/Context.scala index 18a637f6..bb62e96e 100644 --- a/src/main/scala/millfork/Context.scala +++ b/src/main/scala/millfork/Context.scala @@ -21,7 +21,8 @@ case class Context(errorReporting: Logger, extraIncludePath: Seq[String] = IndexedSeq(), flags: Map[CompilationFlag.Value, Boolean] = Map(), features: Map[String, Long] = Map(), - verbosity: Option[Int] = None) { + verbosity: Option[Int] = None, + languageServer: Boolean = false) { def changeFlag(f: CompilationFlag.Value, b: Boolean): Context = { if (flags.contains(f)) { if (flags(f) != b) { diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index bc7bb7e3..968aa818 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -49,7 +49,7 @@ object Main { } errorReporting.assertNoErrors("Invalid command line") errorReporting.verbosity = c0.verbosity.getOrElse(0) - if (c0.inputFileNames.isEmpty) { + if (c0.inputFileNames.isEmpty && !c0.languageServer) { errorReporting.fatalQuit("No input files") } @@ -74,22 +74,25 @@ object Main { case (f, b) => errorReporting.debug(f" $f%-30s : $b%s") } - val server = new MfLanguageServer(c, options) + if (c0.languageServer) { + errorReporting.info("Starting Millfork language server") - val exec = Executors.newCachedThreadPool() + val server = new MfLanguageServer(c, options) - val tracePrinter = new PrintWriter(new File("/users/adam/millforklsp.log")) + val exec = Executors.newCachedThreadPool() + val tracePrinter = new PrintWriter(new File("/users/adam/millforklsp.log")) - val launcher = new Launcher.Builder[MfLanguageClient]() - .traceMessages(tracePrinter) - .setExecutorService(exec) - .setInput(System.in) - .setOutput(System.out) - .setRemoteInterface(classOf[MfLanguageClient]) - .setLocalService(server) - .create() - val clientProxy = launcher.getRemoteProxy - launcher.startListening().get() + val launcher = new Launcher.Builder[MfLanguageClient]() + .traceMessages(tracePrinter) + .setExecutorService(exec) + .setInput(System.in) + .setOutput(System.out) + .setRemoteInterface(classOf[MfLanguageClient]) + .setLocalService(server) + .create() + val clientProxy = launcher.getRemoteProxy + launcher.startListening().get() + } val output = c.outputFileName match { case Some(ofn) => ofn @@ -453,6 +456,10 @@ object Main { c.copy(outputLabels = true, outputLabelsFormatOverride = Some(f)) }.description("Generate also the label file in the given format. Available options: vice, nesasm, sym.") + flag("-lsp").repeatable().action { c => + c.copy(languageServer = true) + }.description("Start the Millfork language server. Does not start compilation") + boolean("-fbreakpoints", "-fno-breakpoints").action((c,v) => c.changeFlag(CompilationFlag.EnableBreakpoints, v) ).description("Include breakpoints in the label file. Requires either -g or -G.") From a7c9594d75ffda18b5175d50b65b754923181df7 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 23 Oct 2020 21:13:22 -0700 Subject: [PATCH 11/54] Basic AST cache --- src/main/scala/millfork/Main.scala | 5 +-- .../millfork/language/MfLanguageServer.scala | 37 ++++++++++++++++--- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index 968aa818..ea10ccd5 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -65,7 +65,7 @@ object Main { val textCodecRepository = new TextCodecRepository("." :: c.includePath) val platform = Platform.lookupPlatformFile("." :: c.includePath, c.platform.getOrElse { - errorReporting.info("No platform selected, defaulting to `c64`") + if (!c0.languageServer) errorReporting.info("No platform selected, defaulting to `c64`") "c64" }, textCodecRepository) val options = CompilationOptions(platform, c.flags, c.outputFileName, c.zpRegisterSize.getOrElse(platform.zpRegisterSize), c.features, textCodecRepository, JobContext(new LanguageServerLogger(), new LabelGenerator)) @@ -75,8 +75,7 @@ object Main { } if (c0.languageServer) { - errorReporting.info("Starting Millfork language server") - + // We cannot log anything to stdout when starting the language server (otherwise it's a protocol violation) val server = new MfLanguageServer(c, options) val exec = Executors.newCachedThreadPool() diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 21a6477a..68e984ef 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -2,6 +2,7 @@ package millfork.language import millfork.CompilationOptions import millfork.parser.MosSourceLoadingQueue import millfork.Context +import millfork.parser.ParsedProgram import millfork.node.{ FunctionDeclarationStatement, @@ -28,6 +29,7 @@ import org.eclipse.lsp4j.TextDocumentPositionParams import org.eclipse.lsp4j.Hover import org.eclipse.lsp4j.jsonrpc.messages.Either import java.{util => ju} +import scala.collection.mutable import org.eclipse.lsp4j.MarkedString import org.eclipse.lsp4j.jsonrpc.services.JsonNotification import org.eclipse.lsp4j.InitializedParams @@ -36,6 +38,9 @@ import org.eclipse.lsp4j.DefinitionParams import org.eclipse.lsp4j.Location class MfLanguageServer(context: Context, options: CompilationOptions) { + val cache: mutable.Map[String, ParsedProgram] = mutable.Map() + val moduleNames: mutable.Map[String, String] = mutable.Map() + @JsonRequest("initialize") def initialize( params: InitializeParams @@ -143,19 +148,39 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { } else null } - private def findExpressionAtPosition( - documentPath: String, - position: Position - ): Option[Node] = { + private def getProgramForPath( + documentPath: String + ): (ParsedProgram, String) = { + var cachedProgram = cache.get(documentPath) + var cachedModuleName = moduleNames.get(documentPath) + + if (cachedProgram.isDefined && cachedModuleName.isDefined) { + return (cachedProgram.get, cachedModuleName.get) + } + val queue = new MosSourceLoadingQueue( initialFilenames = List(documentPath), includePath = context.includePath, options = options ) - val unoptimizedProgram = queue.run() + + var program = queue.run() + var moduleName = queue.extractName(documentPath) + + cache += ((documentPath, program)) + moduleNames += ((documentPath, moduleName)) + + return (program, moduleName) + } + + private def findExpressionAtPosition( + documentPath: String, + position: Position + ): Option[Node] = { + val (unoptimizedProgram, moduleName) = getProgramForPath(documentPath) val node = NodeFinder.findNodeAtPosition( - queue.extractName(documentPath), + moduleName, unoptimizedProgram, position ) From 4dbcc6d868daef214c04429f087449615301c171 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 24 Oct 2020 18:58:05 -0700 Subject: [PATCH 12/54] Module URI logic and partially working GoToDef --- build.sbt | 2 + src/main/scala/millfork/Main.scala | 1 + .../millfork/language/MfLanguageServer.scala | 81 ++++++++++++++++--- .../scala/millfork/language/NodeFinder.scala | 24 ++++-- .../parser/AbstractSourceLoadingQueue.scala | 8 +- 5 files changed, 97 insertions(+), 19 deletions(-) diff --git a/build.sbt b/build.sbt index 0ca08dbd..57d1d829 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,8 @@ libraryDependencies += "org.apache.commons" % "commons-configuration2" % "2.2" libraryDependencies += "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.9.0" +libraryDependencies += "net.liftweb" %% "lift-json" % "3.4.2" + libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test" libraryDependencies += "com.codingrodent.microprocessor" % "Z80Processor" % "2.0.2" % "test" diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index ea10ccd5..e0d66682 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -90,6 +90,7 @@ object Main { .setLocalService(server) .create() val clientProxy = launcher.getRemoteProxy + server.client = Some(clientProxy) launcher.startListening().get() } diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 68e984ef..69e95109 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -36,10 +36,15 @@ import org.eclipse.lsp4j.InitializedParams import org.eclipse.lsp4j.MarkupContent import org.eclipse.lsp4j.DefinitionParams import org.eclipse.lsp4j.Location +import net.liftweb.json._ +import net.liftweb.json.Serialization.{read, write} +import org.eclipse.lsp4j.MessageParams +import org.eclipse.lsp4j.MessageType class MfLanguageServer(context: Context, options: CompilationOptions) { - val cache: mutable.Map[String, ParsedProgram] = mutable.Map() - val moduleNames: mutable.Map[String, String] = mutable.Map() + var client: Option[MfLanguageClient] = None + private val cache: mutable.Map[String, ParsedProgram] = mutable.Map() + private val moduleNames: mutable.Map[String, String] = mutable.Map() @JsonRequest("initialize") def initialize( @@ -86,8 +91,10 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { CompletableFuture.completedFuture { val activePosition = params.getPosition() + val documentPath = params.getTextDocument().getUri().stripPrefix("file:") + val statement = findExpressionAtPosition( - params.getTextDocument().getUri().stripPrefix("file:"), + documentPath, Position( "", activePosition.getLine() + 1, @@ -97,12 +104,35 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ) if (statement.isDefined) { - val declarationContent = statement.get + val (declarationModule, declarationContent) = statement.get val formatting = NodeFormatter.symbol(declarationContent) + logEvent( + TelemetryEvent("Attempting to GoToDef in module", declarationModule) + ) + + // TODO: Prevent second fetch + val (unoptimizedProgram, _) = getProgramForPath(documentPath) + + val modulePath = unoptimizedProgram.modulePaths.getOrElse( + declarationModule, { + logEvent( + TelemetryEvent( + "Could not find path for module", + declarationModule + ) + ) + null + } + ) + + logEvent( + TelemetryEvent("Found module path", modulePath.toString()) + ) + if (declarationContent.position.isDefined) new Location( - params.getTextDocument().getUri(), + modulePath.toUri().toString(), new Range( mfPositionToLSP4j(declarationContent.position.get), mfPositionToLSP4j(declarationContent.position.get) @@ -120,7 +150,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { val hoverPosition = params.getPosition() val statement = findExpressionAtPosition( - params.getTextDocument().getUri().stripPrefix("file:"), + params.getTextDocument().getUri().stripPrefix("file://"), Position( "", // Millfork positions start at 1,2, rather than 0,0, so add to each coord @@ -131,7 +161,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ) if (statement.isDefined) { - val declarationContent = statement.get + val (_, declarationContent) = statement.get val formatting = NodeFormatter.symbol(declarationContent) if (formatting.isDefined) @@ -158,6 +188,10 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { return (cachedProgram.get, cachedModuleName.get) } + logEvent( + TelemetryEvent("Building program AST") + ) + val queue = new MosSourceLoadingQueue( initialFilenames = List(documentPath), includePath = context.includePath, @@ -167,6 +201,10 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { var program = queue.run() var moduleName = queue.extractName(documentPath) + logEvent( + TelemetryEvent("Finished building AST") + ) + cache += ((documentPath, program)) moduleNames += ((documentPath, moduleName)) @@ -176,7 +214,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { private def findExpressionAtPosition( documentPath: String, position: Position - ): Option[Node] = { + ): Option[(String, Node)] = { val (unoptimizedProgram, moduleName) = getProgramForPath(documentPath) val node = NodeFinder.findNodeAtPosition( @@ -186,15 +224,38 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ) if (node.isDefined) { + logEvent(TelemetryEvent("Found node at position", node)) + val usage = NodeFinder.findDeclarationForUsage(unoptimizedProgram, node.get) - if (usage.isDefined) usage else node - } else None + if (usage.isDefined) { + logEvent(TelemetryEvent("Found original declaration", usage)) + usage + } else Some((moduleName, node.get)) + } else { + logEvent(TelemetryEvent("Cannot find node for position", position)) + None + } } private def mfPositionToLSP4j( position: Position ): org.eclipse.lsp4j.Position = new org.eclipse.lsp4j.Position(position.line - 1, position.column - 1) + + private def logEvent(event: TelemetryEvent) = { + val languageClient = client.getOrElse { + throw new Exception("Language client not registered") + } + + implicit val formats = Serialization.formats(NoTypeHints) + val serializedEvent = write(event) + + languageClient.logMessage( + new MessageParams(MessageType.Log, serializedEvent) + ) + } } + +case class TelemetryEvent(message: String, data: Any = None) diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index b04ce1b3..351abe61 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -20,13 +20,25 @@ object NodeFinder { def findDeclarationForUsage( program: ParsedProgram, node: Node - ): Option[DeclarationStatement] = { + ): Option[(String, DeclarationStatement)] = { node match { - case expression: Expression => - matchingDeclarationForExpression( - expression, - program.compilationOrderProgram.declarations - ) + case expression: Expression => { + val foundDeclaration = program.parsedModules.toStream + .map { + case (module, program) => { + val declaration = + matchingDeclarationForExpression( + expression, + program.declarations + ) + if (declaration.isDefined) Some((module, declaration.get)) + else None + } + } + .find(d => d.isDefined) + + if (foundDeclaration.isDefined) foundDeclaration.get else None + } case default => None } } diff --git a/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala b/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala index 8baccafe..ec9df5c0 100644 --- a/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala +++ b/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala @@ -1,7 +1,7 @@ package millfork.parser import java.nio.charset.StandardCharsets -import java.nio.file.{Files, Paths} +import java.nio.file.{Files, Path, Paths} import fastparse.core.Parsed.{Failure, Success} import millfork.{CompilationFlag, CompilationOptions, Tarjan} @@ -10,13 +10,14 @@ import millfork.node.{AliasDefinitionStatement, DeclarationStatement, ImportStat import scala.collection.mutable import scala.collection.convert.ImplicitConversionsToScala._ -case class ParsedProgram(compilationOrderProgram: Program, parsedModules: Map[String, Program]) +case class ParsedProgram(compilationOrderProgram: Program, parsedModules: Map[String, Program], modulePaths: Map[String, Path]) abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], val includePath: List[String], val options: CompilationOptions) { protected val parsedModules: mutable.Map[String, Program] = mutable.Map[String, Program]() + protected val modulePaths: mutable.Map[String, Path] = mutable.Map[String, Path]() protected val moduleDependecies: mutable.Set[(String, String)] = mutable.Set[(String, String)]() protected val moduleQueue: mutable.Queue[() => Unit] = mutable.Queue[() => Unit]() val extension: String = ".mfk" @@ -84,7 +85,7 @@ abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], val compilationOrder = Tarjan.sort(parsedModules.keys, moduleDependecies) options.log.debug("Compilation order: " + compilationOrder.mkString(", ")) - ParsedProgram(compilationOrder.filter(parsedModules.contains).map(parsedModules).reduce(_ + _).applyImportantAliases, parsedModules.toMap) + ParsedProgram(compilationOrder.filter(parsedModules.contains).map(parsedModules).reduce(_ + _).applyImportantAliases, parsedModules.toMap, modulePaths.toMap) } def lookupModuleFile(includePath: List[String], moduleName: String, position: Option[Position]): String = { @@ -110,6 +111,7 @@ abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], val filename: String = why.fold(p => lookupModuleFile(includePath, moduleName, p), s => s) options.log.debug(s"Parsing $filename") val path = Paths.get(filename) + modulePaths.put(moduleName, path) val parentDir = path.toFile.getAbsoluteFile.getParent val shortFileName = path.getFileName.toString val PreprocessingResult(src, featureConstants, pragmas) = Preprocessor(options, shortFileName, Files.readAllLines(path, StandardCharsets.UTF_8).toIndexedSeq, templateParams) From 15f9659332c2b7df5646e00ad24dceecee04bbd2 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 24 Oct 2020 22:39:53 -0700 Subject: [PATCH 13/54] Prebuild AST on document load --- .vscode/tasks.json | 17 +++++++++++++++++ .../millfork/language/MfLanguageServer.scala | 19 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 .vscode/tasks.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..35044b94 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Compile Millfork", + "type": "shell", + "command": "sbt 'set test in assembly := {}' compile && sbt 'set test in assembly := {}' assembly", + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} \ No newline at end of file diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 69e95109..7a790076 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -40,6 +40,8 @@ import net.liftweb.json._ import net.liftweb.json.Serialization.{read, write} import org.eclipse.lsp4j.MessageParams import org.eclipse.lsp4j.MessageType +import org.eclipse.lsp4j.DidOpenTextDocumentParams +import org.eclipse.lsp4j.TextDocumentSyncKind class MfLanguageServer(context: Context, options: CompilationOptions) { var client: Option[MfLanguageClient] = None @@ -56,6 +58,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { val capabilities = new ServerCapabilities() capabilities.setHoverProvider(true) capabilities.setDefinitionProvider(true) + capabilities.setTextDocumentSync(TextDocumentSyncKind.Full) new InitializeResult(capabilities) } @@ -84,6 +87,16 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { @JsonRequest("shutdown") def shutdown(): CompletableFuture[Object] = ??? + @JsonRequest("textDocument/didOpen") + def textDocumentDidOpen( + params: DidOpenTextDocumentParams + ): CompletableFuture[Unit] = + CompletableFuture.completedFuture { + // TODO: Get text directly from client, rather than loading via URI + getProgramForPath(trimDocumentUri(params.getTextDocument().getUri())) + () + } + @JsonRequest("textDocument/definition") def textDocumentDefinition( params: DefinitionParams @@ -91,7 +104,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { CompletableFuture.completedFuture { val activePosition = params.getPosition() - val documentPath = params.getTextDocument().getUri().stripPrefix("file:") + val documentPath = trimDocumentUri(params.getTextDocument().getUri()) val statement = findExpressionAtPosition( documentPath, @@ -150,7 +163,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { val hoverPosition = params.getPosition() val statement = findExpressionAtPosition( - params.getTextDocument().getUri().stripPrefix("file://"), + trimDocumentUri(params.getTextDocument().getUri()), Position( "", // Millfork positions start at 1,2, rather than 0,0, so add to each coord @@ -256,6 +269,8 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { new MessageParams(MessageType.Log, serializedEvent) ) } + + private def trimDocumentUri(uri: String): String = uri.stripPrefix("file:") } case class TelemetryEvent(message: String, data: Any = None) From 37b18ce0c62362a682698bcb240b1690ac179639 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 25 Oct 2020 10:13:12 -0700 Subject: [PATCH 14/54] AST rebuilding on didChange events --- .../millfork/language/MfLanguageServer.scala | 100 ++++++++++++++---- .../scala/millfork/language/NodeFinder.scala | 8 +- .../parser/AbstractSourceLoadingQueue.scala | 16 ++- 3 files changed, 97 insertions(+), 27 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 7a790076..73e41197 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -42,11 +42,20 @@ import org.eclipse.lsp4j.MessageParams import org.eclipse.lsp4j.MessageType import org.eclipse.lsp4j.DidOpenTextDocumentParams import org.eclipse.lsp4j.TextDocumentSyncKind +import org.eclipse.lsp4j.DidChangeTextDocumentParams +import millfork.parser.AbstractSourceLoadingQueue +import java.nio.file.Path +import java.nio.file.Paths +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier +import millfork.node.Program class MfLanguageServer(context: Context, options: CompilationOptions) { var client: Option[MfLanguageClient] = None - private val cache: mutable.Map[String, ParsedProgram] = mutable.Map() + + private val cachedModules: mutable.Map[String, Program] = mutable.Map() + private var cachedProgram: Option[ParsedProgram] = None private val moduleNames: mutable.Map[String, String] = mutable.Map() + private val modulePaths: mutable.Map[String, Path] = mutable.Map() @JsonRequest("initialize") def initialize( @@ -93,10 +102,59 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ): CompletableFuture[Unit] = CompletableFuture.completedFuture { // TODO: Get text directly from client, rather than loading via URI - getProgramForPath(trimDocumentUri(params.getTextDocument().getUri())) + populateProgramForPath(trimDocumentUri(params.getTextDocument().getUri())) () } + @JsonRequest("textDocument/didChange") + def textDocumentDidChange( + params: DidChangeTextDocumentParams + ): CompletableFuture[Unit] = + CompletableFuture.completedFuture { + val pathString = trimDocumentUri(params.getTextDocument().getUri()) + + logEvent(TelemetryEvent("Rebuilding AST for module at path", pathString)) + + val queue = new MosSourceLoadingQueue( + initialFilenames = context.inputFileNames, + includePath = context.includePath, + options = options + ) + + val documentText = + params.getContentChanges().get(0).getText().split("\n").toSeq + + val path = Paths.get(pathString) + + logEvent(TelemetryEvent("Path", path.toString())) + + val moduleName = queue.extractName(pathString) + val newProgram = queue.parseModuleWithLines( + moduleName, + path, + documentText, + context.includePath, + Left(None), + Nil + ) + + if (newProgram.isDefined) { + cachedModules.put(moduleName, newProgram.get) + + logEvent( + TelemetryEvent( + "Finished rebuilding AST for module at path", + pathString + ) + ) + } else { + logEvent( + TelemetryEvent("Failed to rebuild AST for module at path", pathString) + ) + } + + } + @JsonRequest("textDocument/definition") def textDocumentDefinition( params: DefinitionParams @@ -124,10 +182,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { TelemetryEvent("Attempting to GoToDef in module", declarationModule) ) - // TODO: Prevent second fetch - val (unoptimizedProgram, _) = getProgramForPath(documentPath) - - val modulePath = unoptimizedProgram.modulePaths.getOrElse( + val modulePath = modulePaths.getOrElse( declarationModule, { logEvent( TelemetryEvent( @@ -191,16 +246,9 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { } else null } - private def getProgramForPath( + private def populateProgramForPath( documentPath: String - ): (ParsedProgram, String) = { - var cachedProgram = cache.get(documentPath) - var cachedModuleName = moduleNames.get(documentPath) - - if (cachedProgram.isDefined && cachedModuleName.isDefined) { - return (cachedProgram.get, cachedModuleName.get) - } - + ) = { logEvent( TelemetryEvent("Building program AST") ) @@ -218,21 +266,31 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { TelemetryEvent("Finished building AST") ) - cache += ((documentPath, program)) + cachedProgram = Some(program) + program.parsedModules.foreach { + case (moduleName, program) => + cachedModules.put(moduleName, program) + } moduleNames += ((documentPath, moduleName)) - - return (program, moduleName) + program.modulePaths.foreach { + case (moduleName, path) => modulePaths.put(moduleName, path) + } } + private def moduleNameForPath(documentPath: String) = + moduleNames.get(documentPath).getOrElse { + throw new Exception("Cannot find module at " + documentPath) + } + private def findExpressionAtPosition( documentPath: String, position: Position ): Option[(String, Node)] = { - val (unoptimizedProgram, moduleName) = getProgramForPath(documentPath) + val moduleName = moduleNameForPath(documentPath) val node = NodeFinder.findNodeAtPosition( moduleName, - unoptimizedProgram, + cachedModules.toMap, position ) @@ -240,7 +298,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { logEvent(TelemetryEvent("Found node at position", node)) val usage = - NodeFinder.findDeclarationForUsage(unoptimizedProgram, node.get) + NodeFinder.findDeclarationForUsage(cachedModules.toStream, node.get) if (usage.isDefined) { logEvent(TelemetryEvent("Found original declaration", usage)) diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 351abe61..a19fc8f3 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -18,12 +18,12 @@ import millfork.node.ExpressionStatement object NodeFinder { def findDeclarationForUsage( - program: ParsedProgram, + parsedModules: Stream[(String, Program)], node: Node ): Option[(String, DeclarationStatement)] = { node match { case expression: Expression => { - val foundDeclaration = program.parsedModules.toStream + val foundDeclaration = parsedModules.toStream .map { case (module, program) => { val declaration = @@ -61,13 +61,13 @@ object NodeFinder { def findNodeAtPosition( module: String, - program: ParsedProgram, + parsedModules: Map[String, Program], position: Position ): Option[Node] = { val line = position.line val column = position.column - val activeProgram = program.parsedModules.get(module) + val activeProgram = parsedModules.get(module) if (activeProgram.isEmpty) { return None diff --git a/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala b/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala index ec9df5c0..3e869a0b 100644 --- a/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala +++ b/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala @@ -107,14 +107,24 @@ abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], if (templateParams.isEmpty) moduleNameBase else moduleNameBase + templateParams.mkString("<", ",", ">") } + /** + * Finds module path and builds module AST, adding to `parsedModules` + */ def parseModule(moduleName: String, includePath: List[String], why: Either[Option[Position], String], templateParams: List[String]): Unit = { val filename: String = why.fold(p => lookupModuleFile(includePath, moduleName, p), s => s) options.log.debug(s"Parsing $filename") val path = Paths.get(filename) + modulePaths.put(moduleName, path) + + parseModuleWithLines(moduleName, path, Files.readAllLines(path, StandardCharsets.UTF_8).toIndexedSeq, includePath, why, templateParams) + } + + def parseModuleWithLines(moduleName: String, path: Path, lines: Seq[String], includePath: List[String], why: Either[Option[Position], String], templateParams: List[String]): Option[Program] = { val parentDir = path.toFile.getAbsoluteFile.getParent val shortFileName = path.getFileName.toString - val PreprocessingResult(src, featureConstants, pragmas) = Preprocessor(options, shortFileName, Files.readAllLines(path, StandardCharsets.UTF_8).toIndexedSeq, templateParams) + + val PreprocessingResult(src, featureConstants, pragmas) = Preprocessor(options, shortFileName, lines, templateParams) for (pragma <- pragmas) { if (!supportedPragmas(pragma._1) && options.flag(CompilationFlag.BuggyCodeWarning)) { options.log.warn(s"Unsupported pragma: #pragma ${pragma._1}", Some(Position(moduleName, pragma._2, 1, 0))) @@ -136,13 +146,15 @@ abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], case _ => () } } + Some(prog) case f@Failure(a, b, d) => - options.log.error(s"Failed to parse the module `$moduleName` in $filename", Some(parser.indexToPosition(f.index, parser.lastLabel))) + options.log.error(s"Failed to parse the module `$moduleName` in ${path.toString()}", Some(parser.indexToPosition(f.index, parser.lastLabel))) if (parser.lastLabel != "") { options.log.error(s"Syntax error: ${parser.lastLabel} expected", Some(parser.lastPosition)) } else { options.log.error("Syntax error", Some(parser.lastPosition)) } + None } } From ca81506594123f432be8477b90fd9c34d4e405fa Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 25 Oct 2020 12:59:23 -0700 Subject: [PATCH 15/54] Position and GoToDef for import statements --- .../scala/millfork/language/MfLanguageServer.scala | 13 ++++++++++--- src/main/scala/millfork/language/NodeFinder.scala | 3 +++ .../scala/millfork/language/NodeFormatter.scala | 3 +++ src/main/scala/millfork/parser/MfParser.scala | 4 ++-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 73e41197..5f667277 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -48,6 +48,7 @@ import java.nio.file.Path import java.nio.file.Paths import org.eclipse.lsp4j.VersionedTextDocumentIdentifier import millfork.node.Program +import millfork.node.ImportStatement class MfLanguageServer(context: Context, options: CompilationOptions) { var client: Option[MfLanguageClient] = None @@ -198,12 +199,18 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { TelemetryEvent("Found module path", modulePath.toString()) ) - if (declarationContent.position.isDefined) + // ImportStatement declaration is the entire "file". Set position to 1,1 + val position = + if (declarationContent.isInstanceOf[ImportStatement]) + Some(Position(declarationModule, 1, 1, 0)) + else declarationContent.position + + if (position.isDefined) new Location( modulePath.toUri().toString(), new Range( - mfPositionToLSP4j(declarationContent.position.get), - mfPositionToLSP4j(declarationContent.position.get) + mfPositionToLSP4j(position.get), + mfPositionToLSP4j(position.get) ) ) else null diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index a19fc8f3..8c4602d7 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -15,6 +15,7 @@ import millfork.parser.ParsedProgram import scala.collection.mutable import millfork.node.ExpressionStatement +import millfork.node.ImportStatement object NodeFinder { def findDeclarationForUsage( @@ -22,6 +23,8 @@ object NodeFinder { node: Node ): Option[(String, DeclarationStatement)] = { node match { + case importStatement: ImportStatement => + Some((importStatement.filename, importStatement)) case expression: Expression => { val foundDeclaration = parsedModules.toStream .map { diff --git a/src/main/scala/millfork/language/NodeFormatter.scala b/src/main/scala/millfork/language/NodeFormatter.scala index da264a1d..061c3d03 100644 --- a/src/main/scala/millfork/language/NodeFormatter.scala +++ b/src/main/scala/millfork/language/NodeFormatter.scala @@ -12,6 +12,7 @@ import millfork.env.ParamPassingConvention import millfork.env.ByConstant import millfork.env.ByVariable import millfork.env.ByReference +import millfork.node.ImportStatement object NodeFormatter { // TODO: Remove Option @@ -84,6 +85,8 @@ object NodeFormatter { Some(builder.toString()) } + case importStatement: ImportStatement => + Some(s"""import ${importStatement.filename}""") case default => None } case ParameterDeclaration(typ, assemblyParamPassingConvention) => diff --git a/src/main/scala/millfork/parser/MfParser.scala b/src/main/scala/millfork/parser/MfParser.scala index 00288236..f267aaba 100644 --- a/src/main/scala/millfork/parser/MfParser.scala +++ b/src/main/scala/millfork/parser/MfParser.scala @@ -193,9 +193,9 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri case x => x.toString } | textLiteralAtom.! - val importStatement: P[Seq[ImportStatement]] = ("import" ~ !letterOrDigit ~/ SWS ~/ + val importStatement: P[Seq[ImportStatement]] = (position() ~ "import" ~ !letterOrDigit ~/ SWS ~/ identifier.rep(min = 1, sep = "/") ~ HWS ~ ("<" ~/ HWS ~/ quotedAtom.rep(min = 1, sep = HWS ~ "," ~/ HWS) ~/ HWS ~/ ">" ~/ Pass).?). - map{case (name, params) => Seq(ImportStatement(name.mkString("/"), params.getOrElse(Nil).toList))} + map{case (p, name, params) => Seq(ImportStatement(name.mkString("/"), params.getOrElse(Nil).toList).pos(p))} val globalVariableDefinition: P[Seq[BankedDeclarationStatement]] = variableDefinition(true) val localVariableDefinition: P[Seq[DeclarationStatement]] = variableDefinition(false) From 974167dea57b90496164bec83f7b5b9e4ccba34a Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 25 Oct 2020 14:19:00 -0700 Subject: [PATCH 16/54] Find all references support --- .../millfork/language/MfLanguageServer.scala | 77 ++++++++++++++++++- .../scala/millfork/language/NodeFinder.scala | 30 ++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 5f667277..f5b02c59 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -49,6 +49,9 @@ import java.nio.file.Paths import org.eclipse.lsp4j.VersionedTextDocumentIdentifier import millfork.node.Program import millfork.node.ImportStatement +import org.eclipse.lsp4j.ReferenceParams +import millfork.node.DeclarationStatement +import scala.collection.JavaConverters._ class MfLanguageServer(context: Context, options: CompilationOptions) { var client: Option[MfLanguageClient] = None @@ -69,6 +72,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { capabilities.setHoverProvider(true) capabilities.setDefinitionProvider(true) capabilities.setTextDocumentSync(TextDocumentSyncKind.Full) + capabilities.setReferencesProvider(true) new InitializeResult(capabilities) } @@ -163,10 +167,8 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { CompletableFuture.completedFuture { val activePosition = params.getPosition() - val documentPath = trimDocumentUri(params.getTextDocument().getUri()) - val statement = findExpressionAtPosition( - documentPath, + trimDocumentUri(params.getTextDocument().getUri()), Position( "", activePosition.getLine() + 1, @@ -217,6 +219,75 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { } else null } + @JsonRequest("textDocument/references") + def textDocumentReferences( + params: ReferenceParams + ): CompletableFuture[ju.List[Location]] = + CompletableFuture.completedFuture { + val activePosition = params.getPosition() + + val statement = findExpressionAtPosition( + trimDocumentUri(params.getTextDocument().getUri()), + Position( + "", + activePosition.getLine() + 1, + activePosition.getCharacter() + 2, + 0 + ) + ) + + if (statement.isDefined) { + val (declarationModule, declarationContent) = statement.get + + logEvent( + TelemetryEvent("Attempting to find references") + ) + + if (declarationContent.isInstanceOf[DeclarationStatement]) { + val matchingExpressions = + List((declarationModule, declarationContent)) ++ NodeFinder + .matchingExpressionsForDeclaration( + cachedModules.toStream, + declarationContent.asInstanceOf[DeclarationStatement] + ) + + logEvent( + TelemetryEvent("Prepping references", matchingExpressions) + ) + + matchingExpressions + .map { + case (module, expression) => { + var position = expression.position + val modulePath = modulePaths.getOrElse( + module, { + logEvent( + TelemetryEvent( + "Could not find path for module", + module + ) + ) + null + } + ) + + new Location( + modulePath.toUri().toString(), + new Range( + mfPositionToLSP4j(position.get), + mfPositionToLSP4j(position.get) + ) + ) + } + } + .filter(e => e != null) + .asJava + } else { + null + } + } else null + } + @JsonRequest("textDocument/hover") def textDocumentHover( params: TextDocumentPositionParams diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 8c4602d7..26c34360 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -62,6 +62,36 @@ object NodeFinder { case default => None } + def matchingExpressionsForDeclaration( + parsedModules: Stream[(String, Program)], + declaration: DeclarationStatement + ): List[(String, Node)] = { + parsedModules.toStream.flatMap { + case (module, program) => { + val allDeclarations = + program.declarations + .flatMap(d => d.getAllExpressions) + .flatMap(flattenNestedExpressions) + + declaration match { + case f: FunctionDeclarationStatement => + allDeclarations + .filter(d => d.isInstanceOf[FunctionCallExpression]) + .map(d => d.asInstanceOf[FunctionCallExpression]) + .find(d => d.functionName == f.name) + .map(d => (module, d)) + case v: VariableDeclarationStatement => + allDeclarations + .filter(d => d.isInstanceOf[VariableExpression]) + .map(d => d.asInstanceOf[VariableExpression]) + .find(d => d.name == v.name) + .map(d => (module, d)) + case default => List() + } + } + }.toList + } + def findNodeAtPosition( module: String, parsedModules: Map[String, Program], From 3cd97ec1891c983a6ce4fd90e62debc71d9c5b48 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 25 Oct 2020 14:24:18 -0700 Subject: [PATCH 17/54] Added check for isIncludeDeclaration on references --- src/main/scala/millfork/language/MfLanguageServer.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index f5b02c59..2ea7a227 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -244,8 +244,12 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ) if (declarationContent.isInstanceOf[DeclarationStatement]) { + val matchingExpressions = - List((declarationModule, declarationContent)) ++ NodeFinder + // Only include declaration if params specify it + (if (params.getContext().isIncludeDeclaration()) + List((declarationModule, declarationContent)) + else List()) ++ NodeFinder .matchingExpressionsForDeclaration( cachedModules.toStream, declarationContent.asInstanceOf[DeclarationStatement] From ed4c88c84b09b9edc2f9b251f48538846bf7ce35 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 30 Oct 2020 20:26:11 -0700 Subject: [PATCH 18/54] Implemented .millforkrc.json config file --- src/main/scala/millfork/Context.scala | 1 + src/main/scala/millfork/Main.scala | 23 ++++--- .../scala/millfork/cli/JsonConfigParser.scala | 65 +++++++++++++++++++ 3 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 src/main/scala/millfork/cli/JsonConfigParser.scala diff --git a/src/main/scala/millfork/Context.scala b/src/main/scala/millfork/Context.scala index bb62e96e..ac672d5e 100644 --- a/src/main/scala/millfork/Context.scala +++ b/src/main/scala/millfork/Context.scala @@ -9,6 +9,7 @@ import millfork.error.Logger case class Context(errorReporting: Logger, inputFileNames: List[String], outputFileName: Option[String] = None, + configFilePath: Option[String] = None, runFileName: Option[String] = None, runParams: Seq[String] = Vector(), optimizationLevel: Option[Int] = None, diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index e0d66682..22066e66 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -24,6 +24,7 @@ import org.eclipse.lsp4j.services.LanguageServer import org.eclipse.lsp4j.jsonrpc.Launcher import java.util.concurrent.Executors import java.io.PrintWriter +import millfork.cli.JsonConfigParser object Main { @@ -41,6 +42,7 @@ object Main { val startTime = System.nanoTime() val (status, c0) = parser(errorReporting).parse(Context(errorReporting, Nil), args.toList) + val c1 = JsonConfigParser.parseConfig(c0, errorReporting) status match { case CliStatus.Quit => return case CliStatus.Failed => @@ -48,8 +50,8 @@ object Main { case CliStatus.Ok => () } errorReporting.assertNoErrors("Invalid command line") - errorReporting.verbosity = c0.verbosity.getOrElse(0) - if (c0.inputFileNames.isEmpty && !c0.languageServer) { + errorReporting.verbosity = c1.verbosity.getOrElse(0) + if (c1.inputFileNames.isEmpty && !c1.languageServer) { errorReporting.fatalQuit("No input files") } @@ -58,14 +60,14 @@ object Main { errorReporting.trace("This program comes with ABSOLUTELY NO WARRANTY.") errorReporting.trace("This is free software, and you are welcome to redistribute it under certain conditions") errorReporting.trace("You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/") - val c = fixMissingIncludePath(c0).filloutFlags() + val c = fixMissingIncludePath(c1).filloutFlags() if (c.includePath.isEmpty) { errorReporting.warn("Failed to detect the default include directory, consider using the -I option") } val textCodecRepository = new TextCodecRepository("." :: c.includePath) val platform = Platform.lookupPlatformFile("." :: c.includePath, c.platform.getOrElse { - if (!c0.languageServer) errorReporting.info("No platform selected, defaulting to `c64`") + if (!c1.languageServer) errorReporting.info("No platform selected, defaulting to `c64`") "c64" }, textCodecRepository) val options = CompilationOptions(platform, c.flags, c.outputFileName, c.zpRegisterSize.getOrElse(platform.zpRegisterSize), c.features, textCodecRepository, JobContext(new LanguageServerLogger(), new LabelGenerator)) @@ -74,15 +76,13 @@ object Main { case (f, b) => errorReporting.debug(f" $f%-30s : $b%s") } - if (c0.languageServer) { + if (c1.languageServer) { // We cannot log anything to stdout when starting the language server (otherwise it's a protocol violation) val server = new MfLanguageServer(c, options) val exec = Executors.newCachedThreadPool() - val tracePrinter = new PrintWriter(new File("/users/adam/millforklsp.log")) val launcher = new Launcher.Builder[MfLanguageClient]() - .traceMessages(tracePrinter) .setExecutorService(exec) .setInput(System.in) .setOutput(System.out) @@ -456,9 +456,14 @@ object Main { c.copy(outputLabels = true, outputLabelsFormatOverride = Some(f)) }.description("Generate also the label file in the given format. Available options: vice, nesasm, sym.") - flag("-lsp").repeatable().action { c => + flag("-lsp").action { c => c.copy(languageServer = true) - }.description("Start the Millfork language server. Does not start compilation") + }.description("Start the Millfork language server. Does not start compilation.") + + parameter("-c", "--config").placeholder("").action { (p, c) => + assertNone(c.outputFileName, "Config file already defined") + c.copy(configFilePath = Some(p)) + }.description("The Millfork config file. Suppliments the provided CLI options.") boolean("-fbreakpoints", "-fno-breakpoints").action((c,v) => c.changeFlag(CompilationFlag.EnableBreakpoints, v) diff --git a/src/main/scala/millfork/cli/JsonConfigParser.scala b/src/main/scala/millfork/cli/JsonConfigParser.scala new file mode 100644 index 00000000..da89206e --- /dev/null +++ b/src/main/scala/millfork/cli/JsonConfigParser.scala @@ -0,0 +1,65 @@ +package millfork.cli + +import net.liftweb.json._ +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.charset.StandardCharsets +import scala.collection.mutable +import scala.collection.convert.ImplicitConversionsToScala._ +import java.io.InputStreamReader +import millfork.Context +import millfork.error.ConsoleLogger + +case class JsonConfig( + include: Option[List[String]], + platform: Option[String], + inputFiles: Option[List[String]] +) + +object JsonConfigParser { + implicit val formats = DefaultFormats + + def parseConfig(context: Context, logger: ConsoleLogger): Context = { + var newContext = context + + var defaultConfig = false + val filePath = context.configFilePath.getOrElse({ + defaultConfig = true + ".millforkrc.json" + }) + + val path = Paths.get(filePath) + + try { + val jsonString = + Files + .readAllLines(path, StandardCharsets.UTF_8) + .toIndexedSeq + .mkString("") + + val result = parse(jsonString).extract[JsonConfig] + + if (context.inputFileNames.length < 1 && result.inputFiles.isDefined) { + newContext = newContext.copy(inputFileNames = result.inputFiles.get) + } + + if (context.includePath.length < 1 && result.include.isDefined) { + newContext = + newContext.copy(extraIncludePath = result.include.get.toSeq) + } + + if (context.platform.isEmpty && result.platform.isDefined) { + newContext = newContext.copy(platform = Some(result.platform.get)) + } + } catch { + case default: Throwable => { + if (!defaultConfig) { + // Only throw error if not default config + logger.fatalQuit("Invalid config file") + } + } + } + + newContext + } +} From d34c4803fd4c3417f3da9b4e51d7af3d764048d6 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 31 Oct 2020 11:19:43 -0700 Subject: [PATCH 19/54] Proper formatting for asm functions --- .../millfork/language/NodeFormatter.scala | 21 ++++++-- src/main/scala/millfork/node/Node.scala | 52 +++++++++++++++++++ .../scala/millfork/parser/M6809Parser.scala | 15 +----- .../scala/millfork/parser/MosParser.scala | 15 +----- .../scala/millfork/parser/Z80Parser.scala | 15 +----- 5 files changed, 74 insertions(+), 44 deletions(-) diff --git a/src/main/scala/millfork/language/NodeFormatter.scala b/src/main/scala/millfork/language/NodeFormatter.scala index 061c3d03..2db8cd2b 100644 --- a/src/main/scala/millfork/language/NodeFormatter.scala +++ b/src/main/scala/millfork/language/NodeFormatter.scala @@ -13,6 +13,13 @@ import millfork.env.ByConstant import millfork.env.ByVariable import millfork.env.ByReference import millfork.node.ImportStatement +import millfork.env.ByLazilyEvaluableExpressionVariable +import millfork.env.ByMosRegister +import millfork.node.MosRegister +import millfork.env.ByZRegister +import millfork.node.ZRegister +import millfork.env.ByM6809Register +import millfork.node.M6809Register object NodeFormatter { // TODO: Remove Option @@ -102,11 +109,15 @@ object NodeFormatter { def symbol(paramConvention: ParamPassingConvention): String = paramConvention match { - case ByConstant(name) => name - case ByVariable(name) => name - case ByReference(name) => name - // TODO: Remove default - case default => "" + case ByConstant(name) => name + case ByVariable(name) => name + case ByReference(name) => name + case ByLazilyEvaluableExpressionVariable(name) => name + case ByMosRegister(register) => + MosRegister.toString(register).getOrElse("") + case ByZRegister(register) => ZRegister.toString(register).getOrElse("") + case ByM6809Register(register) => + M6809Register.toString(register).getOrElse("") } /** diff --git a/src/main/scala/millfork/node/Node.scala b/src/main/scala/millfork/node/Node.scala index d43d0f67..fcf345a8 100644 --- a/src/main/scala/millfork/node/Node.scala +++ b/src/main/scala/millfork/node/Node.scala @@ -251,12 +251,46 @@ object M6809NiceFunctionProperty { object MosRegister extends Enumeration { val A, X, Y, AX, AY, YA, XA, XY, YX, AW = Value + + private val registerStringToValue = Map[String, MosRegister.Value]( + "xy" -> MosRegister.XY, + "yx" -> MosRegister.YX, + "ax" -> MosRegister.AX, + "ay" -> MosRegister.AY, + "xa" -> MosRegister.XA, + "ya" -> MosRegister.YA, + "a" -> MosRegister.A, + "x" -> MosRegister.X, + "y" -> MosRegister.Y, + ) + private val registerValueToString = registerStringToValue.map { case (key, value) => (value, key)}.toMap + + def fromString(name: String): Option[MosRegister.Value] = registerStringToValue.get(name) + def toString(value: MosRegister.Value): Option[String] = registerValueToString.get(value) } object ZRegister extends Enumeration { val A, B, C, D, E, H, L, AF, BC, HL, DE, SP, IXH, IXL, IYH, IYL, IX, IY, R, I, MEM_HL, MEM_BC, MEM_DE, MEM_IX_D, MEM_IY_D, MEM_ABS_8, MEM_ABS_16, IMM_8, IMM_16 = Value + val registerStringToValue = Map[String, ZRegister.Value]( + "hl" -> ZRegister.HL, + "bc" -> ZRegister.BC, + "de" -> ZRegister.DE, + "a" -> ZRegister.A, + "b" -> ZRegister.B, + "c" -> ZRegister.C, + "d" -> ZRegister.D, + "e" -> ZRegister.E, + "h" -> ZRegister.H, + "l" -> ZRegister.L, + ) + + private val registerValueToString = registerStringToValue.map { case (key, value) => (value, key)}.toMap + + def fromString(name: String): Option[ZRegister.Value] = registerStringToValue.get(name) + def toString(value: ZRegister.Value): Option[String] = registerValueToString.get(value) + def registerSize(reg: Value): Int = reg match { case AF | BC | DE | HL | IX | IY | IMM_16 => 2 case A | B | C | D | E | H | L | IXH | IXL | IYH | IYL | R | I | IMM_8 => 1 @@ -278,6 +312,24 @@ object ZRegister extends Enumeration { object M6809Register extends Enumeration { val A, B, D, DP, X, Y, U, S, PC, CC = Value + val registerStringToValue = Map[String, M6809Register.Value]( + "x" -> M6809Register.X, + "y" -> M6809Register.Y, + "s" -> M6809Register.S, + "u" -> M6809Register.U, + "a" -> M6809Register.A, + "b" -> M6809Register.B, + "d" -> M6809Register.D, + "dp" -> M6809Register.DP, + "pc" -> M6809Register.PC, + "cc" -> M6809Register.CC, + ) + + private val registerValueToString = registerStringToValue.map { case (key, value) => (value, key)}.toMap + + def fromString(name: String): Option[M6809Register.Value] = registerStringToValue.get(name) + def toString(value: M6809Register.Value): Option[String] = registerValueToString.get(value) + def registerSize(reg: Value): Int = reg match { case D | X | Y | U | S | PC => 2 case A | B | DP | CC => 1 diff --git a/src/main/scala/millfork/parser/M6809Parser.scala b/src/main/scala/millfork/parser/M6809Parser.scala index ca15a190..a105c9a6 100644 --- a/src/main/scala/millfork/parser/M6809Parser.scala +++ b/src/main/scala/millfork/parser/M6809Parser.scala @@ -45,21 +45,10 @@ case class M6809Parser(filename: String, val asmOpcode: P[(MOpcode.Value, Option[MAddrMode])] = (position() ~ (letter.rep ~ ("2" | "3").?).! ).map { case (p, o) => MOpcode.lookup(o, Some(p), log) } - private def mapRegister(p: (Position, String)): M6809Register.Value = p._2.toLowerCase(Locale.ROOT) match { - case "x" => M6809Register.X - case "y" => M6809Register.Y - case "s" => M6809Register.S - case "u" => M6809Register.U - case "a" => M6809Register.A - case "b" => M6809Register.B - case "d" => M6809Register.D - case "dp" => M6809Register.DP - case "pc" => M6809Register.PC - case "cc" => M6809Register.CC - case _ => + private def mapRegister(p: (Position, String)): M6809Register.Value = M6809Register.fromString(p._2.toLowerCase(Locale.ROOT)).getOrElse({ log.error("Invalid register " + p._2, Some(p._1)) M6809Register.D - } + }) // only used for TFR, EXG, PSHS, PULS, PSHU, PULU, so it is allowed to accept any register name in order to let parsing continue: val anyRegister: P[M6809Register.Value] = P(position() ~ identifier.!).map(mapRegister) diff --git a/src/main/scala/millfork/parser/MosParser.scala b/src/main/scala/millfork/parser/MosParser.scala index 44447c80..a39af0e8 100644 --- a/src/main/scala/millfork/parser/MosParser.scala +++ b/src/main/scala/millfork/parser/MosParser.scala @@ -116,19 +116,8 @@ case class MosParser(filename: String, input: String, currentDirectory: String, val asmStatement: P[ExecutableStatement] = (position("assembly statement") ~ P(asmLabel | asmMacro | arrayContentsForAsm | asmInstruction)).map { case (p, s) => s.pos(p) } // TODO: macros - - override val appcRegister: P[ParamPassingConvention] = P(("xy" | "yx" | "ax" | "ay" | "xa" | "ya" | "a" | "x" | "y") ~ !letterOrDigit).!.map { - case "xy" => ByMosRegister(MosRegister.XY) - case "yx" => ByMosRegister(MosRegister.YX) - case "ax" => ByMosRegister(MosRegister.AX) - case "ay" => ByMosRegister(MosRegister.AY) - case "xa" => ByMosRegister(MosRegister.XA) - case "ya" => ByMosRegister(MosRegister.YA) - case "a" => ByMosRegister(MosRegister.A) - case "x" => ByMosRegister(MosRegister.X) - case "y" => ByMosRegister(MosRegister.Y) - case x => log.fatal(s"Unknown assembly parameter passing convention: `$x`") - } + override val appcRegister: P[ParamPassingConvention] = P(("xy" | "yx" | "ax" | "ay" | "xa" | "ya" | "a" | "x" | "y") ~ !letterOrDigit).! + .map(name => ByMosRegister(MosRegister.fromString(name).getOrElse(log.fatal(s"Unknown assembly parameter passing convention: `$name`")))) def validateAsmFunctionBody(p: Position, flags: Set[String], name: String, statements: Option[List[Statement]]): Unit = { if (!options.flag(CompilationFlag.BuggyCodeWarning)) return diff --git a/src/main/scala/millfork/parser/Z80Parser.scala b/src/main/scala/millfork/parser/Z80Parser.scala index c0faa95f..7966e0d9 100644 --- a/src/main/scala/millfork/parser/Z80Parser.scala +++ b/src/main/scala/millfork/parser/Z80Parser.scala @@ -31,19 +31,8 @@ case class Z80Parser(filename: String, private val zero = LiteralExpression(0, 1) - override val appcRegister: P[ParamPassingConvention] = (P("hl" | "bc" | "de" | "a" | "b" | "c" | "d" | "e" | "h" | "l").! ~ !letterOrDigit).map { - case "a" => ByZRegister(ZRegister.A) - case "b" => ByZRegister(ZRegister.B) - case "c" => ByZRegister(ZRegister.C) - case "d" => ByZRegister(ZRegister.D) - case "e" => ByZRegister(ZRegister.E) - case "h" => ByZRegister(ZRegister.H) - case "l" => ByZRegister(ZRegister.L) - case "hl" => ByZRegister(ZRegister.HL) - case "bc" => ByZRegister(ZRegister.BC) - case "de" => ByZRegister(ZRegister.DE) - case x => log.fatal(s"Unknown assembly parameter passing convention: `$x`") - } + override val appcRegister: P[ParamPassingConvention] = (P("hl" | "bc" | "de" | "a" | "b" | "c" | "d" | "e" | "h" | "l").! ~ !letterOrDigit) + .map(name => ByZRegister(ZRegister.fromString(name).getOrElse(log.fatal(s"Unknown assembly parameter passing convention: `$name`")))) override val asmParamDefinition: P[ParameterDeclaration] = for { p <- position() From 5bff531a8eb00b5fa364597c5b80798b66f4c11e Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 31 Oct 2020 11:52:13 -0700 Subject: [PATCH 20/54] Added array formatting --- .../millfork/language/NodeFormatter.scala | 76 +++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/src/main/scala/millfork/language/NodeFormatter.scala b/src/main/scala/millfork/language/NodeFormatter.scala index 2db8cd2b..f344ba81 100644 --- a/src/main/scala/millfork/language/NodeFormatter.scala +++ b/src/main/scala/millfork/language/NodeFormatter.scala @@ -20,6 +20,11 @@ import millfork.env.ByZRegister import millfork.node.ZRegister import millfork.env.ByM6809Register import millfork.node.M6809Register +import millfork.node.ArrayDeclarationStatement +import millfork.output.MemoryAlignment +import millfork.output.NoAlignment +import millfork.output.DivisibleAlignment +import millfork.output.WithinPageAlignment object NodeFormatter { // TODO: Remove Option @@ -94,6 +99,57 @@ object NodeFormatter { } case importStatement: ImportStatement => Some(s"""import ${importStatement.filename}""") + case arrayStatement: ArrayDeclarationStatement => { + val builder = new StringBuilder() + + if (arrayStatement.const) { + builder.append("const ") + } + + builder.append( + s"""array(${arrayStatement.elementType}) ${arrayStatement.name}""" + ) + + if (arrayStatement.length.isDefined) { + val formattedLength = symbol(arrayStatement.length.get) + + if (formattedLength.isDefined) { + builder.append(s""" [${formattedLength.get}]""") + } + } + + if (arrayStatement.alignment.isDefined) { + val formattedAlignment = symbol( + arrayStatement.alignment.get + ) + + if (formattedAlignment.isDefined) { + builder.append(s""" align(${formattedAlignment.get})""") + } + } + + if (arrayStatement.address.isDefined) { + val formattedAddress = symbol(arrayStatement.address.get) + + if (formattedAddress.isDefined) { + builder.append(s""" @ ${formattedAddress.get}""") + } + } + + if (arrayStatement.elements.isDefined) { + val formattedInitialValue = arrayStatement.elements.get + .getAllExpressions(false) + .map(e => symbol(e)) + .filter(e => e.isDefined) + .map(e => e.get) + .mkString(", ") + + builder.append(s""" = [${formattedInitialValue}]""") + } + + Some(builder.toString()) + } + // TODO: Finish case default => None } case ParameterDeclaration(typ, assemblyParamPassingConvention) => @@ -120,9 +176,15 @@ object NodeFormatter { M6809Register.toString(register).getOrElse("") } + def symbol(alignment: MemoryAlignment): Option[String] = + alignment match { + case NoAlignment => None + // TOOD: Improve + case DivisibleAlignment(divisor) => Some(s"""${divisor}""") + case WithinPageAlignment => Some("Within page") + } + /** - * TODO: This function is nearly the same as https://github.com/scalameta/metals/blob/main/mtags/src/main/scala/scala/meta/internal/pc/HoverMarkup.scala - * * Render the textDocument/hover result into markdown. * * @param symbolSignature The signature of the symbol over the cursor, for example @@ -134,16 +196,18 @@ object NodeFormatter { docstring: String ): String = { val markdown = new StringBuilder() + + if (docstring.nonEmpty) + markdown + .append("\n") + .append(docstring) + if (symbolSignature.nonEmpty) { markdown .append("```mfk\n") .append(symbolSignature) .append("\n```") } - if (docstring.nonEmpty) - markdown - .append("\n") - .append(docstring) markdown.toString() } } From 47a0a544e851f92c1ad9d2b027a1bd36c60e8bd4 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 31 Oct 2020 12:45:07 -0700 Subject: [PATCH 21/54] Added alias support --- src/main/scala/millfork/language/NodeFormatter.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/scala/millfork/language/NodeFormatter.scala b/src/main/scala/millfork/language/NodeFormatter.scala index f344ba81..8763843f 100644 --- a/src/main/scala/millfork/language/NodeFormatter.scala +++ b/src/main/scala/millfork/language/NodeFormatter.scala @@ -25,6 +25,7 @@ import millfork.output.MemoryAlignment import millfork.output.NoAlignment import millfork.output.DivisibleAlignment import millfork.output.WithinPageAlignment +import millfork.node.AliasDefinitionStatement object NodeFormatter { // TODO: Remove Option @@ -149,6 +150,17 @@ object NodeFormatter { Some(builder.toString()) } + case AliasDefinitionStatement(name, target, important) => { + val builder = new StringBuilder() + + builder.append(s"""alias ${name} = ${target}""") + + if (important) { + builder.append("!") + } + + Some(builder.toString()) + } // TODO: Finish case default => None } From cfbb6d7581e52f2d51da31e81799c51952de8e5f Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 31 Oct 2020 13:24:15 -0700 Subject: [PATCH 22/54] Fixed AST not being built starting from initial paths --- .../millfork/language/MfLanguageServer.scala | 99 ++++++++++--------- .../parser/AbstractSourceLoadingQueue.scala | 1 + 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 2ea7a227..f9d2a076 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -78,12 +78,10 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { } @JsonNotification("initialized") - def initialized(params: InitializedParams): CompletableFuture[Unit] = { - val completableFuture = new CompletableFuture[Unit]() - - completableFuture.complete() - completableFuture - } + def initialized(params: InitializedParams): CompletableFuture[Unit] = + CompletableFuture.completedFuture { + populateProgramForPath() + } // @JsonRequest("getTextDocumentService") // def getTextDocumentService(): CompletableFuture[TextDocumentService] = { @@ -106,9 +104,12 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { params: DidOpenTextDocumentParams ): CompletableFuture[Unit] = CompletableFuture.completedFuture { - // TODO: Get text directly from client, rather than loading via URI - populateProgramForPath(trimDocumentUri(params.getTextDocument().getUri())) - () + val pathString = trimDocumentUri(params.getTextDocument().getUri()) + + val documentText = + params.getTextDocument().getText().split("\n").toSeq + + rebuildASTForFile(pathString, documentText) } @JsonRequest("textDocument/didChange") @@ -118,47 +119,52 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { CompletableFuture.completedFuture { val pathString = trimDocumentUri(params.getTextDocument().getUri()) - logEvent(TelemetryEvent("Rebuilding AST for module at path", pathString)) - - val queue = new MosSourceLoadingQueue( - initialFilenames = context.inputFileNames, - includePath = context.includePath, - options = options - ) - val documentText = params.getContentChanges().get(0).getText().split("\n").toSeq - val path = Paths.get(pathString) + rebuildASTForFile(pathString, documentText) + } - logEvent(TelemetryEvent("Path", path.toString())) + def rebuildASTForFile(pathString: String, text: Seq[String]) = { + logEvent(TelemetryEvent("Rebuilding AST for module at path", pathString)) - val moduleName = queue.extractName(pathString) - val newProgram = queue.parseModuleWithLines( - moduleName, - path, - documentText, - context.includePath, - Left(None), - Nil - ) + val queue = new MosSourceLoadingQueue( + initialFilenames = context.inputFileNames, + includePath = context.includePath, + options = options + ) - if (newProgram.isDefined) { - cachedModules.put(moduleName, newProgram.get) + val path = Paths.get(pathString) - logEvent( - TelemetryEvent( - "Finished rebuilding AST for module at path", - pathString - ) - ) - } else { - logEvent( - TelemetryEvent("Failed to rebuild AST for module at path", pathString) - ) - } + logEvent(TelemetryEvent("Path", path.toString())) + + val moduleName = queue.extractName(pathString) + val newProgram = queue.parseModuleWithLines( + moduleName, + path, + text, + context.includePath, + Left(None), + Nil + ) + + if (newProgram.isDefined) { + cachedModules.put(moduleName, newProgram.get) + moduleNames.put(pathString, moduleName) + + logEvent( + TelemetryEvent( + "Finished rebuilding AST for module at path", + pathString + ) + ) + } else { + logEvent( + TelemetryEvent("Failed to rebuild AST for module at path", pathString) + ) } + } @JsonRequest("textDocument/definition") def textDocumentDefinition( @@ -328,21 +334,21 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { } else null } - private def populateProgramForPath( - documentPath: String - ) = { + /** + * Builds the AST for the entire program, based on the configured "inputFileNames" + */ + private def populateProgramForPath() = { logEvent( TelemetryEvent("Building program AST") ) val queue = new MosSourceLoadingQueue( - initialFilenames = List(documentPath), + initialFilenames = context.inputFileNames, includePath = context.includePath, options = options ) var program = queue.run() - var moduleName = queue.extractName(documentPath) logEvent( TelemetryEvent("Finished building AST") @@ -353,7 +359,6 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { case (moduleName, program) => cachedModules.put(moduleName, program) } - moduleNames += ((documentPath, moduleName)) program.modulePaths.foreach { case (moduleName, path) => modulePaths.put(moduleName, path) } diff --git a/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala b/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala index 3e869a0b..ca971a7b 100644 --- a/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala +++ b/src/main/scala/millfork/parser/AbstractSourceLoadingQueue.scala @@ -158,6 +158,7 @@ abstract class AbstractSourceLoadingQueue[T](val initialFilenames: List[String], } } + // TODO: Separate from Queue def extractName(i: String): String = { val noExt = i.stripSuffix(extension) val lastSlash = noExt.lastIndexOf('/') max noExt.lastIndexOf('\\') From 40ce1801e53053cf451231cf74471f99b1b48d66 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 31 Oct 2020 20:29:08 -0700 Subject: [PATCH 23/54] Support for finding local variable declarations --- .../scala/millfork/language/NodeFinder.scala | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 26c34360..b0844fc2 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -16,12 +16,18 @@ import millfork.parser.ParsedProgram import scala.collection.mutable import millfork.node.ExpressionStatement import millfork.node.ImportStatement +import millfork.node.ParameterDeclaration +import millfork.env.ByConstant +import millfork.env.ByReference +import millfork.env.ByVariable +import millfork.node.ArrayDeclarationStatement +import millfork.node.AliasDefinitionStatement object NodeFinder { def findDeclarationForUsage( parsedModules: Stream[(String, Program)], node: Node - ): Option[(String, DeclarationStatement)] = { + ): Option[(String, Node)] = { node match { case importStatement: ImportStatement => Some((importStatement.filename, importStatement)) @@ -49,7 +55,7 @@ object NodeFinder { private def matchingDeclarationForExpression( expression: Expression, declarations: List[DeclarationStatement] - ): Option[DeclarationStatement] = + ): Option[Node] = expression match { case FunctionCallExpression(name, expressions) => declarations @@ -57,8 +63,25 @@ object NodeFinder { .find(d => d.name == name) case VariableExpression(name) => declarations - .filter(d => d.isInstanceOf[VariableDeclarationStatement]) - .find(d => d.name == name) + .flatMap(flattenNestedDeclarations) + .find(d => + d match { + case variableDeclaration: VariableDeclarationStatement => + variableDeclaration.name == name + case ParameterDeclaration(typ, assemblyParamPassingConvention) => + assemblyParamPassingConvention match { + case ByConstant(pName) => pName == name + case ByReference(pName) => pName == name + case ByVariable(pName) => pName == name + case default => false + } + case arrayDeclaration: ArrayDeclarationStatement => + arrayDeclaration.name == name + case AliasDefinitionStatement(aName, target, important) => + aName == name + case default => false + } + ) case default => None } @@ -216,6 +239,27 @@ object NodeFinder { case None => -1 } + private def flattenNestedDeclarations( + declaration: DeclarationStatement + ): List[Node] = + declaration match { + case functionDeclaration: FunctionDeclarationStatement => { + List( + functionDeclaration + // Pull statements rather than getAllExpressions, as the variable declarations don't seem to be properly handled otherwise + ) ++ functionDeclaration.params ++ functionDeclaration.statements + .getOrElse(List()) + .filter(e => + e.isInstanceOf[DeclarationStatement] || e + .isInstanceOf[ParameterDeclaration] + ) + } + case default => List(default) + } + + /** + * Returns all of the expressions contained within an expression, including itself + */ private def flattenNestedExpressions(node: Node): List[Node] = node match { case statement: ExpressionStatement => { From e40a8109fc8c8d8b6b82517264b66a3b7a93e398 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 1 Nov 2020 06:50:50 -0800 Subject: [PATCH 24/54] Search enclosing scopes when finding declarations --- .../millfork/language/MfLanguageServer.scala | 30 +++- .../scala/millfork/language/NodeFinder.scala | 137 ++++++++++-------- 2 files changed, 98 insertions(+), 69 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index f9d2a076..9efb31c6 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -375,17 +375,37 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ): Option[(String, Node)] = { val moduleName = moduleNameForPath(documentPath) - val node = NodeFinder.findNodeAtPosition( - moduleName, - cachedModules.toMap, - position + val currentModuleDeclarations = cachedModules.get(moduleName) + + if (currentModuleDeclarations.isEmpty) { + return None + } + + val (node, enclosingDeclarations) = NodeFinder.findNodeAtPosition( + currentModuleDeclarations.get, + position, + (data) => logEvent(TelemetryEvent("Find event", data)) ) if (node.isDefined) { logEvent(TelemetryEvent("Found node at position", node)) + // Build ordered scopes to search through + // First, our current enclosing scope, then the current module (which contains the current scope), then all other modules + val orderedScopes = List( + (moduleName, enclosingDeclarations.get), + (moduleName, currentModuleDeclarations.get.declarations) + ) ++ cachedModules.toList + .filter { + case (cachedModuleName, program) => cachedModuleName != moduleName + } + .map { + case (cachedModuleName, program) => + (cachedModuleName, program.declarations) + } + val usage = - NodeFinder.findDeclarationForUsage(cachedModules.toStream, node.get) + NodeFinder.findDeclarationForUsage(orderedScopes, node.get) if (usage.isDefined) { logEvent(TelemetryEvent("Found original declaration", usage)) diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index b0844fc2..d1dfce4e 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -25,28 +25,26 @@ import millfork.node.AliasDefinitionStatement object NodeFinder { def findDeclarationForUsage( - parsedModules: Stream[(String, Program)], + orderedScopes: List[(String, List[DeclarationStatement])], node: Node ): Option[(String, Node)] = { node match { case importStatement: ImportStatement => Some((importStatement.filename, importStatement)) case expression: Expression => { - val foundDeclaration = parsedModules.toStream - .map { - case (module, program) => { - val declaration = - matchingDeclarationForExpression( - expression, - program.declarations - ) - if (declaration.isDefined) Some((module, declaration.get)) - else None - } + for ((moduleName, scopedDeclarations) <- orderedScopes) { + val declaration = + matchingDeclarationForExpression( + expression, + scopedDeclarations + ) + + if (declaration.isDefined) { + return Some((moduleName, declaration.get)) } - .find(d => d.isDefined) + } - if (foundDeclaration.isDefined) foundDeclaration.get else None + return None } case default => None } @@ -115,81 +113,92 @@ object NodeFinder { }.toList } + /** + * Finds the node and enclosing declaration scope for a given position + * + * @param program The program containing the position + * @param position The position of the node to find + * @return A tuple containing the found node, and a list of enclosing declaration scopes + */ def findNodeAtPosition( - module: String, - parsedModules: Map[String, Program], - position: Position - ): Option[Node] = { + program: Program, + position: Position, + log: (Any) => Unit + ): (Option[Node], Option[List[DeclarationStatement]]) = { val line = position.line val column = position.column - val activeProgram = parsedModules.get(module) - - if (activeProgram.isEmpty) { - return None - } - val declarations = - findDeclarationsAtLine(activeProgram.get.declarations, line) + findClosestDeclarationsAtLine(program.declarations, line) + + log(declarations) if (declarations.isEmpty) { - return None + return (None, None) } - if (lineOrNegOne(declarations.get.head.position) == line) { - // All declarations are current line, find matching node for column - findNodeAtColumn( - declarations.get.flatMap(d => List(d) ++ d.getAllExpressions), - line, - column - ) - } else { + if (lineOrNegOne(declarations.get.head.position) != line) { // Declaration is a function or similar wrapper // Find inner expressions if (declarations.get.length > 1) { throw new Exception("Unexpected number of declarations") } - findNodeAtColumn(declarations.get.head.getAllExpressions, line, column) + return ( + findNodeAtColumn(declarations.get.head.getAllExpressions, line, column), + declarations + ) } + + // All declarations are current line, find matching node for column + ( + findNodeAtColumn( + declarations.get.flatMap(d => List(d) ++ d.getAllExpressions), + line, + column + ), + declarations + ) } - private def findDeclarationsAtLine( + /** + * Finds the narrowest top level declaration scope for the given line (typically enclosing function) + * + * @param declarations All program declarations in the file + * @param line The line to search for + */ + private def findClosestDeclarationsAtLine( declarations: List[DeclarationStatement], line: Int ): Option[List[DeclarationStatement]] = { - var lastDeclarations: Option[List[DeclarationStatement]] = None + var lastDeclarations: Option[List[DeclarationStatement]] = + if (declarations.length > 0) Some(declarations.take(1)) else None for ((nextDeclaration, i) <- declarations.view.zipWithIndex) { - if (lastDeclarations.isEmpty) { - // Populate with first item, no matter what - lastDeclarations = Some(List(nextDeclaration)) - } else { - val nextLine = lineOrNegOne(nextDeclaration.position) - - if (nextLine == line) { - // Declaration is on this line - // Check for additional declarations on this line - val newDeclarations = mutable.MutableList(nextDeclaration) - - for (declarationIndex <- i to declarations.length - 1) { - val checkDeclaration = declarations(declarationIndex) - - if (checkDeclaration.position.isDefined) { - if (checkDeclaration.position.get.line == line) { - newDeclarations += checkDeclaration - } else { - // Line doesn't match, done with this line - return Some(newDeclarations.toList) - } - } + val nextLine = lineOrNegOne(nextDeclaration.position) + + if (nextLine == line) { + // Declaration is on this line + // Check for additional declarations on this line + val newDeclarations = mutable.MutableList(nextDeclaration) + + for (declarationIndex <- i to declarations.length - 1) { + val checkDeclaration = declarations(declarationIndex) + + if ( + checkDeclaration.position.isDefined && checkDeclaration.position.get.line == line + ) { + newDeclarations += checkDeclaration + } else { + // Line doesn't match, done with this line + return Some(newDeclarations.toList) } - - return Some(newDeclarations.toList) - } else if (nextLine < line) { - // Closer to desired line - lastDeclarations = Some(List(nextDeclaration)) } + + return Some(newDeclarations.toList) + } else if (nextLine < line) { + // Closer to desired line + lastDeclarations = Some(List(nextDeclaration)) } } From 0ee33201dc1ee742f78ab805ec838048b95148e8 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 1 Nov 2020 08:33:53 -0800 Subject: [PATCH 25/54] Fixed hover not working on arrays --- .../scala/millfork/language/NodeFinder.scala | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index d1dfce4e..34e6aa62 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -22,6 +22,7 @@ import millfork.env.ByReference import millfork.env.ByVariable import millfork.node.ArrayDeclarationStatement import millfork.node.AliasDefinitionStatement +import millfork.node.IndexedExpression object NodeFinder { def findDeclarationForUsage( @@ -50,6 +51,31 @@ object NodeFinder { } } + private def matchVariableExpressionName( + name: String, + declarations: List[DeclarationStatement] + ) = + declarations + .flatMap(flattenNestedDeclarations) + .find(d => + d match { + case variableDeclaration: VariableDeclarationStatement => + variableDeclaration.name == name + case ParameterDeclaration(typ, assemblyParamPassingConvention) => + assemblyParamPassingConvention match { + case ByConstant(pName) => pName == name + case ByReference(pName) => pName == name + case ByVariable(pName) => pName == name + case default => false + } + case arrayDeclaration: ArrayDeclarationStatement => + arrayDeclaration.name == name + case AliasDefinitionStatement(aName, target, important) => + aName == name + case default => false + } + ) + private def matchingDeclarationForExpression( expression: Expression, declarations: List[DeclarationStatement] @@ -60,26 +86,9 @@ object NodeFinder { .filter(d => d.isInstanceOf[FunctionDeclarationStatement]) .find(d => d.name == name) case VariableExpression(name) => - declarations - .flatMap(flattenNestedDeclarations) - .find(d => - d match { - case variableDeclaration: VariableDeclarationStatement => - variableDeclaration.name == name - case ParameterDeclaration(typ, assemblyParamPassingConvention) => - assemblyParamPassingConvention match { - case ByConstant(pName) => pName == name - case ByReference(pName) => pName == name - case ByVariable(pName) => pName == name - case default => false - } - case arrayDeclaration: ArrayDeclarationStatement => - arrayDeclaration.name == name - case AliasDefinitionStatement(aName, target, important) => - aName == name - case default => false - } - ) + matchVariableExpressionName(name, declarations) + case IndexedExpression(name, index) => + matchVariableExpressionName(name, declarations) case default => None } @@ -129,7 +138,7 @@ object NodeFinder { val column = position.column val declarations = - findClosestDeclarationsAtLine(program.declarations, line) + findEnclosingDeclarationsAtLine(program.declarations, line) log(declarations) @@ -167,7 +176,7 @@ object NodeFinder { * @param declarations All program declarations in the file * @param line The line to search for */ - private def findClosestDeclarationsAtLine( + private def findEnclosingDeclarationsAtLine( declarations: List[DeclarationStatement], line: Int ): Option[List[DeclarationStatement]] = { From 26f196b3ffbf60244128c96227cde24a3b953b16 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 1 Nov 2020 08:38:08 -0800 Subject: [PATCH 26/54] Fixed GoToDef on first line/column of file --- src/main/scala/millfork/language/MfLanguageServer.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 9efb31c6..59523b17 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -420,7 +420,11 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { private def mfPositionToLSP4j( position: Position ): org.eclipse.lsp4j.Position = - new org.eclipse.lsp4j.Position(position.line - 1, position.column - 1) + new org.eclipse.lsp4j.Position( + position.line - 1, + // If subtracting 1 would be < 0, set to 0 + if (position.column < 1) 0 else position.column - 1 + ) private def logEvent(event: TelemetryEvent) = { val languageClient = client.getOrElse { From 1cf1c29b6dc965c868f7ebedada956bc003a3486 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 1 Nov 2020 15:15:04 -0800 Subject: [PATCH 27/54] Added first LSP test --- .../millfork/language/MfLanguageServer.scala | 11 ++-- .../test/language/MfLanguageServerSuite.scala | 63 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 src/test/scala/millfork/test/language/MfLanguageServerSuite.scala diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 59523b17..e4280630 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -104,10 +104,10 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { params: DidOpenTextDocumentParams ): CompletableFuture[Unit] = CompletableFuture.completedFuture { - val pathString = trimDocumentUri(params.getTextDocument().getUri()) + val textDocument = params.getTextDocument() + val pathString = trimDocumentUri(textDocument.getUri()) - val documentText = - params.getTextDocument().getText().split("\n").toSeq + val documentText = textDocument.getText().split("\n").toSeq rebuildASTForFile(pathString, documentText) } @@ -426,9 +426,10 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { if (position.column < 1) 0 else position.column - 1 ) - private def logEvent(event: TelemetryEvent) = { + private def logEvent(event: TelemetryEvent): Unit = { val languageClient = client.getOrElse { - throw new Exception("Language client not registered") + // Language client not registered + return } implicit val formats = Serialization.formats(NoTypeHints) diff --git a/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala b/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala new file mode 100644 index 00000000..2724c419 --- /dev/null +++ b/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala @@ -0,0 +1,63 @@ +package millfork.test.language + +import millfork.error.Logger +import millfork.{NullLogger, Platform} +import org.scalatest.{AppendedClues, FunSuite, Matchers} +import millfork.language.MfLanguageServer +import millfork.Context +import millfork.CompilationOptions +import millfork.test.emu.{EmuPlatform, TestErrorReporting} +import millfork.Cpu +import millfork.JobContext +import millfork.compiler.LabelGenerator +import org.eclipse.lsp4j.DidOpenTextDocumentParams +import org.eclipse.lsp4j.TextDocumentItem +import org.eclipse.lsp4j.HoverParams +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.Position +import millfork.language.MfLanguageClient + +/** + * @author Karol Stasiak + */ +class MfLanguageServerSuite extends FunSuite with Matchers with AppendedClues { + test("hover should find node under cursor, and its root declaration") { + implicit val logger: Logger = new NullLogger() + val platform = EmuPlatform.get(Cpu.Mos) + val jobContext = JobContext(TestErrorReporting.log, new LabelGenerator) + val server = new MfLanguageServer( + new Context(logger, List()), + new CompilationOptions( + platform, + Map(), + None, + 0, + Map(), + EmuPlatform.textCodecRepository, + jobContext + ) + ) + + val textDocument = new TextDocumentItem("file.mfk", "millfork", 1, """ + | byte test + | void main() { + | test = test + 1 + | } + """.stripMargin) + val openParams = new DidOpenTextDocumentParams(textDocument) + server.textDocumentDidOpen(openParams) + + val hoverParams = new HoverParams( + new TextDocumentIdentifier("file.mfk"), + new Position(3, 3) + ) + val response = server.textDocumentHover(hoverParams) + + val hover = response.get + + val contents = hover.getContents().getRight() + contents should not equal (null) + contents.getValue() should equal("```mfk\nbyte test\n```") + } + +} From fd0ccfe8db2f2b490394d8514a7c9452f44160b7 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 1 Nov 2020 15:36:28 -0800 Subject: [PATCH 28/54] Expanded initial hover tests --- .scalafmt.conf | 1 + .../test/language/MfLanguageServerSuite.scala | 89 +++++++++++-------- .../test/language/util/LanguageHelper.scala | 45 ++++++++++ 3 files changed, 97 insertions(+), 38 deletions(-) create mode 100644 src/test/scala/millfork/test/language/util/LanguageHelper.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index ba14fb70..2ead7fcf 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1 +1,2 @@ version = "2.6.4" +importSelectors = binPack \ No newline at end of file diff --git a/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala b/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala index 2724c419..737c9371 100644 --- a/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala +++ b/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala @@ -1,63 +1,76 @@ package millfork.test.language -import millfork.error.Logger -import millfork.{NullLogger, Platform} import org.scalatest.{AppendedClues, FunSuite, Matchers} -import millfork.language.MfLanguageServer -import millfork.Context -import millfork.CompilationOptions -import millfork.test.emu.{EmuPlatform, TestErrorReporting} -import millfork.Cpu -import millfork.JobContext -import millfork.compiler.LabelGenerator +import millfork.test.language.util._ import org.eclipse.lsp4j.DidOpenTextDocumentParams import org.eclipse.lsp4j.TextDocumentItem import org.eclipse.lsp4j.HoverParams import org.eclipse.lsp4j.TextDocumentIdentifier import org.eclipse.lsp4j.Position -import millfork.language.MfLanguageClient /** * @author Karol Stasiak */ class MfLanguageServerSuite extends FunSuite with Matchers with AppendedClues { test("hover should find node under cursor, and its root declaration") { - implicit val logger: Logger = new NullLogger() - val platform = EmuPlatform.get(Cpu.Mos) - val jobContext = JobContext(TestErrorReporting.log, new LabelGenerator) - val server = new MfLanguageServer( - new Context(logger, List()), - new CompilationOptions( - platform, - Map(), - None, - 0, - Map(), - EmuPlatform.textCodecRepository, - jobContext - ) - ) + val server = LanguageHelper.createServer - val textDocument = new TextDocumentItem("file.mfk", "millfork", 1, """ + LanguageHelper.openDocument( + server, + "file.mfk", + """ | byte test + | array(byte) foo[4] | void main() { | test = test + 1 + | foo[1] = test | } - """.stripMargin) - val openParams = new DidOpenTextDocumentParams(textDocument) - server.textDocumentDidOpen(openParams) - - val hoverParams = new HoverParams( - new TextDocumentIdentifier("file.mfk"), - new Position(3, 3) + """ ) - val response = server.textDocumentHover(hoverParams) - val hover = response.get + { + // Select `test` variable usage + val hoverParams = new HoverParams( + new TextDocumentIdentifier("file.mfk"), + new Position(4, 3) + ) + val response = server.textDocumentHover(hoverParams) + val hover = response.get + + val contents = hover.getContents().getRight() + contents should not equal (null) + contents.getValue() should equal(LanguageHelper.formatHover("byte test")) + } + { + // Select `main` function + val hoverParams = new HoverParams( + new TextDocumentIdentifier("file.mfk"), + new Position(3, 3) + ) + val response = server.textDocumentHover(hoverParams) + val hover = response.get - val contents = hover.getContents().getRight() - contents should not equal (null) - contents.getValue() should equal("```mfk\nbyte test\n```") + val contents = hover.getContents().getRight() + contents should not equal (null) + contents.getValue() should equal( + LanguageHelper.formatHover("void main()") + ) + } + { + // Select `foo` array usage + val hoverParams = new HoverParams( + new TextDocumentIdentifier("file.mfk"), + new Position(5, 6) + ) + val response = server.textDocumentHover(hoverParams) + val hover = response.get + + val contents = hover.getContents().getRight() + contents should not equal (null) + contents.getValue() should equal( + LanguageHelper.formatHover("array(byte) foo [4]") + ) + } } } diff --git a/src/test/scala/millfork/test/language/util/LanguageHelper.scala b/src/test/scala/millfork/test/language/util/LanguageHelper.scala new file mode 100644 index 00000000..f981dc98 --- /dev/null +++ b/src/test/scala/millfork/test/language/util/LanguageHelper.scala @@ -0,0 +1,45 @@ +package millfork.test.language.util + +import millfork.error.Logger +import millfork.{NullLogger, Platform} +import millfork.language.MfLanguageServer +import millfork.Context +import millfork.CompilationOptions +import millfork.test.emu.{EmuPlatform, TestErrorReporting} +import millfork.Cpu +import millfork.JobContext +import millfork.compiler.LabelGenerator +import org.eclipse.lsp4j.DidOpenTextDocumentParams +import org.eclipse.lsp4j.TextDocumentItem +import org.eclipse.lsp4j.HoverParams +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.Position + +object LanguageHelper { + def createServer(): MfLanguageServer = { + implicit val logger: Logger = new NullLogger() + val platform = EmuPlatform.get(Cpu.Mos) + val jobContext = JobContext(TestErrorReporting.log, new LabelGenerator) + new MfLanguageServer( + new Context(logger, List()), + new CompilationOptions( + platform, + Map(), + None, + 0, + Map(), + EmuPlatform.textCodecRepository, + jobContext + ) + ) + } + + def openDocument(server: MfLanguageServer, name: String, text: String) = { + val textDocument = + new TextDocumentItem(name, "millfork", 1, text.stripMargin) + val openParams = new DidOpenTextDocumentParams(textDocument) + server.textDocumentDidOpen(openParams) + } + + def formatHover(text: String): String = s"""```mfk\n${text}\n```""" +} From 0951f49fa209b2d17b8c9dddf7347e87cb17881c Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 1 Nov 2020 18:27:33 -0800 Subject: [PATCH 29/54] Test checking every character of file for hoverability --- .../test/language/MfLanguageServerSuite.scala | 166 +++++++++++++----- 1 file changed, 118 insertions(+), 48 deletions(-) diff --git a/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala b/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala index 737c9371..79aa4929 100644 --- a/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala +++ b/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala @@ -1,24 +1,27 @@ package millfork.test.language -import org.scalatest.{AppendedClues, FunSuite, Matchers} +import org.scalatest.{AppendedClues, FunSpec, Matchers} import millfork.test.language.util._ import org.eclipse.lsp4j.DidOpenTextDocumentParams import org.eclipse.lsp4j.TextDocumentItem import org.eclipse.lsp4j.HoverParams import org.eclipse.lsp4j.TextDocumentIdentifier import org.eclipse.lsp4j.Position +import java.util.regex.Pattern +import scala.collection.mutable /** * @author Karol Stasiak */ -class MfLanguageServerSuite extends FunSuite with Matchers with AppendedClues { - test("hover should find node under cursor, and its root declaration") { - val server = LanguageHelper.createServer - - LanguageHelper.openDocument( - server, - "file.mfk", - """ +class MfLanguageServerSuite extends FunSpec with Matchers with AppendedClues { + describe("hover") { + it("should find node under cursor, and its root declaration") { + val server = LanguageHelper.createServer + + LanguageHelper.openDocument( + server, + "file.mfk", + """ | byte test | array(byte) foo[4] | void main() { @@ -26,51 +29,118 @@ class MfLanguageServerSuite extends FunSuite with Matchers with AppendedClues { | foo[1] = test | } """ - ) - - { - // Select `test` variable usage - val hoverParams = new HoverParams( - new TextDocumentIdentifier("file.mfk"), - new Position(4, 3) ) - val response = server.textDocumentHover(hoverParams) - val hover = response.get - val contents = hover.getContents().getRight() - contents should not equal (null) - contents.getValue() should equal(LanguageHelper.formatHover("byte test")) - } - { - // Select `main` function - val hoverParams = new HoverParams( - new TextDocumentIdentifier("file.mfk"), - new Position(3, 3) - ) - val response = server.textDocumentHover(hoverParams) - val hover = response.get + { + // Select `test` variable usage + val hoverParams = new HoverParams( + new TextDocumentIdentifier("file.mfk"), + new Position(4, 3) + ) + val response = server.textDocumentHover(hoverParams) + val hover = response.get - val contents = hover.getContents().getRight() - contents should not equal (null) - contents.getValue() should equal( - LanguageHelper.formatHover("void main()") - ) + val contents = hover.getContents().getRight() + contents should not equal (null) + contents.getValue() should equal( + LanguageHelper.formatHover("byte test") + ) + } + { + // Select `main` function + val hoverParams = new HoverParams( + new TextDocumentIdentifier("file.mfk"), + new Position(3, 3) + ) + val response = server.textDocumentHover(hoverParams) + val hover = response.get + + val contents = hover.getContents().getRight() + contents should not equal (null) + contents.getValue() should equal( + LanguageHelper.formatHover("void main()") + ) + } + { + // Select `foo` array usage + val hoverParams = new HoverParams( + new TextDocumentIdentifier("file.mfk"), + new Position(5, 6) + ) + val response = server.textDocumentHover(hoverParams) + val hover = response.get + + val contents = hover.getContents().getRight() + contents should not equal (null) + contents.getValue() should equal( + LanguageHelper.formatHover("array(byte) foo [4]") + ) + } } - { - // Select `foo` array usage - val hoverParams = new HoverParams( - new TextDocumentIdentifier("file.mfk"), - new Position(5, 6) - ) - val response = server.textDocumentHover(hoverParams) - val hover = response.get - val contents = hover.getContents().getRight() - contents should not equal (null) - contents.getValue() should equal( - LanguageHelper.formatHover("array(byte) foo [4]") + describe("should always produce value") { + val server = LanguageHelper.createServer + + val text = """ + | byte test + | array(byte) foo[4] + | void main() { + | test += test + | foo[1] = test + | func() + | } + | byte func() { + | byte i + | byte innerValue + | innerValue = 2 + | innerValue += innerValue + | return innerValue + | } + """.stripMargin + + LanguageHelper.openDocument( + server, + "file.mfk", + text ) + + val lines = text.split("\n") + + val pattern = Pattern.compile("(return|byte)") + + for ((line, i) <- lines.zipWithIndex) { + val matcher = pattern.matcher(line) + + val ignoreRanges = mutable.MutableList[Range]() + + while (matcher.find()) { + ignoreRanges += Range(matcher.start(), matcher.end()) + } + + for ((character, column) <- line.toCharArray().zipWithIndex) { + if ( + Character.isLetter(character) && + // Ignore sections of string matching pattern + ignoreRanges.filter(r => r.contains(column)).length == 0 + ) { + it(s"""should work on ${i}, ${column} contents "${line}" """) { + val hoverParams = new HoverParams( + new TextDocumentIdentifier("file.mfk"), + new Position(i, column + 2) + ) + val response = server.textDocumentHover(hoverParams) + val hover = response.get + + hover should not equal (null) + + val contents = hover.getContents().getRight() + info(contents.toString()) + contents should not equal (null) + } + } + } + } } - } + } } From 23af333b6597df6abbedcc2cb1b917afa5a84460 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 1 Nov 2020 19:28:38 -0800 Subject: [PATCH 30/54] Added basic NodeFinder nodeAtPosition tests --- .scalafmt.conf | 1 - .../millfork/language/MfLanguageServer.scala | 5 +- .../scala/millfork/language/NodeFinder.scala | 5 +- .../test/language/MfLanguageServerSuite.scala | 3 - .../test/language/NodeFinderSuite.scala | 189 ++++++++++++++++++ 5 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 src/test/scala/millfork/test/language/NodeFinderSuite.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index 2ead7fcf..ba14fb70 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,2 +1 @@ version = "2.6.4" -importSelectors = binPack \ No newline at end of file diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index e4280630..ebcd9307 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -56,7 +56,7 @@ import scala.collection.JavaConverters._ class MfLanguageServer(context: Context, options: CompilationOptions) { var client: Option[MfLanguageClient] = None - private val cachedModules: mutable.Map[String, Program] = mutable.Map() + val cachedModules: mutable.Map[String, Program] = mutable.Map() private var cachedProgram: Option[ParsedProgram] = None private val moduleNames: mutable.Map[String, String] = mutable.Map() private val modulePaths: mutable.Map[String, Path] = mutable.Map() @@ -383,8 +383,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { val (node, enclosingDeclarations) = NodeFinder.findNodeAtPosition( currentModuleDeclarations.get, - position, - (data) => logEvent(TelemetryEvent("Find event", data)) + position ) if (node.isDefined) { diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 34e6aa62..7400dde8 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -131,8 +131,7 @@ object NodeFinder { */ def findNodeAtPosition( program: Program, - position: Position, - log: (Any) => Unit + position: Position ): (Option[Node], Option[List[DeclarationStatement]]) = { val line = position.line val column = position.column @@ -140,8 +139,6 @@ object NodeFinder { val declarations = findEnclosingDeclarationsAtLine(program.declarations, line) - log(declarations) - if (declarations.isEmpty) { return (None, None) } diff --git a/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala b/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala index 79aa4929..f39471a9 100644 --- a/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala +++ b/src/test/scala/millfork/test/language/MfLanguageServerSuite.scala @@ -10,9 +10,6 @@ import org.eclipse.lsp4j.Position import java.util.regex.Pattern import scala.collection.mutable -/** - * @author Karol Stasiak - */ class MfLanguageServerSuite extends FunSpec with Matchers with AppendedClues { describe("hover") { it("should find node under cursor, and its root declaration") { diff --git a/src/test/scala/millfork/test/language/NodeFinderSuite.scala b/src/test/scala/millfork/test/language/NodeFinderSuite.scala new file mode 100644 index 00000000..2ca83539 --- /dev/null +++ b/src/test/scala/millfork/test/language/NodeFinderSuite.scala @@ -0,0 +1,189 @@ +package millfork.test.language + +import org.scalatest.{AppendedClues, FunSpec, Matchers} +import millfork.test.language.util._ +import org.eclipse.lsp4j.DidOpenTextDocumentParams +import org.eclipse.lsp4j.TextDocumentItem +import org.eclipse.lsp4j.HoverParams +import org.eclipse.lsp4j.TextDocumentIdentifier +import java.util.regex.Pattern +import millfork.language.NodeFinder +import millfork.node.Position +import millfork.node.FunctionDeclarationStatement +import millfork.node.ExpressionStatement +import millfork.node.FunctionCallExpression +import millfork.node.Assignment + +class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { + describe("nodeAtPosition") { + val text = """ + | byte test + | array(byte) foo[4] + | void main() { + | test += test + | foo[1] = test + | func() + | } + | byte func() { + | byte i + | byte innerValue + | innerValue = 2 + | innerValue += innerValue + | return innerValue + | } + """.stripMargin + + val server = LanguageHelper.createServer + + LanguageHelper + .openDocument( + server, + "file.mfk", + text + ) + + val program = server.cachedModules.get("file").get + + it("should find root variable declarations") { + val startIndex = 7 + val length = 4 + + for (column <- startIndex to startIndex + length) { + NodeFinder + .findNodeAtPosition(program, Position("", 2, column, 0)) + ._2 + .get(0) should equal( + program.declarations(0) + ) + } + } + + it("should find root array declarations") { + val startIndex = 13 + val length = 16 + + for (column <- startIndex to startIndex + length) { + NodeFinder + .findNodeAtPosition(program, Position("", 3, column, 0)) + ._2 + .get(0) should equal( + program.declarations(1) + ) + } + } + + it("should find function declarations") { + val startIndex = 7 + val length = 4 + + for (column <- startIndex to startIndex + length) { + NodeFinder + .findNodeAtPosition(program, Position("", 4, column, 0)) + ._2 + .get(0) should equal( + program.declarations(2) + ) + } + } + + it("should find variable expression within function") { + val startIndex = 4 + val length = 4 + + for (column <- startIndex to startIndex + length) { + NodeFinder + .findNodeAtPosition(program, Position("", 5, column, 0)) + ._1 + .get should equal( + program + .declarations(2) + .asInstanceOf[FunctionDeclarationStatement] + .statements + .get(0) + .asInstanceOf[ExpressionStatement] + .expression + .asInstanceOf[FunctionCallExpression] + .expressions(0) + ) + } + } + + it("should find array expression within function") { + val startIndex = 4 + val length = 3 + + for (column <- startIndex to startIndex + length) { + NodeFinder + .findNodeAtPosition(program, Position("", 6, column, 0)) + ._1 + .get should equal( + program + .declarations(2) + .asInstanceOf[FunctionDeclarationStatement] + .statements + .get(1) + .asInstanceOf[Assignment] + .destination + ) + } + } + + it("should find right hand side of assignment") { + val startIndex = 13 + val length = 4 + + for (column <- startIndex to startIndex + length) { + NodeFinder + .findNodeAtPosition(program, Position("", 6, column, 0)) + ._1 + .get should equal( + program + .declarations(2) + .asInstanceOf[FunctionDeclarationStatement] + .statements + .get(1) + .asInstanceOf[Assignment] + .source + ) + } + } + + it("should find function call") { + val startIndex = 4 + val length = 6 + + for (column <- startIndex to startIndex + length) { + NodeFinder + .findNodeAtPosition(program, Position("", 7, column, 0)) + ._1 + .get should equal( + program + .declarations(2) + .asInstanceOf[FunctionDeclarationStatement] + .statements + .get(2) + .asInstanceOf[ExpressionStatement] + .expression + ) + } + } + + it("should find function nested variable declarations") { + val startIndex = 9 + val length = 1 + + for (column <- startIndex to startIndex + length) { + NodeFinder + .findNodeAtPosition(program, Position("", 10, column, 0)) + ._1 + .get should equal( + program + .declarations(3) + .asInstanceOf[FunctionDeclarationStatement] + .statements + .get(0) + ) + } + } + } +} From c3f8515fd19ee2bf5ed98026756949da78f2b804 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 2 Nov 2020 17:12:18 -0800 Subject: [PATCH 31/54] Improved findNodeAtPosition to consider the entire flattened tree --- .../scala/millfork/language/NodeFinder.scala | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 7400dde8..65594611 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -23,6 +23,7 @@ import millfork.env.ByVariable import millfork.node.ArrayDeclarationStatement import millfork.node.AliasDefinitionStatement import millfork.node.IndexedExpression +import millfork.node.Statement object NodeFinder { def findDeclarationForUsage( @@ -151,7 +152,11 @@ object NodeFinder { } return ( - findNodeAtColumn(declarations.get.head.getAllExpressions, line, column), + findNodeAtColumn( + recursivelyFlatten(declarations.get.head), + line, + column + ), declarations ) } @@ -159,7 +164,8 @@ object NodeFinder { // All declarations are current line, find matching node for column ( findNodeAtColumn( - declarations.get.flatMap(d => List(d) ++ d.getAllExpressions), + declarations.get + .flatMap(recursivelyFlatten), line, column ), @@ -254,6 +260,30 @@ object NodeFinder { case None => -1 } + /** + * Recursively flattens a node tree into a tree containing the node itself and all of its "owned" nodes. + * In particular, function declarations don't call `getAllExpressions`, instead opting to manually pull out + * `params` and `statements` to allow for proper access + * + * @param node The root of the tree to process + * @return A flatten list of all nodes + */ + private def recursivelyFlatten(node: Node): List[Node] = + node match { + case functionDeclaration: FunctionDeclarationStatement => + List( + functionDeclaration + ) ++ functionDeclaration.params ++ functionDeclaration.statements + .getOrElse(List()) + .flatMap(recursivelyFlatten) + case statement: Statement => + List(statement) ++ statement.getAllExpressions.flatMap( + recursivelyFlatten + ) + case expression: Expression => List(expression) + case default => List(default) + } + private def flattenNestedDeclarations( declaration: DeclarationStatement ): List[Node] = From 097859df242ccc2185c6e54a9aa7b21593d9209f Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 2 Nov 2020 17:20:16 -0800 Subject: [PATCH 32/54] Added test for finding function arguments --- .../test/language/NodeFinderSuite.scala | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/test/scala/millfork/test/language/NodeFinderSuite.scala b/src/test/scala/millfork/test/language/NodeFinderSuite.scala index 2ca83539..d3a80bfb 100644 --- a/src/test/scala/millfork/test/language/NodeFinderSuite.scala +++ b/src/test/scala/millfork/test/language/NodeFinderSuite.scala @@ -24,11 +24,12 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { | foo[1] = test | func() | } - | byte func() { + | byte func(byte arg) { | byte i | byte innerValue | innerValue = 2 | innerValue += innerValue + | innerValue += arg | return innerValue | } """.stripMargin @@ -45,6 +46,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { val program = server.cachedModules.get("file").get it("should find root variable declarations") { + // byte test val startIndex = 7 val length = 4 @@ -59,6 +61,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find root array declarations") { + // array(byte) foo[4] val startIndex = 13 val length = 16 @@ -73,6 +76,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find function declarations") { + // void main() val startIndex = 7 val length = 4 @@ -87,6 +91,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find variable expression within function") { + // test val startIndex = 4 val length = 4 @@ -109,6 +114,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find array expression within function") { + // foo[1] val startIndex = 4 val length = 3 @@ -129,6 +135,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find right hand side of assignment") { + // test val startIndex = 13 val length = 4 @@ -149,8 +156,9 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find function call") { + // func() val startIndex = 4 - val length = 6 + val length = 5 for (column <- startIndex to startIndex + length) { NodeFinder @@ -168,7 +176,26 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } } + it("should find function argument") { + // byte arg + val startIndex = 13 + val length = 8 + + for (column <- startIndex to startIndex + length) { + NodeFinder + .findNodeAtPosition(program, Position("", 9, column, 0)) + ._1 + .get should equal( + program + .declarations(3) + .asInstanceOf[FunctionDeclarationStatement] + .params(0) + ) + } + } + it("should find function nested variable declarations") { + // byte i val startIndex = 9 val length = 1 From eb274f8a50228607dbea63aea8089e3806de46ea Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 2 Nov 2020 17:57:46 -0800 Subject: [PATCH 33/54] Documented NodeFinder functions --- .../scala/millfork/language/NodeFinder.scala | 115 ++++++++++-------- 1 file changed, 63 insertions(+), 52 deletions(-) diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 65594611..105cb60a 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -26,6 +26,16 @@ import millfork.node.IndexedExpression import millfork.node.Statement object NodeFinder { + + /** + * Finds the declaration matching the provided node + * + * @param orderedScopes A list, ordered by decreasing scope (function, local module, global module), + * of tuples containing the module name and all declarations contained wherein + * @param node The node to find the source declaration for + * @return A tuple containing the module name and "declaration" (could be `ParameterDeclaration`, hence + * the `Node` type); `None` otherwise + */ def findDeclarationForUsage( orderedScopes: List[(String, List[DeclarationStatement])], node: Node @@ -52,12 +62,54 @@ object NodeFinder { } } + /** + * Searches for the declaration matching the type and name of the provided expression + * + * @param expression The expression to find the root declaration for + * @param declarations The declarations to check + * @return The matching declaration if found; `None` otherwise + */ + private def matchingDeclarationForExpression( + expression: Expression, + declarations: List[DeclarationStatement] + ): Option[Node] = + expression match { + case FunctionCallExpression(name, expressions) => + declarations + .filter(d => d.isInstanceOf[FunctionDeclarationStatement]) + .find(d => d.name == name) + case VariableExpression(name) => + matchVariableExpressionName(name, declarations) + case IndexedExpression(name, index) => + matchVariableExpressionName(name, declarations) + case default => None + } + + /** + * Searches for the declaration matching a variable name + * + * @param name The name of the variable + * @param declarations The declarations to check + * @return The matching declaration if found; `None` otherwise + */ private def matchVariableExpressionName( name: String, declarations: List[DeclarationStatement] ) = declarations - .flatMap(flattenNestedDeclarations) + .flatMap(d => + d match { + // Extract nested declarations (and `ParameterDeclaration`s, which do not extend `DeclarationStatement`) + // from functions + case functionDeclaration: FunctionDeclarationStatement => + recursivelyFlatten(functionDeclaration) + .filter(e => + e.isInstanceOf[DeclarationStatement] || e + .isInstanceOf[ParameterDeclaration] + ) + case default => List(default) + } + ) .find(d => d match { case variableDeclaration: VariableDeclarationStatement => @@ -77,22 +129,13 @@ object NodeFinder { } ) - private def matchingDeclarationForExpression( - expression: Expression, - declarations: List[DeclarationStatement] - ): Option[Node] = - expression match { - case FunctionCallExpression(name, expressions) => - declarations - .filter(d => d.isInstanceOf[FunctionDeclarationStatement]) - .find(d => d.name == name) - case VariableExpression(name) => - matchVariableExpressionName(name, declarations) - case IndexedExpression(name, index) => - matchVariableExpressionName(name, declarations) - case default => None - } - + /** + * Finds all expressions referencing a declaration + * + * @param parsedModules All program modules + * @param declaration The declaration to find all references for + * @return A list of tuples, containing the module name and the corresponding expression + */ def matchingExpressionsForDeclaration( parsedModules: Stream[(String, Program)], declaration: DeclarationStatement @@ -217,6 +260,9 @@ object NodeFinder { lastDeclarations } + /** + * Searches for closest column index less than the selected column on a given line + */ private def findNodeAtColumn( nodes: List[Node], line: Int, @@ -284,24 +330,6 @@ object NodeFinder { case default => List(default) } - private def flattenNestedDeclarations( - declaration: DeclarationStatement - ): List[Node] = - declaration match { - case functionDeclaration: FunctionDeclarationStatement => { - List( - functionDeclaration - // Pull statements rather than getAllExpressions, as the variable declarations don't seem to be properly handled otherwise - ) ++ functionDeclaration.params ++ functionDeclaration.statements - .getOrElse(List()) - .filter(e => - e.isInstanceOf[DeclarationStatement] || e - .isInstanceOf[ParameterDeclaration] - ) - } - case default => List(default) - } - /** * Returns all of the expressions contained within an expression, including itself */ @@ -320,21 +348,4 @@ object NodeFinder { .flatMap(flattenNestedExpressions) case default => List(default) } - - private def sortNodes(nodes: List[Node]) = - nodes.sortWith((a, b) => { - if (a.position.isEmpty && b.position.isEmpty) { - false - } else if (a.position.isEmpty) { - true - } else if (b.position.isEmpty) { - false - } else { - val aPos = a.position.get - val bPos = b.position.get - - // aPos.line < bPos.line && aPos.column < bPos.column - aPos.column < bPos.column - } - }) } From 751138dda21641b49922af6a53446873e088fd3b Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 2 Nov 2020 19:24:58 -0800 Subject: [PATCH 34/54] More dynamic mechanism for testing NodeFinder --- .../test/language/NodeFinderSuite.scala | 95 ++++++++++--------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/src/test/scala/millfork/test/language/NodeFinderSuite.scala b/src/test/scala/millfork/test/language/NodeFinderSuite.scala index d3a80bfb..a47f8533 100644 --- a/src/test/scala/millfork/test/language/NodeFinderSuite.scala +++ b/src/test/scala/millfork/test/language/NodeFinderSuite.scala @@ -6,17 +6,18 @@ import org.eclipse.lsp4j.DidOpenTextDocumentParams import org.eclipse.lsp4j.TextDocumentItem import org.eclipse.lsp4j.HoverParams import org.eclipse.lsp4j.TextDocumentIdentifier -import java.util.regex.Pattern import millfork.language.NodeFinder import millfork.node.Position import millfork.node.FunctionDeclarationStatement import millfork.node.ExpressionStatement import millfork.node.FunctionCallExpression import millfork.node.Assignment +import java.util.regex.Pattern class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { describe("nodeAtPosition") { val text = """ + | | byte test | array(byte) foo[4] | void main() { @@ -45,14 +46,32 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { val program = server.cachedModules.get("file").get + def findRangeOfString( + textMatch: String, + afterLine: Int = 0 + ): (Int, Range) = { + val pattern = Pattern.compile(s"(${Pattern.quote(textMatch)})") + + val lines = text.split("\n") + for ((line, i) <- lines.zipWithIndex) { + if (i >= afterLine) { + val matcher = pattern.matcher(line) + + if (matcher.find()) { + return (i + 1, Range(matcher.start() + 2, matcher.end() + 2)) + } + } + } + + throw new Error(s"Cound not find pattern ${textMatch}") + } + it("should find root variable declarations") { - // byte test - val startIndex = 7 - val length = 4 + val (line, range) = findRangeOfString("test") - for (column <- startIndex to startIndex + length) { + for (column <- range) { NodeFinder - .findNodeAtPosition(program, Position("", 2, column, 0)) + .findNodeAtPosition(program, Position("", line, column, 0)) ._2 .get(0) should equal( program.declarations(0) @@ -61,13 +80,11 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find root array declarations") { - // array(byte) foo[4] - val startIndex = 13 - val length = 16 + val (line, range) = findRangeOfString("foo[4]") - for (column <- startIndex to startIndex + length) { + for (column <- range) { NodeFinder - .findNodeAtPosition(program, Position("", 3, column, 0)) + .findNodeAtPosition(program, Position("", line, column, 0)) ._2 .get(0) should equal( program.declarations(1) @@ -76,13 +93,11 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find function declarations") { - // void main() - val startIndex = 7 - val length = 4 + val (line, range) = findRangeOfString("main()") - for (column <- startIndex to startIndex + length) { + for (column <- range) { NodeFinder - .findNodeAtPosition(program, Position("", 4, column, 0)) + .findNodeAtPosition(program, Position("", line, column, 0)) ._2 .get(0) should equal( program.declarations(2) @@ -91,13 +106,11 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find variable expression within function") { - // test - val startIndex = 4 - val length = 4 + val (line, range) = findRangeOfString("test", 4) - for (column <- startIndex to startIndex + length) { + for (column <- range) { NodeFinder - .findNodeAtPosition(program, Position("", 5, column, 0)) + .findNodeAtPosition(program, Position("", line, column, 0)) ._1 .get should equal( program @@ -114,13 +127,11 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find array expression within function") { - // foo[1] - val startIndex = 4 - val length = 3 + val (line, range) = findRangeOfString("foo", 4) - for (column <- startIndex to startIndex + length) { + for (column <- range) { NodeFinder - .findNodeAtPosition(program, Position("", 6, column, 0)) + .findNodeAtPosition(program, Position("", line, column, 0)) ._1 .get should equal( program @@ -135,13 +146,11 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find right hand side of assignment") { - // test - val startIndex = 13 - val length = 4 + val (line, range) = findRangeOfString("test", 5) - for (column <- startIndex to startIndex + length) { + for (column <- range) { NodeFinder - .findNodeAtPosition(program, Position("", 6, column, 0)) + .findNodeAtPosition(program, Position("", line, column, 0)) ._1 .get should equal( program @@ -156,13 +165,11 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find function call") { - // func() - val startIndex = 4 - val length = 5 + val (line, range) = findRangeOfString("func()") - for (column <- startIndex to startIndex + length) { + for (column <- range) { NodeFinder - .findNodeAtPosition(program, Position("", 7, column, 0)) + .findNodeAtPosition(program, Position("", line, column, 0)) ._1 .get should equal( program @@ -177,13 +184,11 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find function argument") { - // byte arg - val startIndex = 13 - val length = 8 + val (line, range) = findRangeOfString("arg") - for (column <- startIndex to startIndex + length) { + for (column <- range) { NodeFinder - .findNodeAtPosition(program, Position("", 9, column, 0)) + .findNodeAtPosition(program, Position("", line, column, 0)) ._1 .get should equal( program @@ -195,13 +200,11 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find function nested variable declarations") { - // byte i - val startIndex = 9 - val length = 1 + val (line, range) = findRangeOfString("i", 7) - for (column <- startIndex to startIndex + length) { + for (column <- range) { NodeFinder - .findNodeAtPosition(program, Position("", 10, column, 0)) + .findNodeAtPosition(program, Position("", line, column, 0)) ._1 .get should equal( program From 6967158bfec827fbc4074dc444c25d39a78ffb55 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 2 Nov 2020 19:50:18 -0800 Subject: [PATCH 35/54] Documented future test cases and unimplemented functionality --- .../scala/millfork/test/language/NodeFinderSuite.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/scala/millfork/test/language/NodeFinderSuite.scala b/src/test/scala/millfork/test/language/NodeFinderSuite.scala index a47f8533..6fd6dcbc 100644 --- a/src/test/scala/millfork/test/language/NodeFinderSuite.scala +++ b/src/test/scala/millfork/test/language/NodeFinderSuite.scala @@ -215,5 +215,15 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { ) } } + + // TODO: Additional tests: + // Hover on variable-based array indexing: array[index] + // Fields on array indexing: spawn_info[index].hi + // Struct type: Player player + // Struct fields: player1.pos + // Messed up hover positions (in `nes_reset_joy.mfk`, each variable assignment to 0) + // Alias references + // Math expressions: limit = new_row2 - new_row1 + // Pointers: obj_ptr->xvel } } From b78c7bdc75c302d617d351b0964894fab1050175 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Tue, 3 Nov 2020 20:21:12 -0800 Subject: [PATCH 36/54] Indexed expression support --- .../scala/millfork/language/NodeFinder.scala | 8 ++ .../test/language/NodeFinderSuite.scala | 108 ++++++++++++++---- 2 files changed, 96 insertions(+), 20 deletions(-) diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 105cb60a..385275f6 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -24,6 +24,7 @@ import millfork.node.ArrayDeclarationStatement import millfork.node.AliasDefinitionStatement import millfork.node.IndexedExpression import millfork.node.Statement +import millfork.node.SumExpression object NodeFinder { @@ -346,6 +347,13 @@ object NodeFinder { List(functionExpression) ++ functionExpression.expressions .flatMap(flattenNestedExpressions) + case indexExpression: IndexedExpression => + List(indexExpression) ++ + flattenNestedExpressions(indexExpression.index) + case sumExpression: SumExpression => + List(sumExpression) ++ sumExpression.expressions.flatMap(e => + flattenNestedExpressions(e._2) + ) case default => List(default) } } diff --git a/src/test/scala/millfork/test/language/NodeFinderSuite.scala b/src/test/scala/millfork/test/language/NodeFinderSuite.scala index 6fd6dcbc..b3f5a566 100644 --- a/src/test/scala/millfork/test/language/NodeFinderSuite.scala +++ b/src/test/scala/millfork/test/language/NodeFinderSuite.scala @@ -13,8 +13,24 @@ import millfork.node.ExpressionStatement import millfork.node.FunctionCallExpression import millfork.node.Assignment import java.util.regex.Pattern +import millfork.node.Program +import millfork.node.IndexedExpression +import millfork.node.SumExpression class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { + def createProgram(text: String): Program = { + val server = LanguageHelper.createServer + + LanguageHelper + .openDocument( + server, + "file.mfk", + text + ) + + server.cachedModules.get("file").get + } + describe("nodeAtPosition") { val text = """ | @@ -35,18 +51,10 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { | } """.stripMargin - val server = LanguageHelper.createServer - - LanguageHelper - .openDocument( - server, - "file.mfk", - text - ) - - val program = server.cachedModules.get("file").get + val program = createProgram(text) def findRangeOfString( + text: String, textMatch: String, afterLine: Int = 0 ): (Int, Range) = { @@ -67,7 +75,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find root variable declarations") { - val (line, range) = findRangeOfString("test") + val (line, range) = findRangeOfString(text, "test") for (column <- range) { NodeFinder @@ -80,7 +88,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find root array declarations") { - val (line, range) = findRangeOfString("foo[4]") + val (line, range) = findRangeOfString(text, "foo[4]") for (column <- range) { NodeFinder @@ -93,7 +101,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find function declarations") { - val (line, range) = findRangeOfString("main()") + val (line, range) = findRangeOfString(text, "main()") for (column <- range) { NodeFinder @@ -106,7 +114,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find variable expression within function") { - val (line, range) = findRangeOfString("test", 4) + val (line, range) = findRangeOfString(text, "test", 4) for (column <- range) { NodeFinder @@ -127,7 +135,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find array expression within function") { - val (line, range) = findRangeOfString("foo", 4) + val (line, range) = findRangeOfString(text, "foo", 4) for (column <- range) { NodeFinder @@ -146,7 +154,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find right hand side of assignment") { - val (line, range) = findRangeOfString("test", 5) + val (line, range) = findRangeOfString(text, "test", 5) for (column <- range) { NodeFinder @@ -165,7 +173,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find function call") { - val (line, range) = findRangeOfString("func()") + val (line, range) = findRangeOfString(text, "func()") for (column <- range) { NodeFinder @@ -184,7 +192,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find function argument") { - val (line, range) = findRangeOfString("arg") + val (line, range) = findRangeOfString(text, "arg") for (column <- range) { NodeFinder @@ -200,7 +208,7 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } it("should find function nested variable declarations") { - val (line, range) = findRangeOfString("i", 7) + val (line, range) = findRangeOfString(text, "i", 7) for (column <- range) { NodeFinder @@ -216,8 +224,68 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } } + it("should find variable used to index array") { + val innerText = """ + | byte root + | array(byte) anArray[10] + | void main() { + | byte index + | index = 4 + | root = anArray[index] + | index = anArray[root+1] + | } + """.stripMargin + + val program = createProgram(innerText) + + { + // Standard indexing + val (line, range) = findRangeOfString(innerText, "index", 6) + + for (column <- range) { + NodeFinder + .findNodeAtPosition(program, Position("", line, column, 0)) + ._1 + .get should equal( + program + .declarations(2) + .asInstanceOf[FunctionDeclarationStatement] + .statements + .get(2) + .asInstanceOf[Assignment] + .source + .asInstanceOf[IndexedExpression] + .index + ) + } + } + { + // Indexing within sum expression + val (line, range) = findRangeOfString(innerText, "root", 7) + + for (column <- range) { + NodeFinder + .findNodeAtPosition(program, Position("", line, column, 0)) + ._1 + .get should equal( + program + .declarations(2) + .asInstanceOf[FunctionDeclarationStatement] + .statements + .get(3) + .asInstanceOf[Assignment] + .source + .asInstanceOf[IndexedExpression] + .index + .asInstanceOf[SumExpression] + .expressions(0) + ._2 + ) + } + } + } + // TODO: Additional tests: - // Hover on variable-based array indexing: array[index] // Fields on array indexing: spawn_info[index].hi // Struct type: Player player // Struct fields: player1.pos From 8d204990a335f97db976500111d22d67c12c301b Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Tue, 3 Nov 2020 20:28:37 -0800 Subject: [PATCH 37/54] Added test for sums --- .../test/language/NodeFinderSuite.scala | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/test/scala/millfork/test/language/NodeFinderSuite.scala b/src/test/scala/millfork/test/language/NodeFinderSuite.scala index b3f5a566..27ed2072 100644 --- a/src/test/scala/millfork/test/language/NodeFinderSuite.scala +++ b/src/test/scala/millfork/test/language/NodeFinderSuite.scala @@ -285,13 +285,72 @@ class NodeFinderSuite extends FunSpec with Matchers with AppendedClues { } } + it("should find variables within a sum") { + val innerText = """ + | byte valA + | byte valB + | byte valC + | byte output + | void main() { + | output = valB + valA - 102 + valC + | } + """.stripMargin + + val program = createProgram(innerText) + + val sumExpression = program + .declarations(4) + .asInstanceOf[FunctionDeclarationStatement] + .statements + .get(0) + .asInstanceOf[Assignment] + .source + .asInstanceOf[SumExpression] + + { + val (line, range) = findRangeOfString(innerText, "valA", 5) + + for (column <- range) { + NodeFinder + .findNodeAtPosition(program, Position("", line, column, 0)) + ._1 + .get should equal( + sumExpression.expressions(1)._2 + ) + } + } + { + val (line, range) = findRangeOfString(innerText, "valB", 5) + + for (column <- range) { + NodeFinder + .findNodeAtPosition(program, Position("", line, column, 0)) + ._1 + .get should equal( + sumExpression.expressions(0)._2 + ) + } + } + { + val (line, range) = findRangeOfString(innerText, "valC", 5) + + for (column <- range) { + NodeFinder + .findNodeAtPosition(program, Position("", line, column, 0)) + ._1 + .get should equal( + sumExpression.expressions(3)._2 + ) + } + } + } + // TODO: Additional tests: // Fields on array indexing: spawn_info[index].hi // Struct type: Player player // Struct fields: player1.pos // Messed up hover positions (in `nes_reset_joy.mfk`, each variable assignment to 0) // Alias references - // Math expressions: limit = new_row2 - new_row1 // Pointers: obj_ptr->xvel } } From d181e16b855ea7c370c63409e858c6fb96e38060 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 8 Nov 2020 16:21:44 -0800 Subject: [PATCH 38/54] Added find references support to arrays and params --- .../millfork/language/MfLanguageServer.scala | 9 ++-- .../scala/millfork/language/NodeFinder.scala | 46 ++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index ebcd9307..8e311056 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -249,8 +249,11 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { TelemetryEvent("Attempting to find references") ) - if (declarationContent.isInstanceOf[DeclarationStatement]) { - + if ( + declarationContent + .isInstanceOf[DeclarationStatement] || declarationContent + .isInstanceOf[ParameterDeclaration] + ) { val matchingExpressions = // Only include declaration if params specify it (if (params.getContext().isIncludeDeclaration()) @@ -258,7 +261,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { else List()) ++ NodeFinder .matchingExpressionsForDeclaration( cachedModules.toStream, - declarationContent.asInstanceOf[DeclarationStatement] + declarationContent ) logEvent( diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 385275f6..980cdc78 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -25,6 +25,12 @@ import millfork.node.AliasDefinitionStatement import millfork.node.IndexedExpression import millfork.node.Statement import millfork.node.SumExpression +import millfork.env.ByLazilyEvaluableExpressionVariable +import millfork.node.EnumDefinitionStatement +import millfork.node.LabelStatement +import millfork.node.StructDefinitionStatement +import millfork.node.TypeDefinitionStatement +import millfork.node.UnionDefinitionStatement object NodeFinder { @@ -139,7 +145,7 @@ object NodeFinder { */ def matchingExpressionsForDeclaration( parsedModules: Stream[(String, Program)], - declaration: DeclarationStatement + declaration: Node ): List[(String, Node)] = { parsedModules.toStream.flatMap { case (module, program) => { @@ -161,6 +167,28 @@ object NodeFinder { .map(d => d.asInstanceOf[VariableExpression]) .find(d => d.name == v.name) .map(d => (module, d)) + case a: ArrayDeclarationStatement => + allDeclarations + .filter(d => d.isInstanceOf[IndexedExpression]) + .map(d => d.asInstanceOf[IndexedExpression]) + .find(d => d.name == a.name) + .map(d => (module, d)) + case p: ParameterDeclaration => { + val pName = p.assemblyParamPassingConvention match { + case ByConstant(name) => Some(name) + case ByReference(name) => Some(name) + case ByVariable(name) => Some(name) + case ByLazilyEvaluableExpressionVariable(name) => + Some(name) + case _ => None + } + + if (pName.isDefined) { + allDeclarations + .find(d => extractNodeName(d) == pName) + .map(d => (module, d)) + } else List() + } case default => List() } } @@ -356,4 +384,20 @@ object NodeFinder { ) case default => List(default) } + + private def extractNodeName(node: Node): Option[String] = + node match { + case a: AliasDefinitionStatement => Some(a.name) + case a: ArrayDeclarationStatement => Some(a.name) + case e: EnumDefinitionStatement => Some(e.name) + case f: FunctionCallExpression => Some(f.functionName) + case f: FunctionDeclarationStatement => Some(f.name) + case l: LabelStatement => Some(l.name) + case s: StructDefinitionStatement => Some(s.name) + case t: TypeDefinitionStatement => Some(t.name) + case u: UnionDefinitionStatement => Some(u.name) + case v: VariableDeclarationStatement => Some(v.name) + case v: VariableExpression => Some(v.name) + case _ => None + } } From faf31a8899da10fa57dcf5e3b0cc3f5698cf89be Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 8 Nov 2020 16:27:14 -0800 Subject: [PATCH 39/54] Fixed find references only returning one result --- src/main/scala/millfork/language/NodeFinder.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index 980cdc78..a0c71aba 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -159,19 +159,19 @@ object NodeFinder { allDeclarations .filter(d => d.isInstanceOf[FunctionCallExpression]) .map(d => d.asInstanceOf[FunctionCallExpression]) - .find(d => d.functionName == f.name) + .filter(d => d.functionName == f.name) .map(d => (module, d)) case v: VariableDeclarationStatement => allDeclarations .filter(d => d.isInstanceOf[VariableExpression]) .map(d => d.asInstanceOf[VariableExpression]) - .find(d => d.name == v.name) + .filter(d => d.name == v.name) .map(d => (module, d)) case a: ArrayDeclarationStatement => allDeclarations .filter(d => d.isInstanceOf[IndexedExpression]) .map(d => d.asInstanceOf[IndexedExpression]) - .find(d => d.name == a.name) + .filter(d => d.name == a.name) .map(d => (module, d)) case p: ParameterDeclaration => { val pName = p.assemblyParamPassingConvention match { @@ -185,7 +185,7 @@ object NodeFinder { if (pName.isDefined) { allDeclarations - .find(d => extractNodeName(d) == pName) + .filter(d => extractNodeName(d) == pName) .map(d => (module, d)) } else List() } From f0db3c1847d384e49cd53b2acbf7213ede7ee8b8 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 8 Nov 2020 16:46:34 -0800 Subject: [PATCH 40/54] Rough highlighting of go to def and references (based on name length) --- .../millfork/language/MfLanguageServer.scala | 115 +++++++++--------- .../scala/millfork/language/NodeFinder.scala | 5 +- 2 files changed, 59 insertions(+), 61 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 8e311056..03ecb830 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -184,44 +184,8 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ) if (statement.isDefined) { - val (declarationModule, declarationContent) = statement.get - val formatting = NodeFormatter.symbol(declarationContent) - - logEvent( - TelemetryEvent("Attempting to GoToDef in module", declarationModule) - ) - - val modulePath = modulePaths.getOrElse( - declarationModule, { - logEvent( - TelemetryEvent( - "Could not find path for module", - declarationModule - ) - ) - null - } - ) - - logEvent( - TelemetryEvent("Found module path", modulePath.toString()) - ) - - // ImportStatement declaration is the entire "file". Set position to 1,1 - val position = - if (declarationContent.isInstanceOf[ImportStatement]) - Some(Position(declarationModule, 1, 1, 0)) - else declarationContent.position - - if (position.isDefined) - new Location( - modulePath.toUri().toString(), - new Range( - mfPositionToLSP4j(position.get), - mfPositionToLSP4j(position.get) - ) - ) - else null + val (module, declaration) = statement.get + locationForExpression(declaration, module) } else null } @@ -270,28 +234,8 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { matchingExpressions .map { - case (module, expression) => { - var position = expression.position - val modulePath = modulePaths.getOrElse( - module, { - logEvent( - TelemetryEvent( - "Could not find path for module", - module - ) - ) - null - } - ) - - new Location( - modulePath.toUri().toString(), - new Range( - mfPositionToLSP4j(position.get), - mfPositionToLSP4j(position.get) - ) - ) - } + case (module, expression) => + locationForExpression(expression, module) } .filter(e => e != null) .asJava @@ -419,6 +363,57 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { } } + /** + * Builds highlighted `Location` of a declaration or usage + */ + private def locationForExpression( + expression: Node, + module: String + ): Location = { + val name = NodeFinder.extractNodeName(expression) + val position = expression.position.get + val modulePath = modulePaths.getOrElse( + module, { + logEvent( + TelemetryEvent( + "Could not find path for module", + module + ) + ) + null + } + ) + + if (expression.isInstanceOf[ImportStatement]) { + // ImportStatement declaration is the entire "file". Set position to 1,1 + val importPosition = Position(module, 1, 1, 0) + return new Location( + modulePath.toUri().toString(), + new Range( + mfPositionToLSP4j(importPosition), + mfPositionToLSP4j(importPosition) + ) + ) + } + + val endPosition = if (name.isDefined) { + Position( + module, + position.line, + position.column + name.get.length, + 0 + ) + } else position + + new Location( + modulePath.toUri().toString(), + new Range( + mfPositionToLSP4j(position), + mfPositionToLSP4j(endPosition) + ) + ) + } + private def mfPositionToLSP4j( position: Position ): org.eclipse.lsp4j.Position = diff --git a/src/main/scala/millfork/language/NodeFinder.scala b/src/main/scala/millfork/language/NodeFinder.scala index a0c71aba..cfcf4615 100644 --- a/src/main/scala/millfork/language/NodeFinder.scala +++ b/src/main/scala/millfork/language/NodeFinder.scala @@ -385,7 +385,10 @@ object NodeFinder { case default => List(default) } - private def extractNodeName(node: Node): Option[String] = + /** + * Returns the name of the node, if it exists + */ + def extractNodeName(node: Node): Option[String] = node match { case a: AliasDefinitionStatement => Some(a.name) case a: ArrayDeclarationStatement => Some(a.name) From 0335bb9e81461f257210700e1e7947dde625543a Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 8 Nov 2020 16:50:43 -0800 Subject: [PATCH 41/54] Naively sort find references locations --- src/main/scala/millfork/language/MfLanguageServer.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 03ecb830..4019bab8 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -233,6 +233,9 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ) matchingExpressions + .sortBy { + case (_, expression) => expression.position.get.line + } .map { case (module, expression) => locationForExpression(expression, module) From a1ffbf39bc8fb29ab08f0e13d0aab64788f0d4c3 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 11 Nov 2020 18:15:55 -0800 Subject: [PATCH 42/54] Fixed crash on failure to find module --- src/main/scala/millfork/language/MfLanguageServer.scala | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 4019bab8..4c42b790 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -237,8 +237,13 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { case (_, expression) => expression.position.get.line } .map { - case (module, expression) => - locationForExpression(expression, module) + case (module, expression) => { + try { + locationForExpression(expression, module) + } catch { + case _: Throwable => null + } + } } .filter(e => e != null) .asJava From 811449afe07e390537fbfbb807055ef8cf41c5f5 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 11 Nov 2020 18:23:59 -0800 Subject: [PATCH 43/54] Fixed crash on expressions without positions --- src/main/scala/millfork/language/MfLanguageServer.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 4c42b790..bdcbf02f 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -234,7 +234,11 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { matchingExpressions .sortBy { - case (_, expression) => expression.position.get.line + case (_, expression) => + expression.position match { + case Some(value) => value.line + case None => 0 + } } .map { case (module, expression) => { From dcf06cb608c452434a907bb043a69f171cb56ce7 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 11 Nov 2020 18:48:08 -0800 Subject: [PATCH 44/54] Updated launch task --- .gitignore | 1 + .vscode/tasks.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dcdcb316..620f0f48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # various directories target/ .bloop/ +.bsp/ .idea/ .metals/ project/target diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 35044b94..c523fc58 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,7 +6,7 @@ { "label": "Compile Millfork", "type": "shell", - "command": "sbt 'set test in assembly := {}' compile && sbt 'set test in assembly := {}' assembly", + "command": "sbt -DskipTests=true compile && sbt -DskipTests=true assembly", "problemMatcher": [], "group": { "kind": "build", From 4c69c5424033f6fe7c89d7103c80d446c864db61 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 14 Nov 2020 08:52:41 -0800 Subject: [PATCH 45/54] Switched logging to write to stderr --- src/main/scala/millfork/Main.scala | 4 +--- .../scala/millfork/error/ConsoleLogger.scala | 24 +++++++++---------- .../language/LanguageServerLogger.scala | 2 +- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index 22066e66..62525419 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -34,8 +34,6 @@ object Main { val errorReporting = new ConsoleLogger implicit val __implicitLogger: Logger = errorReporting - // Console.printf("Starting server") - if (args.isEmpty) { errorReporting.info("For help, use --help") } @@ -70,7 +68,7 @@ object Main { if (!c1.languageServer) errorReporting.info("No platform selected, defaulting to `c64`") "c64" }, textCodecRepository) - val options = CompilationOptions(platform, c.flags, c.outputFileName, c.zpRegisterSize.getOrElse(platform.zpRegisterSize), c.features, textCodecRepository, JobContext(new LanguageServerLogger(), new LabelGenerator)) + val options = CompilationOptions(platform, c.flags, c.outputFileName, c.zpRegisterSize.getOrElse(platform.zpRegisterSize), c.features, textCodecRepository, JobContext(errorReporting, new LabelGenerator)) errorReporting.debug("Effective flags: ") options.flags.toSeq.sortBy(_._1).foreach{ case (f, b) => errorReporting.debug(f" $f%-30s : $b%s") diff --git a/src/main/scala/millfork/error/ConsoleLogger.scala b/src/main/scala/millfork/error/ConsoleLogger.scala index 55baa889..f64d8aaf 100644 --- a/src/main/scala/millfork/error/ConsoleLogger.scala +++ b/src/main/scala/millfork/error/ConsoleLogger.scala @@ -27,11 +27,11 @@ class ConsoleLogger extends Logger { val line = lines.apply(lineIx) val column = pos.get.column - 1 val margin = " " - print(margin) - println(line) - print(margin) - print(" " * column) - println("^") + System.err.print(margin) + System.err.println(line) + System.err.print(margin) + System.err.print(" " * column) + System.err.println("^") } } } @@ -42,14 +42,14 @@ class ConsoleLogger extends Logger { override def info(msg: String, position: Option[Position] = None): Unit = { if (verbosity < 0) return - println("INFO: " + f(position) + msg) + System.err.println("INFO: " + f(position) + msg) printErrorContext(position) flushOutput() } override def debug(msg: String, position: Option[Position] = None): Unit = { if (verbosity < 1) return - println("DEBUG: " + f(position) + msg) + System.err.println("DEBUG: " + f(position) + msg) flushOutput() } @@ -59,7 +59,7 @@ class ConsoleLogger extends Logger { override def trace(msg: String, position: Option[Position] = None): Unit = { if (verbosity < 2) return - println("TRACE: " + f(position) + msg) + System.err.println("TRACE: " + f(position) + msg) flushOutput() } @@ -71,7 +71,7 @@ class ConsoleLogger extends Logger { override def warn(msg: String, position: Option[Position] = None): Unit = { if (verbosity < 0) return - println("WARN: " + f(position) + msg) + System.err.println("WARN: " + f(position) + msg) printErrorContext(position) flushOutput() if (fatalWarnings) { @@ -81,14 +81,14 @@ class ConsoleLogger extends Logger { override def error(msg: String, position: Option[Position] = None): Unit = { hasErrors = true - println("ERROR: " + f(position) + msg) + System.err.println("ERROR: " + f(position) + msg) printErrorContext(position) flushOutput() } override def fatal(msg: String, position: Option[Position] = None): Nothing = { hasErrors = true - println("FATAL: " + f(position) + msg) + System.err.println("FATAL: " + f(position) + msg) printErrorContext(position) flushOutput() throw new AssertionError(msg) @@ -96,7 +96,7 @@ class ConsoleLogger extends Logger { override def fatalQuit(msg: String, position: Option[Position] = None): Nothing = { hasErrors = true - println("FATAL: " + f(position) + msg) + System.err.println("FATAL: " + f(position) + msg) printErrorContext(position) flushOutput() System.exit(1) diff --git a/src/main/scala/millfork/language/LanguageServerLogger.scala b/src/main/scala/millfork/language/LanguageServerLogger.scala index 8255a379..918ac299 100644 --- a/src/main/scala/millfork/language/LanguageServerLogger.scala +++ b/src/main/scala/millfork/language/LanguageServerLogger.scala @@ -5,7 +5,7 @@ import millfork.node.Position import millfork.assembly.SourceLine class LanguageServerLogger extends Logger { - // TODO: Complete stub to send diagnostics to client + // TODO: Unused. Complete stub to send diagnostics to client override def setFatalWarnings(fatalWarnings: Boolean): Unit = {} override def info(msg: String, position: Option[Position]): Unit = {} From 892a90d65138e1ac439b970c730b6c35f9f7330c Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 14 Nov 2020 08:57:15 -0800 Subject: [PATCH 46/54] Added basic doccomment support to functions --- .../millfork/language/MfLanguageServer.scala | 12 +++++-- .../millfork/language/NodeFormatter.scala | 36 ++++++++++++++++--- src/main/scala/millfork/node/Node.scala | 16 ++++++++- src/main/scala/millfork/parser/MfParser.scala | 10 +++++- 4 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index bdcbf02f..28c326e4 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -135,10 +135,15 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ) val path = Paths.get(pathString) + val moduleName = queue.extractName(pathString) - logEvent(TelemetryEvent("Path", path.toString())) + logEvent( + TelemetryEvent( + "Path", + Map("path" -> path.toString(), "module" -> moduleName) + ) + ) - val moduleName = queue.extractName(pathString) val newProgram = queue.parseModuleWithLines( moduleName, path, @@ -278,6 +283,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { if (statement.isDefined) { val (_, declarationContent) = statement.get val formatting = NodeFormatter.symbol(declarationContent) + val docstring = NodeFormatter.docstring(declarationContent) if (formatting.isDefined) new Hover( @@ -285,7 +291,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { "markdown", NodeFormatter.hover( formatting.get, - "" + docstring.getOrElse("") ) ) ) diff --git a/src/main/scala/millfork/language/NodeFormatter.scala b/src/main/scala/millfork/language/NodeFormatter.scala index 8763843f..b68af300 100644 --- a/src/main/scala/millfork/language/NodeFormatter.scala +++ b/src/main/scala/millfork/language/NodeFormatter.scala @@ -26,8 +26,12 @@ import millfork.output.NoAlignment import millfork.output.DivisibleAlignment import millfork.output.WithinPageAlignment import millfork.node.AliasDefinitionStatement +import java.util.regex.Pattern object NodeFormatter { + val docstringAsteriskPattern = + Pattern.compile("^\\s*\\*? *", Pattern.MULTILINE) + // TODO: Remove Option def symbol(node: Node): Option[String] = node match { @@ -196,6 +200,26 @@ object NodeFormatter { case WithinPageAlignment => Some("Within page") } + def docstring(node: Node): Option[String] = { + val baseString = node match { + case f: FunctionDeclarationStatement => + if (f.docComment.isDefined) Some(f.docComment.get.text) + else None + case _ => None + } + + if (baseString.isEmpty) { + return None + } + + return Some( + docstringAsteriskPattern + .matcher(baseString.get.stripSuffix("*/")) + .replaceAll("") + // baseString.get.stripSuffix("*/").replaceAll("^\\s*\\*?\\s", "") + ) + } + /** * Render the textDocument/hover result into markdown. * @@ -209,17 +233,19 @@ object NodeFormatter { ): String = { val markdown = new StringBuilder() - if (docstring.nonEmpty) - markdown - .append("\n") - .append(docstring) - if (symbolSignature.nonEmpty) { markdown .append("```mfk\n") .append(symbolSignature) .append("\n```") } + + if (docstring.nonEmpty) + markdown + .append("\n---\n") + .append(docstring) + .append("\n") + markdown.toString() } } diff --git a/src/main/scala/millfork/node/Node.scala b/src/main/scala/millfork/node/Node.scala index fcf345a8..c6e2944f 100644 --- a/src/main/scala/millfork/node/Node.scala +++ b/src/main/scala/millfork/node/Node.scala @@ -495,6 +495,18 @@ sealed trait Statement extends Node { sealed trait DeclarationStatement extends Statement { def name: String + var docComment: Option[DocComment] = None +} + +object DeclarationStatement { + implicit class DeclarationStatementOps[D<:DeclarationStatement](val declaration: D) extends AnyVal { + def docComment(comment: Option[DocComment]): D = { + if (comment.isDefined) { + declaration.docComment = comment + } + declaration + } + } } sealed trait BankedDeclarationStatement extends DeclarationStatement { @@ -879,4 +891,6 @@ object MosAssemblyStatement { def implied(opcode: Opcode.Value, elidability: Elidability.Value) = MosAssemblyStatement(opcode, AddrMode.Implied, LiteralExpression(0, 1), elidability) def nonexistent(opcode: Opcode.Value) = MosAssemblyStatement(opcode, AddrMode.DoesNotExist, LiteralExpression(0, 1), elidability = Elidability.Elidable) -} \ No newline at end of file +} + +case class DocComment(text: String) extends Node {} \ No newline at end of file diff --git a/src/main/scala/millfork/parser/MfParser.scala b/src/main/scala/millfork/parser/MfParser.scala index f267aaba..cd6258e8 100644 --- a/src/main/scala/millfork/parser/MfParser.scala +++ b/src/main/scala/millfork/parser/MfParser.scala @@ -71,6 +71,13 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri val comment: P[Unit] = P("//" ~ CharsWhile(c => c != '\n' && c != '\r', min = 0) ~ ("\r\n" | "\r" | "\n")) + val recursiveDocCommentContent: P[Unit] = P(CharsWhile(c => c != '*', min = 0) ~ ("*/" | ("*" ~ recursiveDocCommentContent))) + + val docComment: P[DocComment] = for { + p <- position() + text <- ("/**" ~ recursiveDocCommentContent.!) + } yield DocComment(text).pos(p) + val semicolon: P[Unit] = P(";" ~ CharsWhileIn("; \t", min = 0) ~ position("line break after a semicolon").map(_ => ()) ~ (comment | "\r\n" | "\r" | "\n").opaque("")) val semicolonComment: P[Unit] = P(";" ~ CharsWhile(c => c != '\n' && c != '\r' && c != '{' && c != '}', min = 0) ~ position("line break instead of braces").map(_ => ()) ~ ("\r\n" | "\r" | "\n").opaque("")) @@ -661,6 +668,7 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri } yield Seq(DoWhileStatement(body.toList, Nil, condition)) val functionDefinition: P[Seq[BankedDeclarationStatement]] = for { + docComment <- (docComment ~ AWS).? p <- position() bank <- bankDeclaration flags <- functionFlags ~ HWS @@ -702,7 +710,7 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri flags("interrupt"), flags("kernal_interrupt"), flags("const") && !flags("asm"), - flags("reentrant")).pos(p)) + flags("reentrant")).pos(p).docComment(docComment)) } def validateAsmFunctionBody(p: Position, flags: Set[String], name: String, statements: Option[List[Statement]]) From 162954f6c4cc94b56d1965806988cbff06396d8b Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 14 Nov 2020 09:55:29 -0800 Subject: [PATCH 47/54] Added markdown formating for @param and @returns --- .../millfork/language/NodeFormatter.scala | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/main/scala/millfork/language/NodeFormatter.scala b/src/main/scala/millfork/language/NodeFormatter.scala index b68af300..dfc863ef 100644 --- a/src/main/scala/millfork/language/NodeFormatter.scala +++ b/src/main/scala/millfork/language/NodeFormatter.scala @@ -27,11 +27,17 @@ import millfork.output.DivisibleAlignment import millfork.output.WithinPageAlignment import millfork.node.AliasDefinitionStatement import java.util.regex.Pattern +import scala.collection.mutable.ListBuffer object NodeFormatter { val docstringAsteriskPattern = Pattern.compile("^\\s*\\*? *", Pattern.MULTILINE) + val docstringParamPattern = + Pattern.compile("@param (\\w+) +(.*)$", Pattern.MULTILINE) + val docstringReturnsPattern = + Pattern.compile("@returns +(.*)$", Pattern.MULTILINE) + // TODO: Remove Option def symbol(node: Node): Option[String] = node match { @@ -212,12 +218,46 @@ object NodeFormatter { return None } - return Some( - docstringAsteriskPattern - .matcher(baseString.get.stripSuffix("*/")) - .replaceAll("") - // baseString.get.stripSuffix("*/").replaceAll("^\\s*\\*?\\s", "") - ) + var strippedString = docstringAsteriskPattern + .matcher(baseString.get.stripSuffix("*/")) + .replaceAll("") + + val matchGroups = new ListBuffer[(String, String, Range)]() + + val paramMatcher = docstringParamPattern.matcher(strippedString) + while (paramMatcher.find()) { + matchGroups += ( + ( + paramMatcher.group(1), + paramMatcher + .group(2), + Range(paramMatcher.start(), paramMatcher.end()) + ) + ) + } + + val builder = new StringBuilder(strippedString) + + for (param <- matchGroups.reverse) { + val (paramName, description, range) = param + builder.replace( + range.start, + range.end, + s"\n_@param_ `${paramName}` \u2014 ${description.trim()}\n" + ) + } + + val returnMatch = docstringReturnsPattern.matcher(builder.toString()) + + if (returnMatch.find()) { + builder.replace( + returnMatch.start(), + returnMatch.end(), + s"\n_@returns_ \u2014 ${returnMatch.group(1).trim()}" + ) + } + + return Some(builder.toString()) } /** From 8ff5e9c740fbeb0e55d14cd9a7d27349d52d72d1 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 14 Nov 2020 11:22:07 -0800 Subject: [PATCH 48/54] Added variable and array declaration docstring support --- .../scala/millfork/language/NodeFormatter.scala | 16 +++++++++------- src/main/scala/millfork/parser/MfParser.scala | 6 ++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/scala/millfork/language/NodeFormatter.scala b/src/main/scala/millfork/language/NodeFormatter.scala index dfc863ef..142243af 100644 --- a/src/main/scala/millfork/language/NodeFormatter.scala +++ b/src/main/scala/millfork/language/NodeFormatter.scala @@ -207,19 +207,21 @@ object NodeFormatter { } def docstring(node: Node): Option[String] = { - val baseString = node match { - case f: FunctionDeclarationStatement => - if (f.docComment.isDefined) Some(f.docComment.get.text) - else None - case _ => None + val docComment = node match { + case f: FunctionDeclarationStatement => f.docComment + case v: VariableDeclarationStatement => v.docComment + case a: ArrayDeclarationStatement => a.docComment + case _ => None } - if (baseString.isEmpty) { + if (docComment.isEmpty) { return None } + val baseString = docComment.get.text + var strippedString = docstringAsteriskPattern - .matcher(baseString.get.stripSuffix("*/")) + .matcher(baseString.stripSuffix("*/")) .replaceAll("") val matchGroups = new ListBuffer[(String, String, Range)]() diff --git a/src/main/scala/millfork/parser/MfParser.scala b/src/main/scala/millfork/parser/MfParser.scala index cd6258e8..0e1e0c35 100644 --- a/src/main/scala/millfork/parser/MfParser.scala +++ b/src/main/scala/millfork/parser/MfParser.scala @@ -216,6 +216,7 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri } yield (p, name, addr, initialValue, alignment) def variableDefinition(implicitlyGlobal: Boolean): P[Seq[BankedDeclarationStatement]] = for { + docComment <- (docComment ~ EOL).? p <- position() bank <- bankDeclaration flags <- variableFlags ~ HWS @@ -231,7 +232,7 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri constant = flags("const"), volatile = flags("volatile"), register = flags("register"), - initialValue, addr, alignment).pos(p) + initialValue, addr, alignment).pos(p).docComment(docComment) } } @@ -409,6 +410,7 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri } val arrayDefinition: P[Seq[ArrayDeclarationStatement]] = for { + docComment <- (docComment ~ EOL).? p <- position() bank <- bankDeclaration const <- ("const".! ~ HWS).? @@ -419,7 +421,7 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri alignment <- alignmentDeclaration(fastAlignmentForFunctions).? ~/ HWS addr <- ("@" ~/ HWS ~/ mfExpression(1, false)).? ~/ HWS contents <- ("=" ~/ HWS ~/ arrayContents).? ~/ HWS - } yield Seq(ArrayDeclarationStatement(name, bank, length, elementType.getOrElse("byte"), addr, const.isDefined, contents, alignment, options.isBigEndian).pos(p)) + } yield Seq(ArrayDeclarationStatement(name, bank, length, elementType.getOrElse("byte"), addr, const.isDefined, contents, alignment, options.isBigEndian).pos(p).docComment(docComment)) def tightMfExpression(allowIntelHex: Boolean, allowTopLevelIndexing: Boolean): P[Expression] = { val a = if (allowIntelHex) atomWithIntel else atom From 8cc48866bb53af4f52c390cc4a2585b60ba52ad3 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sat, 14 Nov 2020 11:28:13 -0800 Subject: [PATCH 49/54] Support multiline comments --- src/main/scala/millfork/parser/MfParser.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/scala/millfork/parser/MfParser.scala b/src/main/scala/millfork/parser/MfParser.scala index 0e1e0c35..c7e7d065 100644 --- a/src/main/scala/millfork/parser/MfParser.scala +++ b/src/main/scala/millfork/parser/MfParser.scala @@ -71,18 +71,20 @@ abstract class MfParser[T](fileId: String, input: String, currentDirectory: Stri val comment: P[Unit] = P("//" ~ CharsWhile(c => c != '\n' && c != '\r', min = 0) ~ ("\r\n" | "\r" | "\n")) - val recursiveDocCommentContent: P[Unit] = P(CharsWhile(c => c != '*', min = 0) ~ ("*/" | ("*" ~ recursiveDocCommentContent))) + val recursiveMultilineCommentContent: P[Unit] = P(CharsWhile(c => c != '*', min = 0) ~ ("*/" | ("*" ~ recursiveMultilineCommentContent))) val docComment: P[DocComment] = for { p <- position() - text <- ("/**" ~ recursiveDocCommentContent.!) + text <- ("/**" ~ recursiveMultilineCommentContent.!) } yield DocComment(text).pos(p) + val multilineComment: P[Unit] = P("/*" ~ !"*" ~ recursiveMultilineCommentContent) + val semicolon: P[Unit] = P(";" ~ CharsWhileIn("; \t", min = 0) ~ position("line break after a semicolon").map(_ => ()) ~ (comment | "\r\n" | "\r" | "\n").opaque("")) val semicolonComment: P[Unit] = P(";" ~ CharsWhile(c => c != '\n' && c != '\r' && c != '{' && c != '}', min = 0) ~ position("line break instead of braces").map(_ => ()) ~ ("\r\n" | "\r" | "\n").opaque("")) - val AWS: P[Unit] = P((CharIn(" \t\n\r") | semicolon | comment).rep(min = 0)).opaque("") + val AWS: P[Unit] = P((CharIn(" \t\n\r") | semicolon | comment | multilineComment).rep(min = 0)).opaque("") val AWS_asm: P[Unit] = P((CharIn(" \t\n\r") | semicolonComment | comment).rep(min = 0)).opaque("") From 711b149a3f5331b3a41fba1624545ebe31a94712 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Sun, 15 Nov 2020 16:37:45 -0800 Subject: [PATCH 50/54] Fixed module path not being added when loaded through LSP --- src/main/scala/millfork/language/MfLanguageServer.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index 28c326e4..f919da3b 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -157,6 +157,7 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { cachedModules.put(moduleName, newProgram.get) moduleNames.put(pathString, moduleName) + modulePaths.put(moduleName, Paths.get(pathString)) logEvent( TelemetryEvent( From 718f47fcbe8c206e830a45f8ea55f8f3efdcccda Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 31 Dec 2020 09:23:12 -0800 Subject: [PATCH 51/54] Switch to stderr logging only once LSP starts --- src/main/scala/millfork/Main.scala | 3 +- .../scala/millfork/error/ConsoleLogger.scala | 36 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/main/scala/millfork/Main.scala b/src/main/scala/millfork/Main.scala index 62525419..c946b98b 100644 --- a/src/main/scala/millfork/Main.scala +++ b/src/main/scala/millfork/Main.scala @@ -28,8 +28,6 @@ import millfork.cli.JsonConfigParser object Main { - - def main(args: Array[String]): Unit = { val errorReporting = new ConsoleLogger implicit val __implicitLogger: Logger = errorReporting @@ -76,6 +74,7 @@ object Main { if (c1.languageServer) { // We cannot log anything to stdout when starting the language server (otherwise it's a protocol violation) + errorReporting.setOutput(true) val server = new MfLanguageServer(c, options) val exec = Executors.newCachedThreadPool() diff --git a/src/main/scala/millfork/error/ConsoleLogger.scala b/src/main/scala/millfork/error/ConsoleLogger.scala index f64d8aaf..296c6787 100644 --- a/src/main/scala/millfork/error/ConsoleLogger.scala +++ b/src/main/scala/millfork/error/ConsoleLogger.scala @@ -4,10 +4,12 @@ import millfork.assembly.SourceLine import millfork.node.Position import scala.collection.mutable +import java.io.PrintStream class ConsoleLogger extends Logger { FatalErrorReporting.considerAsGlobal(this) + private var defaultUseStderr = false var verbosity = 0 var fatalWarnings = false @@ -15,6 +17,10 @@ class ConsoleLogger extends Logger { this.fatalWarnings = fatalWarnings } + def setOutput(useStderr: Boolean): Unit = { + this.defaultUseStderr = useStderr + } + var hasErrors = false private val sourceLines: mutable.Map[String, IndexedSeq[String]] = mutable.Map() @@ -27,11 +33,11 @@ class ConsoleLogger extends Logger { val line = lines.apply(lineIx) val column = pos.get.column - 1 val margin = " " - System.err.print(margin) - System.err.println(line) - System.err.print(margin) - System.err.print(" " * column) - System.err.println("^") + this.print(margin) + this.println(line) + this.print(margin) + this.print(" " * column) + this.println("^") } } } @@ -42,14 +48,14 @@ class ConsoleLogger extends Logger { override def info(msg: String, position: Option[Position] = None): Unit = { if (verbosity < 0) return - System.err.println("INFO: " + f(position) + msg) + this.println("INFO: " + f(position) + msg) printErrorContext(position) flushOutput() } override def debug(msg: String, position: Option[Position] = None): Unit = { if (verbosity < 1) return - System.err.println("DEBUG: " + f(position) + msg) + this.println("DEBUG: " + f(position) + msg) flushOutput() } @@ -59,7 +65,7 @@ class ConsoleLogger extends Logger { override def trace(msg: String, position: Option[Position] = None): Unit = { if (verbosity < 2) return - System.err.println("TRACE: " + f(position) + msg) + this.println("TRACE: " + f(position) + msg) flushOutput() } @@ -71,7 +77,7 @@ class ConsoleLogger extends Logger { override def warn(msg: String, position: Option[Position] = None): Unit = { if (verbosity < 0) return - System.err.println("WARN: " + f(position) + msg) + this.println("WARN: " + f(position) + msg) printErrorContext(position) flushOutput() if (fatalWarnings) { @@ -81,14 +87,14 @@ class ConsoleLogger extends Logger { override def error(msg: String, position: Option[Position] = None): Unit = { hasErrors = true - System.err.println("ERROR: " + f(position) + msg) + this.println("ERROR: " + f(position) + msg) printErrorContext(position) flushOutput() } override def fatal(msg: String, position: Option[Position] = None): Nothing = { hasErrors = true - System.err.println("FATAL: " + f(position) + msg) + this.println("FATAL: " + f(position) + msg) printErrorContext(position) flushOutput() throw new AssertionError(msg) @@ -96,7 +102,7 @@ class ConsoleLogger extends Logger { override def fatalQuit(msg: String, position: Option[Position] = None): Nothing = { hasErrors = true - System.err.println("FATAL: " + f(position) + msg) + this.println("FATAL: " + f(position) + msg) printErrorContext(position) flushOutput() System.exit(1) @@ -128,4 +134,10 @@ class ConsoleLogger extends Logger { file <- sourceLines.get(line.moduleName) line <- file.lift(line.line - 1) } yield line + + private def getOutputStream: PrintStream = if (this.defaultUseStderr) System.err else System.out + + private def print(x: String): Unit = getOutputStream.print(x) + + private def println(x: String): Unit = getOutputStream.println(x) } \ No newline at end of file From a05a1eb827d726a5ff2a93eba8ef9bcf8e078b28 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 31 Dec 2020 09:49:25 -0800 Subject: [PATCH 52/54] Enabled other instruction support for LSP --- .../millfork/language/MfLanguageServer.scala | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index f919da3b..afd8c5f4 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -1,8 +1,12 @@ package millfork.language import millfork.CompilationOptions -import millfork.parser.MosSourceLoadingQueue +import millfork.parser.{ + MosSourceLoadingQueue, + ZSourceLoadingQueue, + MSourceLoadingQueue, + ParsedProgram +} import millfork.Context -import millfork.parser.ParsedProgram import millfork.node.{ FunctionDeclarationStatement, @@ -43,7 +47,6 @@ import org.eclipse.lsp4j.MessageType import org.eclipse.lsp4j.DidOpenTextDocumentParams import org.eclipse.lsp4j.TextDocumentSyncKind import org.eclipse.lsp4j.DidChangeTextDocumentParams -import millfork.parser.AbstractSourceLoadingQueue import java.nio.file.Path import java.nio.file.Paths import org.eclipse.lsp4j.VersionedTextDocumentIdentifier @@ -52,6 +55,9 @@ import millfork.node.ImportStatement import org.eclipse.lsp4j.ReferenceParams import millfork.node.DeclarationStatement import scala.collection.JavaConverters._ +import millfork.CpuFamily +import millfork.parser.ZSourceLoadingQueue +import millfork.parser.MSourceLoadingQueue class MfLanguageServer(context: Context, options: CompilationOptions) { var client: Option[MfLanguageClient] = None @@ -128,12 +134,6 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { def rebuildASTForFile(pathString: String, text: Seq[String]) = { logEvent(TelemetryEvent("Rebuilding AST for module at path", pathString)) - val queue = new MosSourceLoadingQueue( - initialFilenames = context.inputFileNames, - includePath = context.includePath, - options = options - ) - val path = Paths.get(pathString) val moduleName = queue.extractName(pathString) @@ -308,12 +308,6 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { TelemetryEvent("Building program AST") ) - val queue = new MosSourceLoadingQueue( - initialFilenames = context.inputFileNames, - includePath = context.includePath, - options = options - ) - var program = queue.run() logEvent( @@ -433,6 +427,28 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ) } + private def queue() = + CpuFamily.forType(options.platform.cpu) match { + case CpuFamily.M6502 => + new MosSourceLoadingQueue( + initialFilenames = context.inputFileNames, + includePath = context.includePath, + options = options + ) + case CpuFamily.I80 | CpuFamily.I86 => + new ZSourceLoadingQueue( + initialFilenames = context.inputFileNames, + includePath = context.includePath, + options = options + ) + case CpuFamily.M6809 => + new MSourceLoadingQueue( + initialFilenames = context.inputFileNames, + includePath = context.includePath, + options = options + ) + } + private def mfPositionToLSP4j( position: Position ): org.eclipse.lsp4j.Position = From 75e4f26ce0034c22c78bbb1941a2d30efd0b859c Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 31 Dec 2020 10:56:12 -0800 Subject: [PATCH 53/54] Updated build.sbt --- build.sbt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build.sbt b/build.sbt index b7020817..50d0ac83 100644 --- a/build.sbt +++ b/build.sbt @@ -37,7 +37,7 @@ val testDependencies = Seq( val includesTests = System.getProperty("skipTests") == null -libraryDependencies ++=( +libraryDependencies ++= ( if (includesTests) { println("Including test dependencies") testDependencies @@ -47,11 +47,11 @@ libraryDependencies ++=( ) (if (!includesTests) { - // Disable assembling tests - sbt.internals.DslEntry.fromSettingsDef(test in assembly := {}) -} else { - sbt.internals.DslEntry.fromSettingsDef(Seq[sbt.Def.Setting[_]]()) -}) + // Disable assembling tests + sbt.internal.DslEntry.fromSettingsDef(test in assembly := {}) + } else { + sbt.internal.DslEntry.fromSettingsDef(Seq[sbt.Def.Setting[_]]()) + }) mainClass in Compile := Some("millfork.Main") From b1f7a058803722bfdad11bf6634c8e5037f9ccb4 Mon Sep 17 00:00:00 2001 From: agg23 Date: Sat, 30 Jan 2021 08:09:22 -0800 Subject: [PATCH 54/54] Fixed paths not being properly sanitized on Windows --- src/main/scala/millfork/language/MfLanguageServer.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/scala/millfork/language/MfLanguageServer.scala b/src/main/scala/millfork/language/MfLanguageServer.scala index afd8c5f4..e2199d32 100644 --- a/src/main/scala/millfork/language/MfLanguageServer.scala +++ b/src/main/scala/millfork/language/MfLanguageServer.scala @@ -472,7 +472,12 @@ class MfLanguageServer(context: Context, options: CompilationOptions) { ) } - private def trimDocumentUri(uri: String): String = uri.stripPrefix("file:") + private def trimDocumentUri(uri: String): String = + uri + .replaceFirst("file:(//)?", "") + // Trim Windows path oddities provided by VSCode (may not be for all LSP clients) + .replaceFirst("%3A", ":") + .replaceFirst("/([A-Za-z]):", "$1:") } case class TelemetryEvent(message: String, data: Any = None)