Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5d37d3c
feat(ls): extract repeated workspace logic
mjossdev May 11, 2023
e049219
feat(ls): react to changes on file system
mjossdev May 11, 2023
c69710f
feat(ls): rename SamtWorkspace::add to set
mjossdev May 11, 2023
c062503
test(ls): add test for getByFile
mjossdev May 12, 2023
4597bd5
feat(ls): add workingDirectory property to SamtWorkspace
mjossdev May 12, 2023
c01c36c
feat(ls): read closed files from disk
mjossdev May 12, 2023
28b0dd0
feat(ls): handle file rename, creation and deletion
mjossdev May 12, 2023
5963d02
feat(ls): editor state when file is opened
mjossdev May 12, 2023
99bf147
feat(ls): ensure that only directories and SAMT files are handled
mjossdev May 12, 2023
7cf8352
feat(ls): support didChangeWorkspaceFolders
mjossdev May 13, 2023
14074f4
fix(ls): handle lexer errors
mjossdev May 13, 2023
2139870
test(ls): test parseFile
mjossdev May 13, 2023
0c8c1d8
test(ls): test SamtWorkspace
mjossdev May 13, 2023
c99e1a7
test(ls): test Mapping.kt
mjossdev May 14, 2023
24565b3
refactor(ls): rename SamtWorkspace to SamtFolder
mjossdev May 14, 2023
42d3171
refactor(ls): introduce new workspace abstraction
mjossdev May 14, 2023
cb5c962
feat(ls): incorporate review feedback
mjossdev May 18, 2023
118ea74
test(ls): test SamtWorkspace abstraction
mjossdev May 19, 2023
d433b4c
test(cli): test typealias dump
mjossdev May 19, 2023
617e689
test(cli): increase AST printer test coverage
mjossdev May 19, 2023
790c38a
chore(deps): upgrade kover to 0.7.0
mjossdev May 19, 2023
af91012
fix(test): fix failing test on windows
mjossdev May 19, 2023
737cac2
feat(ls): check if file has changed before reparsing
mjossdev May 19, 2023
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
55 changes: 41 additions & 14 deletions cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,19 @@ class ASTPrinterTest {
@Description("This is a service")
service MyService {
testmethod(foo: A): B
oneway testmethod2(foo: A)
}

provide MyEndpoint {
implements MyService
transport HTTP
transport HTTP {
operations: {
MyService: {
testmethod: "POST /testmethod",
testmethod2: "POST /testmethod2"
}
}
}
}

consume MyEndpoint {
Expand Down Expand Up @@ -105,22 +113,41 @@ class ASTPrinterTest {
│ │ │ └─IdentifierNode A <17:19>
│ │ └─BundleIdentifierNode B <17:23>
│ │ └─IdentifierNode B <17:23>
│ ├─OnewayOperationNode <18:3>
│ │ ├─IdentifierNode testmethod2 <18:10>
│ │ └─OperationParameterNode <18:22>
│ │ ├─IdentifierNode foo <18:22>
│ │ └─BundleIdentifierNode A <18:27>
│ │ └─IdentifierNode A <18:27>
│ └─AnnotationNode <15:1>
│ ├─IdentifierNode Description <15:2>
│ └─StringNode "This is a service" <15:14>
├─ProviderDeclarationNode <20:1>
│ ├─IdentifierNode MyEndpoint <20:9>
│ ├─ProviderImplementsNode <21:3>
│ │ └─BundleIdentifierNode MyService <21:14>
│ │ └─IdentifierNode MyService <21:14>
│ └─ProviderTransportNode <22:3>
│ └─IdentifierNode HTTP <22:13>
└─ConsumerDeclarationNode <25:1>
├─BundleIdentifierNode MyEndpoint <25:9>
│ └─IdentifierNode MyEndpoint <25:9>
└─ConsumerUsesNode <26:3>
└─BundleIdentifierNode MyService <26:8>
└─IdentifierNode MyService <26:8>
├─ProviderDeclarationNode <21:1>
│ ├─IdentifierNode MyEndpoint <21:9>
│ ├─ProviderImplementsNode <22:3>
│ │ └─BundleIdentifierNode MyService <22:14>
│ │ └─IdentifierNode MyService <22:14>
│ └─ProviderTransportNode <23:3>
│ ├─IdentifierNode HTTP <23:13>
│ └─ObjectNode <23:18>
│ └─ObjectFieldNode <24:5>
│ ├─IdentifierNode operations <24:5>
│ └─ObjectNode <24:17>
│ └─ObjectFieldNode <25:9>
│ ├─IdentifierNode MyService <25:9>
│ └─ObjectNode <25:20>
│ ├─ObjectFieldNode <26:13>
│ │ ├─IdentifierNode testmethod <26:13>
│ │ └─StringNode "POST /testmethod" <26:25>
│ └─ObjectFieldNode <27:13>
│ ├─IdentifierNode testmethod2 <27:13>
│ └─StringNode "POST /testmethod2" <27:26>
└─ConsumerDeclarationNode <33:1>
├─BundleIdentifierNode MyEndpoint <33:9>
│ └─IdentifierNode MyEndpoint <33:9>
└─ConsumerUsesNode <34:3>
└─BundleIdentifierNode MyService <34:8>
└─IdentifierNode MyService <34:8>
""".trimIndent().trim(), dumpWithoutColorCodes.trimIndent().trim())
}

Expand Down
5 changes: 4 additions & 1 deletion cli/src/test/kotlin/tools/samt/cli/TypePrinterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import kotlin.test.assertFalse

class TypePrinterTest {
@Test
fun `correctly formats an AST dump`() {
fun `correctly formats a type dump`() {
val stuffPackage = parse("""
package test.stuff

Expand All @@ -33,6 +33,8 @@ class TypePrinterTest {
implements MyService
transport HTTP
}

typealias F = E
""".trimIndent())
val consumerPackage = parse("""
package test.other.company
Expand All @@ -57,6 +59,7 @@ class TypePrinterTest {
├─stuff
│ enum E
│ record A
│ typealias F = E
│ service MyService
│ provider MyEndpoint
└─other
Expand Down
25 changes: 20 additions & 5 deletions language-server/src/main/kotlin/tools/samt/ls/FileInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,28 @@ import tools.samt.lexer.Lexer
import tools.samt.lexer.Token
import tools.samt.parser.FileNode
import tools.samt.parser.Parser
import java.net.URI
import kotlin.io.path.readText
import kotlin.io.path.toPath

class FileInfo(
val diagnosticContext: DiagnosticContext,
val sourceFile: SourceFile,
@Suppress("unused") val tokens: List<Token>,
val fileNode: FileNode? = null,
val diagnosticContext: DiagnosticContext,
val sourceFile: SourceFile,
val tokens: List<Token> = emptyList(),
val fileNode: FileNode? = null,
)

val FileInfo.path get() = sourceFile.path
val FileInfo.content get() = sourceFile.content

fun parseFile(sourceFile: SourceFile): FileInfo {
val diagnosticContext = DiagnosticContext(sourceFile)

val tokens = Lexer.scan(sourceFile.content.reader(), diagnosticContext).toList()
val tokens = try {
Lexer.scan(sourceFile.content.reader(), diagnosticContext).toList()
} catch (e: DiagnosticException) {
return FileInfo(diagnosticContext, sourceFile)
}

if (diagnosticContext.hasErrors()) {
return FileInfo(diagnosticContext, sourceFile, tokens)
Expand All @@ -33,3 +43,8 @@ fun parseFile(sourceFile: SourceFile): FileInfo {

return FileInfo(diagnosticContext, sourceFile, tokens, fileNode)
}

fun readAndParseFile(uri: URI): FileInfo {
val sourceFile = SourceFile(uri, uri.toPath().readText())
return parseFile(sourceFile)
}
20 changes: 10 additions & 10 deletions language-server/src/main/kotlin/tools/samt/ls/Mapping.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ fun DiagnosticMessage.toDiagnostic(): Diagnostic? {
val diagnostic = Diagnostic()
val primaryHighlight = this.highlights.firstOrNull() ?: return null
diagnostic.range = primaryHighlight.location.toRange()
diagnostic.severity = when (severity) {
DiagnosticSeverity.Error -> org.eclipse.lsp4j.DiagnosticSeverity.Error
DiagnosticSeverity.Warning -> org.eclipse.lsp4j.DiagnosticSeverity.Warning
DiagnosticSeverity.Info -> org.eclipse.lsp4j.DiagnosticSeverity.Information
}
diagnostic.severity = severity.toLspSeverity()
diagnostic.source = "samt"
diagnostic.message = message
diagnostic.relatedInformation = highlights.filter { it.message != null }.map {
Expand All @@ -25,9 +21,13 @@ fun DiagnosticMessage.toDiagnostic(): Diagnostic? {
return diagnostic
}

fun SamtLocation.toRange(): Range {
return Range(
Position(start.row, start.col),
Position(end.row, end.col)
)
fun DiagnosticSeverity.toLspSeverity(): org.eclipse.lsp4j.DiagnosticSeverity = when (this) {
DiagnosticSeverity.Error -> org.eclipse.lsp4j.DiagnosticSeverity.Error
DiagnosticSeverity.Warning -> org.eclipse.lsp4j.DiagnosticSeverity.Warning
DiagnosticSeverity.Info -> org.eclipse.lsp4j.DiagnosticSeverity.Information
}

fun SamtLocation.toRange(): Range = Range(
Position(start.row, start.col),
Position(end.row, end.col)
)
61 changes: 61 additions & 0 deletions language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package tools.samt.ls

import tools.samt.common.DiagnosticController
import tools.samt.common.DiagnosticMessage
import tools.samt.common.collectSamtFiles
import tools.samt.common.readSamtSource
import tools.samt.semantic.Package
import tools.samt.semantic.SemanticModelBuilder
import java.net.URI

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

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

fun remove(fileUri: URI) {
files.remove(fileUri)
}

operator fun get(path: URI): FileInfo? = files[path]

override fun iterator(): Iterator<FileInfo> = files.values.iterator()

operator fun contains(path: URI) = path in files

fun getFilesIn(directoryPath: URI): List<URI> {
return files.keys.filter { it.startsWith(directoryPath) }
}

fun buildSemanticModel() {
semanticController = DiagnosticController(path)
globalPackage = SemanticModelBuilder.build(mapNotNull { it.fileNode }, semanticController)
}

private fun getMessages(path: URI): List<DiagnosticMessage> {
val fileInfo = files[path] ?: return emptyList()
return fileInfo.diagnosticContext.messages +
semanticController.getOrCreateContext(fileInfo.sourceFile).messages
}

fun getAllMessages() = files.keys.associateWith {
getMessages(it)
}

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))
}
return workspace
}
}
}
82 changes: 53 additions & 29 deletions language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@ package tools.samt.ls
import org.eclipse.lsp4j.*
import org.eclipse.lsp4j.jsonrpc.messages.Either
import org.eclipse.lsp4j.services.*
import tools.samt.common.DiagnosticController
import tools.samt.common.collectSamtFiles
import tools.samt.common.readSamtSource
import java.io.Closeable
import java.net.URI
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.logging.Logger
import kotlin.system.exitProcess

class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable {
private lateinit var client: LanguageClient
private val logger = Logger.getLogger("SamtLanguageServer")
private val workspaces = mutableMapOf<URI, SamtWorkspace>()
private val textDocumentService = SamtTextDocumentService(workspaces)
private val workspace = SamtWorkspace()
private val textDocumentService = SamtTextDocumentService(workspace)
private val workspaceService = SamtWorkspaceService(workspace)

override fun initialize(params: InitializeParams): CompletableFuture<InitializeResult> =
CompletableFuture.supplyAsync {
Expand All @@ -28,14 +26,44 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable {
range = Either.forLeft(false)
full = Either.forLeft(true)
}
workspace = WorkspaceServerCapabilities().apply {
workspaceFolders = WorkspaceFoldersOptions().apply {
supported = true
changeNotifications = Either.forRight(true)
}
fileOperations = FileOperationsServerCapabilities().apply {
val samtFilter = FileOperationFilter().apply {
pattern = FileOperationPattern().apply {
glob = "**/*.samt"
matches = FileOperationPatternKind.File
}
}
val folderFilter = FileOperationFilter().apply {
pattern = FileOperationPattern().apply {
glob = "**"
matches = FileOperationPatternKind.Folder
}
}
didCreate = FileOperationOptions().apply {
filters = listOf(samtFilter)
}
FileOperationOptions().apply {
filters = listOf(samtFilter, folderFilter)
}.let {
didRename = it
didDelete = it
}
}
}
definitionProvider = Either.forLeft(true)
referencesProvider = Either.forLeft(true)
}
InitializeResult(capabilities)
}

override fun initialized(params: InitializedParams) {
pushDiagnostics()
registerFileWatchCapability()
client.updateWorkspace(workspace)
}

override fun shutdown(): CompletableFuture<Any> = CompletableFuture.completedFuture(null)
Expand All @@ -46,11 +74,12 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable {

override fun getTextDocumentService(): TextDocumentService = textDocumentService

override fun getWorkspaceService(): WorkspaceService? = null
override fun getWorkspaceService(): WorkspaceService = workspaceService

override fun connect(client: LanguageClient) {
this.client = client
textDocumentService.connect(client)
workspaceService.connect(client)
logger.info("Connected to client")
}

Expand All @@ -59,31 +88,26 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable {
}

private fun buildSamtModel(params: InitializeParams) {
val folders = params.workspaceFolders.map { it.uri.toPathUri() }
val folders = params.workspaceFolders?.map { it.uri.toPathUri() }.orEmpty()
for (folder in folders) {
// if the folder is contained within another folder ignore it
if (folders.any { folder != it && folder.path.startsWith(it.path) }) continue
workspaces[folder] = buildWorkspace(folder)
workspace.addFolder(SamtFolder.fromDirectory(folder))
}
}

private fun buildWorkspace(workspacePath: URI): SamtWorkspace {
val diagnosticController = DiagnosticController(workspacePath)
val sourceFiles = collectSamtFiles(workspacePath).readSamtSource(diagnosticController)
val workspace = SamtWorkspace(diagnosticController)
sourceFiles.asSequence().map(::parseFile).forEach(workspace::add)
workspace.buildSemanticModel()
return workspace
}

private fun pushDiagnostics() {
workspaces.values.flatMap { workspace ->
workspace.getAllMessages().map { (path, messages) ->
PublishDiagnosticsParams(
path.toString(),
messages.map { it.toDiagnostic() }
private fun registerFileWatchCapability() {
val capability = "workspace/didChangeWatchedFiles"
client.registerCapability(RegistrationParams(listOf(
Registration(UUID.randomUUID().toString(), capability, DidChangeWatchedFilesRegistrationOptions().apply {
watchers = listOf(
FileSystemWatcher().apply {
globPattern = Either.forLeft("**/*.samt")
},
FileSystemWatcher().apply {
globPattern = Either.forLeft("**/")
kind = WatchKind.Create or WatchKind.Delete
},
)
}
}.forEach(client::publishDiagnostics)
})
)))
}
}
Loading