Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions language-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
implementation(project(":lexer"))
implementation(project(":parser"))
implementation(project(":semantic"))
implementation(project(":samt-config"))
}

application {
Expand Down
21 changes: 21 additions & 0 deletions language-server/src/main/kotlin/tools/samt/ls/SamtConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package tools.samt.ls

import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.isRegularFile

val SAMT_CONFIG_FILE_NAME = Path("samt.yaml")

fun findSamtConfigs(path: Path): List<Path> {
fun Path.parents(): Sequence<Path> = generateSequence(parent) { it.parent }

return path.toFile().walkTopDown()
.map { it.toPath() }
.filter { it.fileName == SAMT_CONFIG_FILE_NAME && it.isRegularFile() }
.ifEmpty {
path.parents()
.map { it.resolve(SAMT_CONFIG_FILE_NAME) }
.filter { it.isRegularFile() }
.take(1)
}.toList()
}
36 changes: 25 additions & 11 deletions language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@ import tools.samt.common.DiagnosticController
import tools.samt.common.DiagnosticMessage
import tools.samt.common.collectSamtFiles
import tools.samt.common.readSamtSource
import tools.samt.config.SamtConfigurationParser
import tools.samt.semantic.SemanticModel
import java.net.URI
import kotlin.io.path.toPath

class SamtFolder(val path: URI) : Iterable<FileInfo> {
class SamtFolder(val configPath: URI, val sourcePath: URI) : Iterable<FileInfo> {
private val files = mutableMapOf<URI, FileInfo>()
var semanticModel: SemanticModel? = null
private set
private var semanticController: DiagnosticController = DiagnosticController(path)
private var semanticController: DiagnosticController = DiagnosticController(sourcePath)

init {
require(configPath.toPath().fileName == SAMT_CONFIG_FILE_NAME)
}

fun set(fileInfo: FileInfo) {
files[fileInfo.sourceFile.path] = fileInfo
require(fileInfo.path.startsWith(sourcePath))
files[fileInfo.path] = fileInfo
}

fun remove(fileUri: URI) {
Expand All @@ -32,7 +39,7 @@ class SamtFolder(val path: URI) : Iterable<FileInfo> {
}

fun buildSemanticModel() {
semanticController = DiagnosticController(path)
semanticController = DiagnosticController(sourcePath)
semanticModel = SemanticModel.build(mapNotNull { it.fileNode }, semanticController)
}

Expand All @@ -47,14 +54,21 @@ class SamtFolder(val path: URI) : Iterable<FileInfo> {
}

companion object {
fun fromDirectory(path: URI): SamtFolder {
val controller = DiagnosticController(path)
val workspace = SamtFolder(path)
val sourceFiles = collectSamtFiles(path).readSamtSource(controller)
sourceFiles.forEach {
workspace.set(parseFile(it))
fun fromConfig(configPath: URI): SamtFolder? {
val folder =
try {
configPath.toPath().let {
val config = SamtConfigurationParser.parseConfiguration(it)
SamtFolder(configPath.normalize(), it.resolveSibling(config.source).normalize().toUri())
}
} catch (e: SamtConfigurationParser.ParseException) {
return null
}
val sourceFiles = collectSamtFiles(folder.sourcePath).readSamtSource(DiagnosticController(folder.sourcePath))
for (file in sourceFiles) {
folder.set(parseFile(file))
}
return workspace
return folder
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import java.io.Closeable
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.logging.Logger
import kotlin.io.path.toPath
import kotlin.system.exitProcess

class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable {
Expand Down Expand Up @@ -89,10 +90,13 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable {
}

private fun buildSamtModel(params: InitializeParams) {
val folders = params.workspaceFolders?.map { it.uri.toPathUri() }.orEmpty()
for (folder in folders) {
workspace.addFolder(SamtFolder.fromDirectory(folder))
}
params.workspaceFolders
?.flatMap { folder ->
val path = folder.uri.toPathUri().toPath()
findSamtConfigs(path).mapNotNull {
SamtFolder.fromConfig(it.toUri())
}
}?.forEach(workspace::addFolder)
}

private fun registerFileWatchCapability() {
Expand All @@ -107,6 +111,9 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable {
globPattern = Either.forLeft("**/")
kind = WatchKind.Create or WatchKind.Delete
},
FileSystemWatcher().apply {
globPattern = Either.forLeft("**/$SAMT_CONFIG_FILE_NAME")
}
)
})
)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import tools.samt.parser.OperationNode
import tools.samt.semantic.*
import java.util.concurrent.CompletableFuture
import java.util.logging.Logger
import kotlin.io.path.toPath

class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocumentService,
LanguageClientAware {
LanguageClientAware {
private lateinit var client: LanguageClient
private val logger = Logger.getLogger("SamtTextDocumentService")

Expand All @@ -24,6 +25,12 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume
val path = params.textDocument.uri.toPathUri()
val text = params.textDocument.text

if (!workspace.containsFile(path)) {
val configPath = findSamtConfigs(path.toPath().parent).singleOrNull()
configPath
?.let { SamtFolder.fromConfig(it.toUri()) }
?.let { workspace.addFolder(it) }
}
workspace.setFile(parseFile(SourceFile(path, text)))
client.updateWorkspace(workspace)
}
Expand Down Expand Up @@ -52,110 +59,110 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume
}

override fun definition(params: DefinitionParams): CompletableFuture<Either<List<Location>, List<LocationLink>>> =
CompletableFuture.supplyAsync {
val path = params.textDocument.uri.toPathUri()
CompletableFuture.supplyAsync {
val path = params.textDocument.uri.toPathUri()

val fileInfo = workspace.getFile(path) ?: return@supplyAsync Either.forRight(emptyList())
val fileInfo = workspace.getFile(path) ?: return@supplyAsync Either.forRight(emptyList())

val fileNode: FileNode = fileInfo.fileNode ?: return@supplyAsync Either.forRight(emptyList())
val semanticModel = workspace.getSemanticModel(path) ?: return@supplyAsync Either.forRight(emptyList())
val globalPackage: Package = semanticModel.global
val fileNode: FileNode = fileInfo.fileNode ?: return@supplyAsync Either.forRight(emptyList())
val semanticModel = workspace.getSemanticModel(path) ?: return@supplyAsync Either.forRight(emptyList())
val globalPackage: Package = semanticModel.global

val token = fileInfo.tokens.findAt(params.position) ?: return@supplyAsync Either.forRight(emptyList())
val token = fileInfo.tokens.findAt(params.position) ?: return@supplyAsync Either.forRight(emptyList())

val filePackage = globalPackage.resolveSubPackage(fileNode.packageDeclaration.name)
val filePackage = globalPackage.resolveSubPackage(fileNode.packageDeclaration.name)

val typeLookup = SamtDeclarationLookup.analyze(fileNode, filePackage, semanticModel.userMetadata)
val type = typeLookup[token.location] ?: return@supplyAsync Either.forRight(emptyList())
val typeLookup = SamtDeclarationLookup.analyze(fileNode, filePackage, semanticModel.userMetadata)
val type = typeLookup[token.location] ?: return@supplyAsync Either.forRight(emptyList())

val definition = type.declaration
val location = definition.location
val definition = type.declaration
val location = definition.location

val targetLocation = when (definition) {
is NamedDeclarationNode -> definition.name.location
is OperationNode -> definition.name.location
else -> error("Unexpected definition type")
}
val locationLink = LocationLink().apply {
targetUri = location.source.path.toString()
targetRange = location.toRange()
targetSelectionRange = targetLocation.toRange()
}
return@supplyAsync Either.forRight(listOf(locationLink))
val targetLocation = when (definition) {
is NamedDeclarationNode -> definition.name.location
is OperationNode -> definition.name.location
else -> error("Unexpected definition type")
}
val locationLink = LocationLink().apply {
targetUri = location.source.path.toString()
targetRange = location.toRange()
targetSelectionRange = targetLocation.toRange()
}
return@supplyAsync Either.forRight(listOf(locationLink))
}

override fun references(params: ReferenceParams): CompletableFuture<List<Location>> =
CompletableFuture.supplyAsync {
val path = params.textDocument.uri.toPathUri()

val relevantFileInfo = workspace.getFile(path) ?: return@supplyAsync emptyList()
val relevantFileNode = relevantFileInfo.fileNode ?: return@supplyAsync emptyList()
val token = relevantFileInfo.tokens.findAt(params.position) ?: return@supplyAsync emptyList()

val (_, files, semanticModel) = workspace.getFolderSnapshot(path) ?: return@supplyAsync emptyList()
if (semanticModel == null) return@supplyAsync emptyList()

val globalPackage = semanticModel.global
val typeLookup = SamtDeclarationLookup.analyze(
relevantFileNode,
globalPackage.resolveSubPackage(relevantFileInfo.fileNode.packageDeclaration.name),
semanticModel.userMetadata
)
val type = typeLookup[token.location] ?: return@supplyAsync emptyList()

val filesAndPackages = buildList {
for (fileInfo in files) {
val fileNode: FileNode = fileInfo.fileNode ?: continue
val filePackage = globalPackage.resolveSubPackage(fileNode.packageDeclaration.name)
add(fileNode to filePackage)
}
CompletableFuture.supplyAsync {
val path = params.textDocument.uri.toPathUri()

val relevantFileInfo = workspace.getFile(path) ?: return@supplyAsync emptyList()
val relevantFileNode = relevantFileInfo.fileNode ?: return@supplyAsync emptyList()
val token = relevantFileInfo.tokens.findAt(params.position) ?: return@supplyAsync emptyList()

val (_, files, semanticModel) = workspace.getFolderSnapshot(path) ?: return@supplyAsync emptyList()
if (semanticModel == null) return@supplyAsync emptyList()

val globalPackage = semanticModel.global
val typeLookup = SamtDeclarationLookup.analyze(
relevantFileNode,
globalPackage.resolveSubPackage(relevantFileInfo.fileNode.packageDeclaration.name),
semanticModel.userMetadata
)
val type = typeLookup[token.location] ?: return@supplyAsync emptyList()

val filesAndPackages = buildList {
for (fileInfo in files) {
val fileNode: FileNode = fileInfo.fileNode ?: continue
val filePackage = globalPackage.resolveSubPackage(fileNode.packageDeclaration.name)
add(fileNode to filePackage)
}
}

val typeReferencesLookup = SamtReferencesLookup.analyze(filesAndPackages, semanticModel.userMetadata)
val typeReferencesLookup = SamtReferencesLookup.analyze(filesAndPackages, semanticModel.userMetadata)

val references = typeReferencesLookup[type] ?: emptyList()
val references = typeReferencesLookup[type] ?: emptyList()

return@supplyAsync references.map { Location(it.source.path.toString(), it.toRange()) }
}
return@supplyAsync references.map { Location(it.source.path.toString(), it.toRange()) }
}

override fun semanticTokensFull(params: SemanticTokensParams): CompletableFuture<SemanticTokens> =
CompletableFuture.supplyAsync {
val path = params.textDocument.uri.toPathUri()

val fileInfo = workspace.getFile(path) ?: return@supplyAsync SemanticTokens(emptyList())

val tokens: List<Token> = fileInfo.tokens
val fileNode: FileNode = fileInfo.fileNode ?: return@supplyAsync SemanticTokens(emptyList())
val semanticModel = workspace.getSemanticModel(path) ?: return@supplyAsync SemanticTokens(emptyList())
val filePackage = semanticModel.global.resolveSubPackage(fileNode.packageDeclaration.name)

val semanticTokens = SamtSemanticTokens.analyze(fileNode, filePackage, semanticModel.userMetadata)

var lastLine = 0
var lastStartChar = 0

val encodedData = buildList {
for (token in tokens) {
val (tokenType, modifier) = semanticTokens[token.location] ?: continue
val (_, start, end) = token.location
val line = start.row
val deltaLine = line - lastLine
val startChar = start.col
val deltaStartChar = if (deltaLine == 0) startChar - lastStartChar else startChar
val length = end.charIndex - start.charIndex
add(deltaLine)
add(deltaStartChar)
add(length)
add(tokenType.ordinal)
add(modifier.bitmask)
lastLine = line
lastStartChar = startChar
}
CompletableFuture.supplyAsync {
val path = params.textDocument.uri.toPathUri()

val fileInfo = workspace.getFile(path) ?: return@supplyAsync SemanticTokens(emptyList())

val tokens: List<Token> = fileInfo.tokens
val fileNode: FileNode = fileInfo.fileNode ?: return@supplyAsync SemanticTokens(emptyList())
val semanticModel = workspace.getSemanticModel(path) ?: return@supplyAsync SemanticTokens(emptyList())
val filePackage = semanticModel.global.resolveSubPackage(fileNode.packageDeclaration.name)

val semanticTokens = SamtSemanticTokens.analyze(fileNode, filePackage, semanticModel.userMetadata)

var lastLine = 0
var lastStartChar = 0

val encodedData = buildList {
for (token in tokens) {
val (tokenType, modifier) = semanticTokens[token.location] ?: continue
val (_, start, end) = token.location
val line = start.row
val deltaLine = line - lastLine
val startChar = start.col
val deltaStartChar = if (deltaLine == 0) startChar - lastStartChar else startChar
val length = end.charIndex - start.charIndex
add(deltaLine)
add(deltaStartChar)
add(length)
add(tokenType.ordinal)
add(modifier.bitmask)
lastLine = line
lastStartChar = startChar
}

SemanticTokens(encodedData)
}

SemanticTokens(encodedData)
}

override fun hover(params: HoverParams): CompletableFuture<Hover> = CompletableFuture.supplyAsync {
val path = params.textDocument.uri.toPathUri()

Expand Down Expand Up @@ -185,7 +192,8 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume
}

private fun UserDeclared.peekDeclaration(): String {
fun List<ServiceType.Operation.Parameter>.toParameterList(): String = joinToString(", ") { it.peekDeclaration() }
fun List<ServiceType.Operation.Parameter>.toParameterList(): String =
joinToString(", ") { it.peekDeclaration() }

return when (this) {
is AliasType -> "${getHumanReadableName<TypealiasToken>()} $humanReadableName"
Expand All @@ -212,6 +220,7 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume
raisesTypes.joinTo(this, ", ") { it.humanReadableName }
}
}

is ServiceType.Operation.Parameter -> "$name: ${type.humanReadableName}"
is RecordType -> "${getHumanReadableName<RecordToken>()} $humanReadableName"
is ServiceType -> "${getHumanReadableName<ServiceToken>()} $humanReadableName"
Expand Down
Loading