diff --git a/.idea/modules.xml b/.idea/modules.xml
index e986d3fcd..99975de84 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -15,9 +15,10 @@
+
-
\ No newline at end of file
+
diff --git a/languageServer/build.gradle.kts b/languageServer/build.gradle.kts
new file mode 100644
index 000000000..9c816c96e
--- /dev/null
+++ b/languageServer/build.gradle.kts
@@ -0,0 +1,103 @@
+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") {
+ filePermissions {
+ user {
+ read=true
+ execute=true
+ write=true
+ }
+ other.execute = true
+ group.execute = true
+ }
+ }
+}
+
+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..8c075754d
--- /dev/null
+++ b/languageServer/languageServer.iml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/languageServer/src/prog8lsp/AsyncExecutor.kt b/languageServer/src/prog8lsp/AsyncExecutor.kt
new file mode 100644
index 000000000..285b1e0dd
--- /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)
+ }
+ }
+}
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 3b825d1c3..e98a4e306 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -9,5 +9,6 @@ include(
':codeGenCpu6502',
':codeGenExperimental',
':compiler',
- ':beanshell'
+ ':beanshell',
+ ':languageServer'
)