Compare commits

...

3 Commits

Author SHA1 Message Date
Irmen de Jong
d999637cdb more fleshed out LSP server 2025-09-17 01:18:48 +02:00
Irmen de Jong
4a78976aff first setup of LSP languageserver 2025-09-17 01:18:48 +02:00
Irmen de Jong
c36799b7c6 implement some more in place pointer operators 2025-09-16 22:37:23 +02:00
12 changed files with 1101 additions and 44 deletions

1
.idea/modules.xml generated
View File

@@ -15,6 +15,7 @@
<module fileurl="file://$PROJECT_DIR$/docs/docs.iml" filepath="$PROJECT_DIR$/docs/docs.iml" />
<module fileurl="file://$PROJECT_DIR$/examples/examples.iml" filepath="$PROJECT_DIR$/examples/examples.iml" />
<module fileurl="file://$PROJECT_DIR$/intermediate/intermediate.iml" filepath="$PROJECT_DIR$/intermediate/intermediate.iml" />
<module fileurl="file://$PROJECT_DIR$/languageServer/languageServer.iml" filepath="$PROJECT_DIR$/languageServer/languageServer.iml" />
<module fileurl="file://$PROJECT_DIR$/parser/parser.iml" filepath="$PROJECT_DIR$/parser/parser.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/prog8.iml" filepath="$PROJECT_DIR$/.idea/modules/prog8.iml" />
<module fileurl="file://$PROJECT_DIR$/simpleAst/simpleAst.iml" filepath="$PROJECT_DIR$/simpleAst/simpleAst.iml" />

View File

