Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LSP support #369

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
7 changes: 7 additions & 0 deletions buildSrc/src/main/kotlin/pklFatJar.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ val relocations = mapOf(

// pkl-codegen-kotlin dependencies
"com.squareup.kotlinpoet." to "org.pkl.thirdparty.kotlinpoet.",

// pkl-lsp dependencies
"com.google.gson" to "org.pkl.thirdparty.gson",
"org.eclipse.lsp4j" to "org.pkl.thirdparty.lsp4j",
)

val nonRelocations = listOf("com/oracle/truffle/")
Expand All @@ -70,6 +74,9 @@ tasks.shadowJar {
// org.antlr.v4.runtime.misc.RuleDependencyProcessor
exclude("META-INF/services/javax.annotation.processing.Processor")

// org.eclipse.lsp4j
exclude("about.html")

exclude("module-info.*")

for ((from, to) in relocations) {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ ktfmt = "0.44"
# replaces nuValidator's log4j dependency
# something related to log4j-1.2-api is apparently broken in 2.17.2
log4j = "2.17.1"
lsp4j = "0.23.1"
msgpack = "0.9.0"
nexusPublishPlugin = "1.3.0"
nuValidator = "20.+"
Expand Down Expand Up @@ -80,6 +81,7 @@ kotlinStdLib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", ve
kotlinxHtml = { group = "org.jetbrains.kotlinx", name = "kotlinx-html-jvm", version.ref = "kotlinxHtml" }
kotlinxSerializationJson = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
log4j12Api = { group = "org.apache.logging.log4j", name = "log4j-1.2-api", version.ref = "log4j" }
lsp4j = { group = "org.eclipse.lsp4j", name = "org.eclipse.lsp4j", version.ref = "lsp4j" }
msgpack = { group = "org.msgpack", name = "msgpack-core", version.ref = "msgpack" }
nuValidator = { group = "nu.validator", name = "validator", version.ref = "nuValidator" }
# to be replaced with https://github.com/usethesource/capsule or https://github.com/lacuna/bifurcan
Expand Down
3 changes: 3 additions & 0 deletions pkl-cli/gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
# This file is expected to be part of source control.
com.github.ajalt.clikt:clikt-jvm:3.5.1=compileClasspath,default,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.ajalt.clikt:clikt:3.5.1=apiDependenciesMetadata,compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.google.code.gson:gson:2.10.1=runtimeClasspath,testRuntimeClasspath
com.tunnelvisionlabs:antlr4-runtime:4.9.0=default,runtimeClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.14.11=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata
org.assertj:assertj-core:3.25.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.22.0=runtimeClasspath,testRuntimeClasspath
org.eclipse.lsp4j:org.eclipse.lsp4j:0.22.0=runtimeClasspath,testRuntimeClasspath
org.fusesource.jansi:jansi:2.4.0=default
org.fusesource.jansi:jansi:2.4.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.graalvm.compiler:compiler:23.0.2=compileClasspath,compileOnlyDependenciesMetadata
Expand Down
1 change: 1 addition & 0 deletions pkl-cli/pkl-cli.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
implementation(libs.jlineTerminal)
implementation(libs.jlineTerminalJansi)
implementation(projects.pklServer)
implementation(projects.pklLsp)
implementation(libs.clikt) {
// force clikt to use our version of the kotlin stdlib
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8")
Expand Down
3 changes: 2 additions & 1 deletion pkl-cli/src/main/kotlin/org/pkl/cli/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ internal fun main(args: Array<String>) {
ServerCommand(helpLink),
TestCommand(helpLink),
ProjectCommand(helpLink),
DownloadPackageCommand(helpLink)
DownloadPackageCommand(helpLink),
LspCommand(helpLink)
)
.main(args)
}
Expand Down
39 changes: 39 additions & 0 deletions pkl-cli/src/main/kotlin/org/pkl/cli/commands/LspCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.cli.commands

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import org.pkl.commons.cli.commands.single
import org.pkl.lsp.PklLSP

class LspCommand(helpLink: String) :
CliktCommand(
name = "lsp",
help = "Run a Language Server Protocol server that communicates over standard input/output",
epilog = "For more information, visit $helpLink"
) {

private val verbose: Boolean by
option(names = arrayOf("--verbose"), help = "Send debug information to the client")
.single()
.flag(default = false)

override fun run() {
PklLSP.run(verbose)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2714,7 +2714,7 @@ private String getCommonIndent(MultiLineStringPartContext lastPart, Token endQuo
.build();
}

private static boolean isIndentChars(Token token) {
public static boolean isIndentChars(Token token) {
var text = token.getText();

for (var i = 0; i < text.length(); i++) {
Expand All @@ -2725,7 +2725,7 @@ private static boolean isIndentChars(Token token) {
return true;
}

private static String getLeadingIndent(Token token) {
public static String getLeadingIndent(Token token) {
var text = token.getText();

for (var i = 0; i < text.length(); i++) {
Expand Down
38 changes: 38 additions & 0 deletions pkl-lsp/gradle.lockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.google.code.gson:gson:2.10.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
com.tunnelvisionlabs:antlr4-runtime:4.9.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.14.11=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata
org.assertj:assertj-core:3.25.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.23.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.eclipse.lsp4j:org.eclipse.lsp4j:0.23.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.graalvm.sdk:graal-sdk:22.3.3=runtimeClasspath,testRuntimeClasspath
org.graalvm.truffle:truffle-api:22.3.3=runtimeClasspath,testRuntimeClasspath
org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.7.10=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains:annotations:13.0=apiDependenciesMetadata,compileClasspath,implementationDependenciesMetadata,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.10.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.junit.jupiter:junit-jupiter-engine:5.10.2=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.junit.jupiter:junit-jupiter-params:5.10.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.10.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.junit.platform:junit-platform-engine:1.10.2=testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.junit:junit-bom:5.10.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.organicdesign:Paguro:3.10.3=runtimeClasspath,testRuntimeClasspath
org.snakeyaml:snakeyaml-engine:2.5=runtimeClasspath,testRuntimeClasspath
empty=annotationProcessor,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions
13 changes: 13 additions & 0 deletions pkl-lsp/pkl-lsp.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
pklAllProjects
pklKotlinLibrary
}

dependencies {
implementation(projects.pklCore)
implementation(libs.antlrRuntime)
implementation(libs.lsp4j)
}

tasks.test {
}
132 changes: 132 additions & 0 deletions pkl-lsp/src/main/kotlin/org/pkl/lsp/Builder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.lsp

import java.net.URI
import java.text.MessageFormat
import java.util.*
import java.util.concurrent.CompletableFuture
import org.eclipse.lsp4j.Diagnostic
import org.eclipse.lsp4j.DiagnosticSeverity
import org.eclipse.lsp4j.PublishDiagnosticsParams
import org.pkl.core.parser.LexParseException
import org.pkl.core.parser.Parser
import org.pkl.core.util.IoUtils
import org.pkl.lsp.LSPUtil.toRange
import org.pkl.lsp.analyzers.Analyzer
import org.pkl.lsp.analyzers.AnnotationAnalyzer
import org.pkl.lsp.analyzers.ModifierAnalyzer
import org.pkl.lsp.analyzers.PklDiagnostic
import org.pkl.lsp.ast.Node
import org.pkl.lsp.ast.PklModule
import org.pkl.lsp.ast.PklModuleImpl
import org.pkl.lsp.ast.Span

class Builder(private val server: PklLSPServer) {
private val runningBuild: MutableMap<String, CompletableFuture<PklModule?>> = mutableMapOf()
private val successfulBuilds: MutableMap<String, PklModule> = mutableMapOf()

private val parser = Parser()

private val analyzers: List<Analyzer> =
listOf(ModifierAnalyzer(server), AnnotationAnalyzer(server))

fun runningBuild(uri: String): CompletableFuture<PklModule?> =
runningBuild[uri] ?: CompletableFuture.supplyAsync(::noop)

fun requestBuild(file: URI) {
val change = IoUtils.readString(file.toURL())
requestBuild(file, change)
}

fun requestBuild(file: URI, change: String) {
runningBuild[file.toString()] = CompletableFuture.supplyAsync { build(file, change) }
}

fun lastSuccessfulBuild(uri: String): PklModule? = successfulBuilds[uri]

private fun build(file: URI, change: String): PklModule? {
return try {
server.logger().log("building $file")
val moduleCtx = parser.parseModule(change)
val module = PklModuleImpl(moduleCtx, file)
val diagnostics = analyze(module)
makeDiagnostics(file, diagnostics)
successfulBuilds[file.toString()] = module
return module
} catch (e: LexParseException) {
server.logger().error("Parser Error building $file: ${e.message}")
makeParserDiagnostics(file, listOf(toParserError(e)))
null
} catch (e: Exception) {
server.logger().error("Error building $file: ${e.message} ${e.stackTraceToString()}")
null
}
}

private fun analyze(node: Node): List<Diagnostic> {
return buildList<PklDiagnostic> {
for (analyzer in analyzers) {
analyzer.analyze(node, this)
}
}
}

private fun makeParserDiagnostics(file: URI, errors: List<ParseError>) {
val diags =
errors.map { err ->
val msg = resolveErrorMessage(err.errorType, *err.args)
val diag = Diagnostic(err.span.toRange(), "$msg\n\n")
diag.severity = DiagnosticSeverity.Error
diag.source = "Pkl Language Server"
server.logger().log("diagnostic: $msg at ${err.span}")
diag
}
makeDiagnostics(file, diags)
}

private fun makeDiagnostics(file: URI, diags: List<Diagnostic>) {
server.logger().log("Found ${diags.size} diagnostic errors for $file")
val params = PublishDiagnosticsParams(file.toString(), diags)
// Have to publish diagnostics even if there are no errors, so we clear previous problems
server.client().publishDiagnostics(params)
}

companion object {
private fun noop(): PklModule? {
return null
}

private fun toParserError(ex: LexParseException): ParseError {
val span = Span(ex.line, ex.column, ex.line, ex.column + ex.length)
return ParseError(ex.message ?: "Parser error", span)
}

private fun resolveErrorMessage(key: String, vararg args: Any): String {
val locale = Locale.getDefault()
val bundle = ResourceBundle.getBundle("org.pkl.lsp.errorMessages", locale)
return if (bundle.containsKey(key)) {
val msg = bundle.getString(key)
if (args.isNotEmpty()) {
val formatter = MessageFormat(msg, locale)
formatter.format(args)
} else msg
} else key
}
}
}

class ParseError(val errorType: String, val span: Span, vararg val args: Any)
47 changes: 47 additions & 0 deletions pkl-lsp/src/main/kotlin/org/pkl/lsp/ClientLogger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.lsp

import org.eclipse.lsp4j.MessageParams
import org.eclipse.lsp4j.MessageType
import org.eclipse.lsp4j.services.LanguageClient

class ClientLogger(private val client: LanguageClient, private val verbose: Boolean) {

fun log(msg: String) {
if (!verbose) return
val params = MessageParams(MessageType.Log, msg)
client.logMessage(params)
}

fun warn(msg: String) {
if (!verbose) return
val params = MessageParams(MessageType.Warning, msg)
client.logMessage(params)
}

fun info(msg: String) {
if (!verbose) return
val params = MessageParams(MessageType.Info, msg)
client.logMessage(params)
}

fun error(msg: String) {
if (!verbose) return
val params = MessageParams(MessageType.Error, msg)
client.logMessage(params)
}
}
34 changes: 34 additions & 0 deletions pkl-lsp/src/main/kotlin/org/pkl/lsp/ErrorMessages.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.lsp

import java.text.MessageFormat
import java.util.*

object ErrorMessages {
fun create(messageName: String, vararg args: Any): String {

val locale = Locale.getDefault()
val errorMessage =
ResourceBundle.getBundle("org.pkl.lsp.errorMessages", locale).getString(messageName)

// only format if `errorMessage` is a format string
if (args.isEmpty()) return errorMessage

val formatter = MessageFormat(errorMessage, locale)
return formatter.format(args)
}
}