diff --git a/.idea/libraries/eclipse_lsp4j.xml b/.idea/libraries/eclipse_lsp4j.xml new file mode 100644 index 000000000..fb8db4222 --- /dev/null +++ b/.idea/libraries/eclipse_lsp4j.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 347a298f4..b064dad95 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -13,7 +13,9 @@ + + diff --git a/languageServer/build.gradle.kts b/languageServer/build.gradle.kts new file mode 100644 index 000000000..d0d504eca --- /dev/null +++ b/languageServer/build.gradle.kts @@ -0,0 +1,95 @@ +plugins { + kotlin("jvm") + id("application") +} + +val debugPort = 8000 +val debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=n,quiet=y" + +val serverMainClassName = "prog8lsp.MainKt" +val applicationName = "prog8-language-server" + +application { + mainClass.set(serverMainClassName) + description = "Code completions, diagnostics and more for Prog8" + // applicationDefaultJvmArgs = listOf("-DkotlinLanguageServer.version=$version") + applicationDistribution.into("bin") { + fileMode = 755 + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.23.1") + implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.23.1") +} + +configurations.forEach { config -> + config.resolutionStrategy { + preferProjectModules() + } +} + +sourceSets.main { + java.srcDir("src") + resources.srcDir("resources") +} + +sourceSets.test { + java.srcDir("src") + resources.srcDir("resources") +} + +tasks.startScripts { + applicationName = "prog8-language-server" +} + +tasks.register("fixFilePermissions") { + // When running on macOS or Linux the start script + // needs executable permissions to run. + + onlyIf { !System.getProperty("os.name").lowercase().contains("windows") } + commandLine("chmod", "+x", "${tasks.installDist.get().destinationDir}/bin/prog8-language-server") +} + +tasks.register("debugRun") { + mainClass.set(serverMainClassName) + classpath(sourceSets.main.get().runtimeClasspath) + standardInput = System.`in` + + jvmArgs(debugArgs) + doLast { + println("Using debug port $debugPort") + } +} + +tasks.register("debugStartScripts") { + applicationName = "prog8-language-server" + mainClass.set(serverMainClassName) + outputDir = tasks.installDist.get().destinationDir.toPath().resolve("bin").toFile() + classpath = tasks.startScripts.get().classpath + defaultJvmOpts = listOf(debugArgs) +} + +tasks.register("installDebugDist") { + dependsOn("installDist") + finalizedBy("debugStartScripts") +} + +tasks.withType() { + testLogging { + events("failed") + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } +} + +tasks.installDist { + finalizedBy("fixFilePermissions") +} + +tasks.build { + finalizedBy("installDist") +} diff --git a/languageServer/languageServer.iml b/languageServer/languageServer.iml new file mode 100644 index 000000000..65e9cf5e0 --- /dev/null +++ b/languageServer/languageServer.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/languageServer/src/prog8lsp/AsyncExecutor.kt b/languageServer/src/prog8lsp/AsyncExecutor.kt new file mode 100644 index 000000000..5858e86e9 --- /dev/null +++ b/languageServer/src/prog8lsp/AsyncExecutor.kt @@ -0,0 +1,34 @@ +package prog8lsp + +import java.util.function.Supplier +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +private var threadCount = 0 + +class AsyncExecutor { + private val workerThread = Executors.newSingleThreadExecutor { Thread(it, "async${threadCount++}") } + + fun execute(task: () -> Unit) = + CompletableFuture.runAsync(Runnable(task), workerThread) + + fun compute(task: () -> R) = + CompletableFuture.supplyAsync(Supplier(task), workerThread) + + fun computeOr(defaultValue: R, task: () -> R?) = + CompletableFuture.supplyAsync(Supplier { + try { + task() ?: defaultValue + } catch (e: Exception) { + defaultValue + } + }, workerThread) + + fun shutdown(awaitTermination: Boolean) { + workerThread.shutdown() + if (awaitTermination) { + workerThread.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS) + } + } +} \ No newline at end of file diff --git a/languageServer/src/prog8lsp/Main.kt b/languageServer/src/prog8lsp/Main.kt new file mode 100644 index 000000000..69779ca4f --- /dev/null +++ b/languageServer/src/prog8lsp/Main.kt @@ -0,0 +1,19 @@ +package prog8lsp + +import org.eclipse.lsp4j.launch.LSPLauncher +import java.util.concurrent.Executors +import java.util.logging.Level +import java.util.logging.Logger + +fun main(args: Array) { + Logger.getLogger("").level = Level.INFO + + val inStream = System.`in` + val outStream = System.out + val server = Prog8LanguageServer() + val threads = Executors.newSingleThreadExecutor { Thread(it, "client") } + val launcher = LSPLauncher.createServerLauncher(server, inStream, outStream, threads) { it } + + server.connect(launcher.remoteProxy) + launcher.startListening() +} diff --git a/languageServer/src/prog8lsp/Prog8LanguageServer.kt b/languageServer/src/prog8lsp/Prog8LanguageServer.kt new file mode 100644 index 000000000..bfd044bed --- /dev/null +++ b/languageServer/src/prog8lsp/Prog8LanguageServer.kt @@ -0,0 +1,46 @@ +package prog8lsp + +import org.eclipse.lsp4j.InitializeParams +import org.eclipse.lsp4j.InitializeResult +import org.eclipse.lsp4j.services.* +import java.io.Closeable +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletableFuture.completedFuture +import java.util.logging.Logger + +class Prog8LanguageServer: LanguageServer, LanguageClientAware, Closeable { + private lateinit var client: LanguageClient + private val textDocuments = Prog8TextDocumentService() + private val workspaces = Prog8WorkspaceService() + private val async = AsyncExecutor() + private val logger = Logger.getLogger(Prog8LanguageServer::class.simpleName) + + override fun initialize(params: InitializeParams): CompletableFuture = async.compute { + logger.info("Initializing LanguageServer") + + InitializeResult() + } + + override fun shutdown(): CompletableFuture { + close() + return completedFuture(null) + } + + override fun exit() { } + + override fun getTextDocumentService(): TextDocumentService = textDocuments + + override fun getWorkspaceService(): WorkspaceService = workspaces + + override fun connect(client: LanguageClient) { + logger.info("connecting to language client") + this.client = client + workspaces.connect(client) + textDocuments.connect(client) + } + + override fun close() { + logger.info("closing down") + async.shutdown(awaitTermination = true) + } +} diff --git a/languageServer/src/prog8lsp/Prog8TextDocumentService.kt b/languageServer/src/prog8lsp/Prog8TextDocumentService.kt new file mode 100644 index 000000000..40d76ab4a --- /dev/null +++ b/languageServer/src/prog8lsp/Prog8TextDocumentService.kt @@ -0,0 +1,62 @@ +package prog8lsp + +import org.eclipse.lsp4j.* +import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.services.TextDocumentService +import java.util.concurrent.CompletableFuture +import java.util.logging.Logger +import kotlin.system.measureTimeMillis + +class Prog8TextDocumentService: TextDocumentService { + private var client: LanguageClient? = null + private val async = AsyncExecutor() + private val logger = Logger.getLogger(Prog8TextDocumentService::class.simpleName) + + fun connect(client: LanguageClient) { + this.client = client + } + + override fun didOpen(params: DidOpenTextDocumentParams) { + logger.info("didOpen: $params") + } + + override fun didChange(params: DidChangeTextDocumentParams) { + logger.info("didChange: $params") + } + + override fun didClose(params: DidCloseTextDocumentParams) { + logger.info("didClose: $params") + } + + override fun didSave(params: DidSaveTextDocumentParams) { + logger.info("didSave: $params") + } + + override fun documentSymbol(params: DocumentSymbolParams): CompletableFuture>> = async.compute { + logger.info("Find symbols in ${params.textDocument.uri}") + val result: MutableList> + val time = measureTimeMillis { + result = mutableListOf() + val range = Range(Position(1,1), Position(1,5)) + val selectionRange = Range(Position(1,2), Position(1,10)) + val symbol = DocumentSymbol("test-symbolName", SymbolKind.Constant, range, selectionRange) + result.add(Either.forRight(symbol)) + } + logger.info("Finished in $time ms") + result + } + + override fun completion(position: CompletionParams): CompletableFuture, CompletionList>> = async.compute{ + logger.info("Completion for ${position}") + val result: Either, CompletionList> + val time = measureTimeMillis { + val list = CompletionList(false, listOf(CompletionItem("test-completionItem"))) + result = Either.forRight(list) + } + logger.info("Finished in $time ms") + result + } + + // TODO add all other methods that get called.... :P +} diff --git a/languageServer/src/prog8lsp/Prog8WorkspaceService.kt b/languageServer/src/prog8lsp/Prog8WorkspaceService.kt new file mode 100644 index 000000000..87fc9cba2 --- /dev/null +++ b/languageServer/src/prog8lsp/Prog8WorkspaceService.kt @@ -0,0 +1,80 @@ +package prog8lsp + +import org.eclipse.lsp4j.* +import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.eclipse.lsp4j.services.LanguageClient +import org.eclipse.lsp4j.services.WorkspaceService +import java.util.concurrent.CompletableFuture +import java.util.logging.Logger + +class Prog8WorkspaceService: WorkspaceService { + private var client: LanguageClient? = null + private val logger = Logger.getLogger(Prog8WorkspaceService::class.simpleName) + + fun connect(client: LanguageClient) { + this.client = client + } + + override fun executeCommand(params: ExecuteCommandParams): CompletableFuture { + logger.info("executeCommand $params") + return super.executeCommand(params) + } + + override fun symbol(params: WorkspaceSymbolParams): CompletableFuture, MutableList>> { + logger.info("symbol $params") + return super.symbol(params) + } + + override fun resolveWorkspaceSymbol(workspaceSymbol: WorkspaceSymbol): CompletableFuture { + logger.info("resolveWorkspaceSymbol $workspaceSymbol") + return super.resolveWorkspaceSymbol(workspaceSymbol) + } + + override fun didChangeConfiguration(params: DidChangeConfigurationParams) { + logger.info("didChangeConfiguration: $params") + } + + override fun didChangeWatchedFiles(params: DidChangeWatchedFilesParams) { + logger.info("didChangeWatchedFiles: $params") + } + + override fun didChangeWorkspaceFolders(params: DidChangeWorkspaceFoldersParams) { + logger.info("didChangeWorkspaceFolders $params") + super.didChangeWorkspaceFolders(params) + } + + override fun willCreateFiles(params: CreateFilesParams): CompletableFuture { + logger.info("willCreateFiles $params") + return super.willCreateFiles(params) + } + + override fun didCreateFiles(params: CreateFilesParams) { + logger.info("didCreateFiles $params") + super.didCreateFiles(params) + } + + override fun willRenameFiles(params: RenameFilesParams): CompletableFuture { + logger.info("willRenameFiles $params") + return super.willRenameFiles(params) + } + + override fun didRenameFiles(params: RenameFilesParams) { + logger.info("didRenameFiles $params") + super.didRenameFiles(params) + } + + override fun willDeleteFiles(params: DeleteFilesParams): CompletableFuture { + logger.info("willDeleteFiles $params") + return super.willDeleteFiles(params) + } + + override fun didDeleteFiles(params: DeleteFilesParams) { + logger.info("didDeleteFiles $params") + super.didDeleteFiles(params) + } + + override fun diagnostic(params: WorkspaceDiagnosticParams): CompletableFuture { + logger.info("diagnostic $params") + return super.diagnostic(params) + } +} diff --git a/settings.gradle b/settings.gradle index c68f1b9b6..9a99e5a39 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,5 +8,6 @@ include( ':codeGenIntermediate', ':codeGenCpu6502', ':codeGenExperimental', - ':compiler' + ':compiler', + ':languageServer' )