@@ -282,22 +282,26 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
"%" -> TODO("inplace ptr %")
"<<" -> {
if(target.dt.isByte) TODO("inplaceByteShiftLeft(target, value) ${target.position}")
if(target.dt.isByte) inplaceByteShiftLeft(target, value)
else if(target.dt.isWord) inplaceWordShiftLeft(target, value)
else throw AssemblyError("weird dt ${target.position}")
}
">>" -> {
if(target.dt.isByte) TODO("inplaceByteShiftRight(target, value) ${target.position}")
if(target.dt.isByte) inplaceByteShiftRight(target, value)
else if(target.dt.isWord) inplaceWordShiftRight(target, value)
else throw AssemblyError("weird dt ${target.position}")
}
"&", "and" -> {
// byte targets are handled as direct memory access, not a pointer operation anymore however boolean targets are still to be handled here
TODO("inplace ptr &")
if(target.dt.isByteOrBool) inplaceByteAnd(target, value)
else if(target.dt.isWord) inplaceWordAnd(target, value)
else throw AssemblyError("weird dt ${target.dt} ${target.position}")
}
"|", "or" -> {
// byte targets are handled as direct memory access, not a pointer operation anymore however boolean targets are still to be handled here
TODO("inplace ptr |")
if(target.dt.isByteOrBool) inplaceByteOr(target, value)
else if(target.dt.isWord) inplaceWordOr(target, value)
else throw AssemblyError("weird dt ${target.dt} ${target.position}")
}
"^", "xor" -> {
// byte targets are handled as direct memory access, not a pointer operation anymore however boolean targets are still to be handled here
@@ -305,12 +309,6 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
else if(target.dt.isWord) inplaceWordXor(target, value)
else throw AssemblyError("weird dt ${target.dt} ${target.position}")
}
"==" -> TODO("inplace ptr ==")
"!=" -> TODO("inplace ptr !=")
"<" -> TODO("inplace ptr <")
"<=" -> TODO("inplace ptr <=")
">" -> TODO("inplace ptr >")
">=" -> TODO("inplace ptr >=")
else -> throw AssemblyError("invalid operator for in-place modification $operator")
}
}
@@ -693,7 +691,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
val (zpPtrVar, offset) = deref(target.pointer)
if(target.dt.isSigned)
TODO("signed word shift rigth ${target.position} $value")
TODO("signed word shift right ${target.position} $value")
fun shift1unsigned() {
asmgen.out("""
@@ -720,21 +718,92 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
}
SourceStorageKind.VARIABLE -> {
require(value.datatype.isWord)
require(value.datatype.isByte)
val varname = value.asmVarname
TODO("<< variable")
asmgen.out(" ldx $varname")
asmgen.out("-")
shift1unsigned()
asmgen.out(" dex | bne -")
}
SourceStorageKind.EXPRESSION -> {
require(value.datatype.isWord)
asmgen.assignExpressionToRegister(value.expression!!, RegisterOrPair.AX)
TODO("<< expression")
require(value.datatype.isByte)
asmgen.assignExpressionToRegister(value.expression!!, RegisterOrPair.X)
asmgen.out("-")
shift1unsigned()
asmgen.out(" dex | bne -")
}
SourceStorageKind.REGISTER -> {
require(value.datatype.isWord)
require(value.datatype.isByte)
val register = value.register!!
asmgen.assignRegister(register, AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, DataType.UWORD, null, target.position, variableAsmName = "P8ZP_SCRATCH_PTR"))
require(register.isWord())
TODO("<< register")
asmgen.assignRegister(register, AsmAssignTarget(TargetStorageKind.REGISTER, asmgen, DataType.UBYTE, null, target.position, register = RegisterOrPair.X))
asmgen.out("-")
shift1unsigned()
asmgen.out(" dex | bne -")
}
else -> throw AssemblyError("weird source value $value")
}
}
private fun inplaceByteShiftRight(target: PtrTarget, value: AsmAssignSource) {
val (zpPtrVar, offset) = deref(target.pointer)
if(target.dt.isSigned)
TODO("signed byte shift right ${target.position} $value")
when(value.kind) {
SourceStorageKind.LITERALNUMBER -> {
val number = value.number!!.number.toInt()
if(number==1) {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
lsr a
sta ($zpPtrVar),y""")
} else if(number>1) {
asmgen.out("""
ldx #$number
ldy #$offset
lda ($zpPtrVar),y
- lsr a
dex
bne -
sta ($zpPtrVar),y""")
}
}
SourceStorageKind.VARIABLE -> {
require(value.datatype.isByte)
val varname = value.asmVarname
asmgen.out("""
ldx $varname
ldy #$offset
lda ($zpPtrVar),y
- lsr a
dex
bne -
sta ($zpPtrVar),y""")
}
SourceStorageKind.EXPRESSION -> {
require(value.datatype.isByte)
asmgen.assignExpressionToRegister(value.expression!!, RegisterOrPair.X)
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
- lsr a
dex
bne -
sta ($zpPtrVar),y""")
}
SourceStorageKind.REGISTER -> {
require(value.datatype.isByte)
val register = value.register!!
asmgen.assignRegister(register, AsmAssignTarget(TargetStorageKind.REGISTER, asmgen, DataType.UWORD, null, target.position, register = RegisterOrPair.X))
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
- lsr a
dex
bne -
sta ($zpPtrVar),y""")
}
else -> throw AssemblyError("weird source value $value")
}
@@ -768,21 +837,90 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
}
SourceStorageKind.VARIABLE -> {
require(value.datatype.isWord)
require(value.datatype.isByte)
val varname = value.asmVarname
TODO("<< variable")
asmgen.out(" ldx $varname")
asmgen.out("-")
shift1()
asmgen.out(" dex | bne -")
}
SourceStorageKind.EXPRESSION -> {
require(value.datatype.isWord)
asmgen.assignExpressionToRegister(value.expression!!, RegisterOrPair.AX)
TODO("<< expression")
require(value.datatype.isByte)
asmgen.assignExpressionToRegister(value.expression!!, RegisterOrPair.X)
asmgen.out("-")
shift1()
asmgen.out(" dex | bne -")
}
SourceStorageKind.REGISTER -> {
require(value.datatype.isWord)
require(value.datatype.isByte)
val register = value.register!!
asmgen.assignRegister(register, AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, DataType.UWORD, null, target.position, variableAsmName = "P8ZP_SCRATCH_PTR"))
require(register.isWord())
TODO("<< register")
asmgen.assignRegister(register, AsmAssignTarget(TargetStorageKind.REGISTER, asmgen, DataType.UBYTE, null, target.position, register = RegisterOrPair.X))
asmgen.out("-")
shift1()
asmgen.out(" dex | bne -")
}
else -> throw AssemblyError("weird source value $value")
}
}
private fun inplaceByteShiftLeft(target: PtrTarget, value: AsmAssignSource) {
val (zpPtrVar, offset) = deref(target.pointer)
when(value.kind) {
SourceStorageKind.LITERALNUMBER -> {
val number = value.number!!.number.toInt()
if(number==1) {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
asl a
sta ($zpPtrVar),y""")
} else if(number>1) {
asmgen.out("""
ldx #$number
ldy #$offset
lda ($zpPtrVar),y
- asl a
dex
bne -
sta ($zpPtrVar),y""")
}
}
SourceStorageKind.VARIABLE -> {
require(value.datatype.isByte)
val varname = value.asmVarname
asmgen.out("""
ldx $varname
ldy #$offset
lda ($zpPtrVar),y
- asl a
dex
bne -
sta ($zpPtrVar),y""")
}
SourceStorageKind.EXPRESSION -> {
require(value.datatype.isByte)
asmgen.assignExpressionToRegister(value.expression!!, RegisterOrPair.X)
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
- asl a
dex
bne -
sta ($zpPtrVar),y""")
}
SourceStorageKind.REGISTER -> {
require(value.datatype.isByte)
val register = value.register!!
asmgen.assignRegister(register, AsmAssignTarget(TargetStorageKind.REGISTER, asmgen, DataType.UBYTE, null, target.position, register = RegisterOrPair.X))
asmgen.out("-")
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
- asl a
dex
bne -
sta ($zpPtrVar),y""")
}
else -> throw AssemblyError("weird source value $value")
}
@@ -1230,6 +1368,180 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
}
private fun inplaceByteOr(target: PtrTarget, value: AsmAssignSource) {
val (zpPtrVar, offset) = deref(target.pointer)
when(value.kind) {
SourceStorageKind.LITERALNUMBER -> {
val number = value.number!!.number.toInt()
if(offset==0.toUByte() && asmgen.isTargetCpu(CpuType.CPU65C02))
asmgen.out("""
lda ($zpPtrVar)
ora #$number
sta ($zpPtrVar)""")
else
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
ora #$number
sta ($zpPtrVar),y""")
}
SourceStorageKind.VARIABLE -> {
val varname = value.asmVarname
if(offset==0.toUByte() && asmgen.isTargetCpu(CpuType.CPU65C02))
asmgen.out("""
lda ($zpPtrVar)
ora $varname
sta ($zpPtrVar)""")
else
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
ora $varname
sta ($zpPtrVar),y""")
}
SourceStorageKind.EXPRESSION -> {
asmgen.assignExpressionToRegister(value.expression!!, RegisterOrPair.A)
asmgen.out("""
ldy #$offset
ora ($zpPtrVar),y
sta ($zpPtrVar),y""")
}
SourceStorageKind.REGISTER -> TODO("register | byte")
else -> throw AssemblyError("weird source value $value")
}
}
private fun inplaceWordOr(target: PtrTarget, value: AsmAssignSource) {
val (zpPtrVar, offset) = deref(target.pointer)
when(value.kind) {
SourceStorageKind.LITERALNUMBER -> {
val number = value.number!!.number.toInt()
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
ora #<$number
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
ora #>$number
sta ($zpPtrVar),y""")
}
SourceStorageKind.VARIABLE -> {
require(value.datatype.isWord)
val varname = value.asmVarname
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
ora $varname
sta ($zpPtrVar),y
lda ($zpPtrVar),y
ora $varname+1
sta ($zpPtrVar),y""")
}
SourceStorageKind.EXPRESSION -> {
require(value.datatype.isWord)
asmgen.assignExpressionToRegister(value.expression!!, RegisterOrPair.AX)
asmgen.out("""
ldy #$offset
ora ($zpPtrVar),y
sta ($zpPtrVar),y
iny
txa
ora ($zpPtrVar),y
sta ($zpPtrVar),y""")
}
SourceStorageKind.REGISTER -> TODO("register | word")
else -> throw AssemblyError("weird source value $value")
}
}
private fun inplaceByteAnd(target: PtrTarget, value: AsmAssignSource) {
val (zpPtrVar, offset) = deref(target.pointer)
when(value.kind) {
SourceStorageKind.LITERALNUMBER -> {
val number = value.number!!.number.toInt()
if(offset==0.toUByte() && asmgen.isTargetCpu(CpuType.CPU65C02))
asmgen.out("""
lda ($zpPtrVar)
and #$number
sta ($zpPtrVar)""")
else
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
and #$number
sta ($zpPtrVar),y""")
}
SourceStorageKind.VARIABLE -> {
val varname = value.asmVarname
if(offset==0.toUByte() && asmgen.isTargetCpu(CpuType.CPU65C02))
asmgen.out("""
lda ($zpPtrVar)
and $varname
sta ($zpPtrVar)""")
else
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
and $varname
sta ($zpPtrVar),y""")
}
SourceStorageKind.EXPRESSION -> {
asmgen.assignExpressionToRegister(value.expression!!, RegisterOrPair.A)
asmgen.out("""
ldy #$offset
and ($zpPtrVar),y
sta ($zpPtrVar),y""")
}
SourceStorageKind.REGISTER -> TODO("register & byte")
else -> throw AssemblyError("weird source value $value")
}
}
private fun inplaceWordAnd(target: PtrTarget, value: AsmAssignSource) {
val (zpPtrVar, offset) = deref(target.pointer)
when(value.kind) {
SourceStorageKind.LITERALNUMBER -> {
val number = value.number!!.number.toInt()
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
and #<$number
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
and #>$number
sta ($zpPtrVar),y""")
}
SourceStorageKind.VARIABLE -> {
require(value.datatype.isWord)
val varname = value.asmVarname
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
and $varname
sta ($zpPtrVar),y
lda ($zpPtrVar),y
and $varname+1
sta ($zpPtrVar),y""")
}
SourceStorageKind.EXPRESSION -> {
require(value.datatype.isWord)
asmgen.assignExpressionToRegister(value.expression!!, RegisterOrPair.AX)
asmgen.out("""
ldy #$offset
and ($zpPtrVar),y
sta ($zpPtrVar),y
iny
txa
and ($zpPtrVar),y
sta ($zpPtrVar),y""")
}
SourceStorageKind.REGISTER -> TODO("register & word")
else -> throw AssemblyError("weird source value $value")
}
}
fun assignIndexedPointer(target: AsmAssignTarget, arrayVarName: String, index: PtExpression, arrayDt: DataType) {
TODO("assign indexed pointer from array $arrayVarName at ${target.position}")
val ptrZp = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, DataType.UWORD, target.scope, target.position, variableAsmName="P8ZP_SCRATCH_PTR")

View File

@@ -90,6 +90,7 @@ Future Things and Ideas
7 }
8 modifications.forEach { it.perform() }
9 }
- improve ANTLR grammar with better error handling (according to Qwen AI)
- allow memory() to occur in array initializer
- %breakpoint after an assignment is parsed as part of the expression (x % breakpoint), that should not happen
- when a complete block is removed because unused, suppress all info messages about everything in the block being removed

View File

@@ -13,20 +13,24 @@ main {
}
sub start() {
^^Node[] @shared nodeswithtype = [
^^Node: [1,"one", 1000, true, 1.111],
^^Node: [],
]
^^Node test = []
^^Node derp2 = ^^Foobar: []
^^Node[] @shared nodeswithout = [
[2,"two", 2000, false, 2.222],
[1,2,3,true,5],
[]
]
^^Node @shared nptrwithtype = ^^Node : [1, "one", 1000, false, 3.333]
^^Node @shared nptrwithouttype = [1, "one", 1000, false, 3.333]
test.id ++
test.array += 1000
test.id <<= 2
test.id <<= cx16.r0L
test.id >>= 3
test.id >>= cx16.r0L
test.id &= 1
test.id *= 5 ; TODO implement this
test.id /= 5 ; TODO implement this
test.array ^= 1000
test.array |= 1000
test.array &= 1000
test.array >>= 3
test.array >>= cx16.r0L
test.array <<= 2
test.array <<= cx16.r0L
test.array *= 5
}
}

View File

@@ -0,0 +1,105 @@
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.24.0")
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.24.0")
// For JSON processing if needed
//implementation("com.google.code.gson:gson:2.10.1")
// For more advanced text processing
//implementation("org.apache.commons:commons-lang3:3.12.0")
implementation(project(":compiler"))
}
configurations.forEach { config ->
config.resolutionStrategy {
preferProjectModules()
}
}
sourceSets.main {
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> {
// Disable tests for now since we don't have any
enabled = false
}
tasks.installDist {
finalizedBy("fixFilePermissions")
}
tasks.build {
finalizedBy("installDist")
}

View File

@@ -0,0 +1,14 @@
<?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="eclipse.lsp4j" level="project" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
<orderEntry type="module" module-name="compiler" />
</component>
</module>

View File

@@ -0,0 +1,34 @@
package prog8lsp
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.function.Supplier
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)
}
}
}

View File

@@ -0,0 +1,22 @@
package prog8lsp
import org.eclipse.lsp4j.launch.LSPLauncher
import prog8.buildversion.VERSION
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.newCachedThreadPool { Thread(it, "client") }
val launcher = LSPLauncher.createServerLauncher(server, inStream, outStream, threads) { it }
server.connect(launcher.remoteProxy)
launcher.startListening()
println("Prog8 Language Server started. Prog8 version: ${VERSION}")
}

View File

@@ -0,0 +1,85 @@
package prog8lsp
import org.eclipse.lsp4j.*
import org.eclipse.lsp4j.jsonrpc.messages.Either
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")
val result = InitializeResult()
val capabilities = ServerCapabilities()
// Text document synchronization
capabilities.textDocumentSync = Either.forLeft(TextDocumentSyncKind.Full)
// Completion support
val completionOptions = CompletionOptions()
completionOptions.resolveProvider = true
completionOptions.triggerCharacters = listOf(".", ":")
capabilities.completionProvider = completionOptions
// Document symbol support
capabilities.documentSymbolProvider = Either.forLeft(true)
// Hover support
capabilities.hoverProvider = Either.forLeft(true)
// Definition support
capabilities.definitionProvider = Either.forLeft(true)
// Code action support
val codeActionOptions = CodeActionOptions()
codeActionOptions.codeActionKinds = listOf(CodeActionKind.QuickFix)
capabilities.codeActionProvider = Either.forRight(codeActionOptions)
// Document formatting support
capabilities.documentFormattingProvider = Either.forLeft(true)
capabilities.documentRangeFormattingProvider = Either.forLeft(true)
// Rename support
val renameOptions = RenameOptions()
renameOptions.prepareProvider = true
capabilities.renameProvider = Either.forRight(renameOptions)
// Workspace symbol support
capabilities.workspaceSymbolProvider = Either.forLeft(true)
result.capabilities = capabilities
result
}
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)
}
}

View File

@@ -0,0 +1,389 @@
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
// Document model to maintain in memory
data class Prog8Document(
val uri: String,
var text: String,
var version: Int
)
class Prog8TextDocumentService: TextDocumentService {
private var client: LanguageClient? = null
private val async = AsyncExecutor()
private val logger = Logger.getLogger(Prog8TextDocumentService::class.simpleName)
// In-memory document store
private val documents = mutableMapOf<String, Prog8Document>()
fun connect(client: LanguageClient) {
this.client = client
}
override fun didOpen(params: DidOpenTextDocumentParams) {
logger.info("didOpen: ${params.textDocument.uri}")
// Create and store document model
val document = Prog8Document(
uri = params.textDocument.uri,
text = params.textDocument.text,
version = params.textDocument.version
)
documents[params.textDocument.uri] = document
// Trigger diagnostics when a document is opened
validateDocument(document)
}
override fun didChange(params: DidChangeTextDocumentParams) {
logger.info("didChange: ${params.textDocument.uri}")
// Get the document from our store
val document = documents[params.textDocument.uri]
if (document != null) {
// Update document version
document.version = params.textDocument.version
// Apply changes to the document text
// For simplicity, we're assuming full document sync (TextDocumentSyncKind.Full)
// In a real implementation, you might need to handle incremental changes
val text = params.contentChanges.firstOrNull()?.text
if (text != null) {
document.text = text
}
// Trigger diagnostics when a document changes
validateDocument(document)
}
}
override fun didClose(params: DidCloseTextDocumentParams) {
logger.info("didClose: ${params.textDocument.uri}")
// Remove document from our store
documents.remove(params.textDocument.uri)
// Clear diagnostics when a document is closed
client?.publishDiagnostics(PublishDiagnosticsParams(params.textDocument.uri, listOf()))
}
override fun didSave(params: DidSaveTextDocumentParams) {
logger.info("didSave: ${params.textDocument.uri}")
// Handle save events if needed
}
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()
// Get document from our store
val document = documents[params.textDocument.uri]
if (document != null) {
// Parse document and extract symbols
// This is just a placeholder implementation
val range = Range(Position(0, 0), Position(0, 10))
val selectionRange = Range(Position(0, 0), Position(0, 10))
val symbol = DocumentSymbol("exampleSymbol", SymbolKind.Function, range, selectionRange)
result.add(Either.forRight(symbol))
}
}
logger.info("Finished in $time ms")
result
}
override fun completion(params: CompletionParams): CompletableFuture<Either<MutableList<CompletionItem>, CompletionList>> = async.compute {
logger.info("Completion for ${params.textDocument.uri} at ${params.position}")
val result: Either<MutableList<CompletionItem>, CompletionList>
val time = measureTimeMillis {
val items = mutableListOf<CompletionItem>()
// Get document from our store
val document = documents[params.textDocument.uri]
if (document != null) {
// Implement actual completion logic based on context
// This is just a placeholder implementation
val printItem = CompletionItem("print")
printItem.kind = CompletionItemKind.Function
printItem.detail = "Print text to console"
printItem.documentation = Either.forLeft("Outputs the given text to the console")
val forItem = CompletionItem("for")
forItem.kind = CompletionItemKind.Keyword
forItem.detail = "For loop"
forItem.documentation = Either.forLeft("Iterates over a range or collection")
val ifItem = CompletionItem("if")
ifItem.kind = CompletionItemKind.Keyword
ifItem.detail = "Conditional statement"
ifItem.documentation = Either.forLeft("Executes code based on a condition")
items.add(printItem)
items.add(forItem)
items.add(ifItem)
}
val list = CompletionList(false, items)
result = Either.forRight(list)
}
logger.info("Finished in $time ms")
result
}
override fun hover(params: HoverParams): CompletableFuture<Hover?> = async.compute {
logger.info("Hover for ${params.textDocument.uri} at ${params.position}")
// Get document from our store
val document = documents[params.textDocument.uri]
if (document != null) {
// Simple implementation that checks for keywords at the position
val keyword = getWordAtPosition(document, params.position)
when (keyword) {
"print" -> {
val hover = Hover()
hover.contents = Either.forLeft(listOf(Either.forLeft("**print** - Outputs text to the console\n\n```prog8\nprint \"Hello, World!\"\n```")))
return@compute hover
}
"for" -> {
val hover = Hover()
hover.contents = Either.forLeft(listOf(Either.forLeft("**for** - Loop construct\n\n```prog8\nfor i in 0..10 {\n print i\n}\n```")))
return@compute hover
}
"if" -> {
val hover = Hover()
hover.contents = Either.forLeft(listOf(Either.forLeft("**if** - Conditional statement\n\n```prog8\nif x > 5 {\n print \"x is greater than 5\"\n}\n```")))
return@compute hover
}
"sub" -> {
val hover = Hover()
hover.contents = Either.forLeft(listOf(Either.forLeft("**sub** - Defines a subroutine\n\n```prog8\nsub myFunction() {\n print \"Hello from function\"\n}\n```")))
return@compute hover
}
else -> {
// Return null for unknown symbols
return@compute null
}
}
}
// Return null if document not found
null
}
override fun definition(params: DefinitionParams): CompletableFuture<Either<MutableList<out Location>, MutableList<out LocationLink>>> = async.compute {
logger.info("Definition request for ${params.textDocument.uri} at ${params.position}")
// Get document from our store
val document = documents[params.textDocument.uri]
if (document != null) {
// Implement actual definition lookup
// This would involve parsing the document, finding the symbol at the position,
// and then finding where that symbol is defined
val locations = mutableListOf<Location>()
// Placeholder implementation
// locations.add(Location("file:///path/to/definition.p8", Range(Position(0, 0), Position(0, 10))))
return@compute Either.forLeft(locations)
}
Either.forLeft(mutableListOf<Location>())
}
override fun formatting(params: DocumentFormattingParams): CompletableFuture<MutableList<out TextEdit>> = async.compute {
logger.info("Formatting document ${params.textDocument.uri}")
// Get document from our store
val document = documents[params.textDocument.uri]
if (document != null) {
// Implement actual code formatting
// This is just a placeholder implementation
val edits = mutableListOf<TextEdit>()
// Example of how you might implement formatting:
// 1. Parse the document
// 2. Apply formatting rules (indentation, spacing, etc.)
// 3. Generate TextEdit objects for the changes
return@compute edits
}
mutableListOf<TextEdit>()
}
override fun rangeFormatting(params: DocumentRangeFormattingParams): CompletableFuture<MutableList<out TextEdit>> = async.compute {
logger.info("Range formatting document ${params.textDocument.uri}")
// Get document from our store
val document = documents[params.textDocument.uri]
if (document != null) {
// Implement actual code formatting for range
// This is just a placeholder implementation
val edits = mutableListOf<TextEdit>()
// Example of how you might implement range formatting:
// 1. Parse the document range
// 2. Apply formatting rules to the selected range
// 3. Generate TextEdit objects for the changes
return@compute edits
}
mutableListOf<TextEdit>()
}
override fun rename(params: RenameParams): CompletableFuture<WorkspaceEdit> = async.compute {
logger.info("Rename symbol in ${params.textDocument.uri} at ${params.position}")
// Get document from our store
val document = documents[params.textDocument.uri]
if (document != null) {
// Implement actual rename functionality
// This would involve:
// 1. Finding all references to the symbol at the given position
// 2. Creating TextEdit objects to rename each reference
// 3. Adding the edits to a WorkspaceEdit
return@compute WorkspaceEdit()
}
WorkspaceEdit()
}
override fun codeAction(params: CodeActionParams): CompletableFuture<MutableList<Either<Command, CodeAction>>> = async.compute {
logger.info("Code actions for ${params.textDocument.uri}")
// Get document from our store
val document = documents[params.textDocument.uri]
if (document != null) {
val actions = mutableListOf<Either<Command, CodeAction>>()
// Check diagnostics to provide quick fixes
for (diagnostic in params.context.diagnostics) {
when (diagnostic.code?.left) {
"UnmatchedQuotes" -> {
val action = CodeAction()
action.title = "Add closing quote"
action.kind = CodeActionKind.QuickFix
action.diagnostics = listOf(diagnostic)
action.isPreferred = true
// TODO: Add actual TextEdit to fix the issue
actions.add(Either.forRight(action))
}
"InvalidCharacter" -> {
val action = CodeAction()
action.title = "Remove invalid characters"
action.kind = CodeActionKind.QuickFix
action.diagnostics = listOf(diagnostic)
// TODO: Add actual TextEdit to fix the issue
actions.add(Either.forRight(action))
}
}
}
// Add some general code actions
val organizeImportsAction = CodeAction()
organizeImportsAction.title = "Organize imports"
organizeImportsAction.kind = CodeActionKind.SourceOrganizeImports
actions.add(Either.forRight(organizeImportsAction))
return@compute actions
}
mutableListOf<Either<Command, CodeAction>>()
}
private fun getWordAtPosition(document: Prog8Document, position: Position): String {
// Extract the word at the given position from the document text
val lines = document.text.lines()
if (position.line < lines.size) {
val line = lines[position.line]
// Simple word extraction - in a real implementation, you'd want a more robust solution
val words = line.split(Regex("\\s+|[^a-zA-Z0-9_]"))
var charIndex = 0
for (word in words) {
if (position.character >= charIndex && position.character <= charIndex + word.length) {
return word
}
charIndex += word.length + 1 // +1 for the separator
}
}
return "" // Default to empty string
}
private fun validateDocument(document: Prog8Document) {
logger.info("Validating document: ${document.uri}")
val diagnostics = mutableListOf<Diagnostic>()
// Split text into lines for easier processing
val lines = document.text.lines()
// Check for syntax errors
for ((lineNumber, line) in lines.withIndex()) {
// Check for unmatched quotes
val quoteCount = line.count { it == '"' }
if (quoteCount % 2 != 0) {
val range = Range(Position(lineNumber, 0), Position(lineNumber, line.length))
val diagnostic = Diagnostic(
range,
"Unmatched quotes",
DiagnosticSeverity.Error,
"prog8-lsp",
"UnmatchedQuotes"
)
diagnostics.add(diagnostic)
}
// Check for invalid characters
if (line.contains(Regex("[^\\u0000-\\u007F]"))) {
val range = Range(Position(lineNumber, 0), Position(lineNumber, line.length))
val diagnostic = Diagnostic(
range,
"Invalid character found",
DiagnosticSeverity.Error,
"prog8-lsp",
"InvalidCharacter"
)
diagnostics.add(diagnostic)
}
// Check for common Prog8 syntax issues
// For example, check if a line starts with a keyword but doesn't follow proper syntax
if (line.trim().startsWith("sub ") && !line.contains("(")) {
val range = Range(Position(lineNumber, 0), Position(lineNumber, line.length))
val diagnostic = Diagnostic(
range,
"Subroutine declaration missing parentheses",
DiagnosticSeverity.Error,
"prog8-lsp",
"InvalidSubroutine"
)
diagnostics.add(diagnostic)
}
}
// Check for other issues
if (document.text.contains("error")) {
val range = Range(Position(0, 0), Position(0, 5))
val diagnostic = Diagnostic(
range,
"This is a sample diagnostic",
DiagnosticSeverity.Warning,
"prog8-lsp",
"SampleDiagnostic"
)
diagnostics.add(diagnostic)
}
client?.publishDiagnostics(PublishDiagnosticsParams(document.uri, diagnostics))
}
}

View File

@@ -0,0 +1,89 @@
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")
// TODO: Implement workspace symbol search
// This is just a placeholder implementation
val symbols = mutableListOf<WorkspaceSymbol>()
val symbol = WorkspaceSymbol(
"workspaceSymbol",
SymbolKind.Function,
Either.forLeft(Location("file:///example.p8", Range(Position(0, 0), Position(0, 10))))
)
symbols.add(symbol)
return CompletableFuture.completedFuture(Either.forRight(symbols))
}
override fun resolveWorkspaceSymbol(workspaceSymbol: WorkspaceSymbol): CompletableFuture<WorkspaceSymbol> {
logger.info("resolveWorkspaceSymbol $workspaceSymbol")
return CompletableFuture.completedFuture(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)
}
}

View File

@@ -10,5 +10,6 @@ include(
':codeGenCpu6502',
':codeGenExperimental',
':compiler',
':beanshell'
':beanshell',
':languageServer'
)