Skip to content

Commit

Permalink
first setup of LSP server
Browse files Browse the repository at this point in the history
  • Loading branch information
irmen committed Nov 9, 2024
1 parent 2fcb83a commit 1cd79c6
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

103 changes: 103 additions & 0 deletions languageServer/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<Exec>("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<JavaExec>("debugRun") {
mainClass.set(serverMainClassName)
classpath(sourceSets.main.get().runtimeClasspath)
standardInput = System.`in`

jvmArgs(debugArgs)
doLast {
println("Using debug port $debugPort")
}
}

tasks.register<CreateStartScripts>("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<Sync>("installDebugDist") {
dependsOn("installDist")
finalizedBy("debugStartScripts")
}

tasks.withType<Test>() {
testLogging {
events("failed")
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}
}

tasks.installDist {
finalizedBy("fixFilePermissions")
}

tasks.build {
finalizedBy("installDist")
}
15 changes: 15 additions & 0 deletions languageServer/languageServer.iml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
<orderEntry type="library" name="jetbrains.kotlinx.cli.jvm" level="project" />
<orderEntry type="library" name="michael.bull.kotlin.result.jvm" level="project" />
<orderEntry type="library" name="eclipse.lsp4j" level="project" />
</component>
</module>
34 changes: 34 additions & 0 deletions languageServer/src/prog8lsp/AsyncExecutor.kt
Original file line number Diff line number Diff line change
@@ -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 <R> compute(task: () -> R) =
CompletableFuture.supplyAsync(Supplier(task), workerThread)

fun <R> 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)
}
}
}
19 changes: 19 additions & 0 deletions languageServer/src/prog8lsp/Main.kt
Original file line number Diff line number Diff line change
@@ -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<String>) {
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()
}
46 changes: 46 additions & 0 deletions languageServer/src/prog8lsp/Prog8LanguageServer.kt
Original file line number Diff line number Diff line change
@@ -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<InitializeResult> = async.compute {
logger.info("Initializing LanguageServer")

InitializeResult()
}

override fun shutdown(): CompletableFuture<Any> {
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)
}
}
62 changes: 62 additions & 0 deletions languageServer/src/prog8lsp/Prog8TextDocumentService.kt
Original file line number Diff line number Diff line change
@@ -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<MutableList<Either<SymbolInformation, DocumentSymbol>>> = async.compute {
logger.info("Find symbols in ${params.textDocument.uri}")
val result: MutableList<Either<SymbolInformation, DocumentSymbol>>
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<Either<MutableList<CompletionItem>, CompletionList>> = async.compute{
logger.info("Completion for ${position}")
val result: Either<MutableList<CompletionItem>, 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
}
80 changes: 80 additions & 0 deletions languageServer/src/prog8lsp/Prog8WorkspaceService.kt
Original file line number Diff line number Diff line change
@@ -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<Any> {
logger.info("executeCommand $params")
return super.executeCommand(params)
}

override fun symbol(params: WorkspaceSymbolParams): CompletableFuture<Either<MutableList<out SymbolInformation>, MutableList<out WorkspaceSymbol>>> {
logger.info("symbol $params")
return super.symbol(params)
}

override fun resolveWorkspaceSymbol(workspaceSymbol: WorkspaceSymbol): CompletableFuture<WorkspaceSymbol> {
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<WorkspaceEdit> {
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<WorkspaceEdit> {
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<WorkspaceEdit> {
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<WorkspaceDiagnosticReport> {
logger.info("diagnostic $params")
return super.diagnostic(params)
}
}
3 changes: 2 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ include(
':codeGenCpu6502',
':codeGenExperimental',
':compiler',
':beanshell'
':beanshell',
':languageServer'
)

0 comments on commit 1cd79c6

Please sign in to comment.