From 029235fcce88ee3ae9633de2f4843bd2083f831c Mon Sep 17 00:00:00 2001 From: Marcel Joss Date: Thu, 25 May 2023 16:30:45 +0200 Subject: [PATCH 1/7] feat(ls): use samt.yaml to find project roots --- language-server/build.gradle.kts | 1 + .../src/main/kotlin/tools/samt/ls/Path.kt | 29 ++++++++++++ .../main/kotlin/tools/samt/ls/SamtFolder.kt | 18 +++++--- .../tools/samt/ls/SamtLanguageServer.kt | 7 ++- .../kotlin/tools/samt/ls/SamtWorkspace.kt | 11 ++--- .../tools/samt/ls/SamtWorkspaceService.kt | 4 +- .../src/test/kotlin/tools/samt/ls/PathTest.kt | 45 +++++++++++++++++++ .../yaml-in-parentdir/parent/samt.yaml | 1 + .../path-test/yaml-in-subdir/sub/samt.yaml | 1 + .../path-test/yaml-in-workdir/samt.yaml | 1 + .../tools/samt/config/SamtConfiguration.kt | 14 +++--- .../samt/config/SamtConfigurationParser.kt | 9 +++- .../config/SamtConfigurationParserTest.kt | 2 +- 13 files changed, 115 insertions(+), 28 deletions(-) create mode 100644 language-server/src/main/kotlin/tools/samt/ls/Path.kt create mode 100644 language-server/src/test/kotlin/tools/samt/ls/PathTest.kt create mode 100644 language-server/src/test/resources/path-test/yaml-in-parentdir/parent/samt.yaml create mode 100644 language-server/src/test/resources/path-test/yaml-in-subdir/sub/samt.yaml create mode 100644 language-server/src/test/resources/path-test/yaml-in-workdir/samt.yaml diff --git a/language-server/build.gradle.kts b/language-server/build.gradle.kts index 7f7d8100..25afa76e 100644 --- a/language-server/build.gradle.kts +++ b/language-server/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(project(":lexer")) implementation(project(":parser")) implementation(project(":semantic")) + implementation(project(":samt-config")) } application { diff --git a/language-server/src/main/kotlin/tools/samt/ls/Path.kt b/language-server/src/main/kotlin/tools/samt/ls/Path.kt new file mode 100644 index 00000000..3034cf1c --- /dev/null +++ b/language-server/src/main/kotlin/tools/samt/ls/Path.kt @@ -0,0 +1,29 @@ +package tools.samt.ls + +import tools.samt.config.SamtConfigurationParser +import java.nio.file.Path +import kotlin.io.path.isRegularFile + + +internal fun Path.findSamtRoots(): List { + fun Path.parents(): Sequence = generateSequence(this) { it.parent } + fun getSourceDirectory(yamlPath: Path) = try { + val config = SamtConfigurationParser.parseConfiguration(yamlPath) + yamlPath.resolveSibling(config.source).normalize() + } catch (e: SamtConfigurationParser.ParseException) { + null + } + + return toFile().walkTopDown() + .map { it.toPath() } + .filter { it.endsWith("samt.yaml") && it.isRegularFile() } + .mapNotNull { getSourceDirectory(it) } + .ifEmpty { + parents() + .map { it.resolve("samt.yaml") } + .filter { it.isRegularFile() } + .mapNotNull { getSourceDirectory(it) } + .take(1) + } + .toList() +} \ No newline at end of file diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt index 1ec12a10..ba854682 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt @@ -6,6 +6,7 @@ import tools.samt.common.collectSamtFiles import tools.samt.common.readSamtSource import tools.samt.semantic.SemanticModel import java.net.URI +import kotlin.io.path.toPath class SamtFolder(val path: URI) : Iterable { private val files = mutableMapOf() @@ -47,14 +48,17 @@ class SamtFolder(val path: URI) : Iterable { } 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 fromDirectory(path: URI): List = path.toPath().let { + it.findSamtRoots().ifEmpty { listOf(it) }.map { root -> + val uri = root.toUri() + val controller = DiagnosticController(uri) + val folder = SamtFolder(uri) + val sourceFiles = collectSamtFiles(uri).readSamtSource(controller) + for (file in sourceFiles) { + folder.set(parseFile(file)) + } + folder } - return workspace } } } diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt index ff7e6eab..3cc2b505 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt @@ -89,10 +89,9 @@ 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 { SamtFolder.fromDirectory(it.uri.toPathUri()) } + ?.forEach(workspace::addFolder) } private fun registerFileWatchCapability() { diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt index 691357a7..c9d1f49a 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt @@ -73,7 +73,8 @@ class SamtWorkspace { removedFiles.clear() } - private fun getFolder(path: URI): SamtFolder? = folders[path] ?: folders.values.singleOrNull { path.startsWith(it.path) } + private fun getFolder(path: URI): SamtFolder? = + folders[path] ?: folders.values.singleOrNull { path.startsWith(it.path) } } data class FolderSnapshot(val path: URI, val files: List, val semanticModel: SemanticModel?) @@ -83,10 +84,10 @@ fun LanguageClient.updateWorkspace(workspace: SamtWorkspace) { workspace.buildSemanticModel() workspace.getPendingMessages().forEach { (path, messages) -> publishDiagnostics( - PublishDiagnosticsParams( - path.toString(), - messages.mapNotNull { it.toDiagnostic() } - ) + PublishDiagnosticsParams( + path.toString(), + messages.mapNotNull { it.toDiagnostic() } + ) ) } workspace.clearChanges() diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt index 7498c49d..c419e97c 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt @@ -83,9 +83,7 @@ class SamtWorkspaceService(private val workspace: SamtWorkspace) : WorkspaceServ val event = params.event for (added in event.added) { val path = added.uri.toPathUri() - val folder = SamtFolder.fromDirectory(path) - workspace.addFolder(folder) - folder.buildSemanticModel() + SamtFolder.fromDirectory(path).forEach(workspace::addFolder) } for (removed in event.removed) { workspace.removeFolder(removed.uri.toPathUri()) diff --git a/language-server/src/test/kotlin/tools/samt/ls/PathTest.kt b/language-server/src/test/kotlin/tools/samt/ls/PathTest.kt new file mode 100644 index 00000000..dd644c70 --- /dev/null +++ b/language-server/src/test/kotlin/tools/samt/ls/PathTest.kt @@ -0,0 +1,45 @@ +package tools.samt.ls + +import kotlin.io.path.Path +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class PathTest { + private val testDirectory = Path("src/test/resources/path-test") + @BeforeTest + fun setup() { + assertTrue(testDirectory.exists() && testDirectory.isDirectory(), "Test directory does not exist") + } + + @Test + fun `no samt yaml`() { + val workDir = testDirectory.resolve("no-yaml") + val roots = workDir.findSamtRoots() + assertEquals(emptyList(), roots) + } + + @Test + fun `samt yaml in workdir`() { + val workDir = testDirectory.resolve("yaml-in-workdir") + val roots = workDir.findSamtRoots() + assertEquals(listOf(workDir.resolve("src")), roots) + } + + @Test + fun `samt yaml in subdir`() { + val workDir = testDirectory.resolve("yaml-in-subdir") + val roots = workDir.findSamtRoots() + assertEquals(listOf(workDir.resolve("sub/src")), roots) + } + + @Test + fun `samt yaml in parent dir`() { + val workDir = testDirectory.resolve("yaml-in-parentdir/parent/work") + val roots = workDir.findSamtRoots() + assertEquals(listOf(workDir.resolve("src")), roots) + } +} \ No newline at end of file diff --git a/language-server/src/test/resources/path-test/yaml-in-parentdir/parent/samt.yaml b/language-server/src/test/resources/path-test/yaml-in-parentdir/parent/samt.yaml new file mode 100644 index 00000000..e0fa9dec --- /dev/null +++ b/language-server/src/test/resources/path-test/yaml-in-parentdir/parent/samt.yaml @@ -0,0 +1 @@ +source: ./work/src \ No newline at end of file diff --git a/language-server/src/test/resources/path-test/yaml-in-subdir/sub/samt.yaml b/language-server/src/test/resources/path-test/yaml-in-subdir/sub/samt.yaml new file mode 100644 index 00000000..b2b56b0d --- /dev/null +++ b/language-server/src/test/resources/path-test/yaml-in-subdir/sub/samt.yaml @@ -0,0 +1 @@ +source: ./src \ No newline at end of file diff --git a/language-server/src/test/resources/path-test/yaml-in-workdir/samt.yaml b/language-server/src/test/resources/path-test/yaml-in-workdir/samt.yaml new file mode 100644 index 00000000..b2b56b0d --- /dev/null +++ b/language-server/src/test/resources/path-test/yaml-in-workdir/samt.yaml @@ -0,0 +1 @@ +source: ./src \ No newline at end of file diff --git a/samt-config/src/main/kotlin/tools/samt/config/SamtConfiguration.kt b/samt-config/src/main/kotlin/tools/samt/config/SamtConfiguration.kt index 78aa56a1..cb2d1720 100644 --- a/samt-config/src/main/kotlin/tools/samt/config/SamtConfiguration.kt +++ b/samt-config/src/main/kotlin/tools/samt/config/SamtConfiguration.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class SamtConfiguration( +internal data class SamtConfiguration( val source: String = "./src", val repositories: SamtRepositoriesConfiguration = SamtRepositoriesConfiguration(), val plugins: List = emptyList(), @@ -12,22 +12,22 @@ data class SamtConfiguration( ) @Serializable -data class SamtRepositoriesConfiguration( +internal data class SamtRepositoriesConfiguration( val maven: String = "https://repo.maven.apache.org/maven2" ) @Serializable -sealed interface SamtPluginConfiguration +internal sealed interface SamtPluginConfiguration @Serializable @SerialName("local") -data class SamtLocalPluginConfiguration( +internal data class SamtLocalPluginConfiguration( val path: String, ) : SamtPluginConfiguration @Serializable @SerialName("maven") -data class SamtMavenPluginConfiguration( +internal data class SamtMavenPluginConfiguration( val groupId: String, val artifactId: String, val version: String, @@ -36,13 +36,13 @@ data class SamtMavenPluginConfiguration( @Serializable @SerialName("gradle") -data class SamtGradlePluginConfiguration( +internal data class SamtGradlePluginConfiguration( val dependency: String, val repository: String? = null, ) : SamtPluginConfiguration @Serializable -data class SamtGeneratorConfiguration( +internal data class SamtGeneratorConfiguration( val name: String, val output: String = "./out", val options: Map = emptyMap(), diff --git a/samt-config/src/main/kotlin/tools/samt/config/SamtConfigurationParser.kt b/samt-config/src/main/kotlin/tools/samt/config/SamtConfigurationParser.kt index 3c64f55b..7f3ec9c9 100644 --- a/samt-config/src/main/kotlin/tools/samt/config/SamtConfigurationParser.kt +++ b/samt-config/src/main/kotlin/tools/samt/config/SamtConfigurationParser.kt @@ -1,6 +1,7 @@ package tools.samt.config import com.charleskorn.kaml.* +import kotlinx.serialization.SerializationException import java.nio.file.Path import kotlin.io.path.exists import kotlin.io.path.inputStream @@ -23,9 +24,15 @@ object SamtConfigurationParser { ) ) + class ParseException(exception: Throwable) : RuntimeException(exception.message, exception) + fun parseConfiguration(path: Path): CommonSamtConfiguration { val parsedConfiguration: SamtConfiguration = if (path.exists()) { - yaml.decodeFromStream(path.inputStream()) + try { + yaml.decodeFromStream(path.inputStream()) + } catch (exception: SerializationException) { + throw ParseException(exception) + } } else { SamtConfiguration() } diff --git a/samt-config/src/test/kotlin/tools/samt/config/SamtConfigurationParserTest.kt b/samt-config/src/test/kotlin/tools/samt/config/SamtConfigurationParserTest.kt index 847e0954..517d2b4c 100644 --- a/samt-config/src/test/kotlin/tools/samt/config/SamtConfigurationParserTest.kt +++ b/samt-config/src/test/kotlin/tools/samt/config/SamtConfigurationParserTest.kt @@ -85,7 +85,7 @@ class SamtConfigurationParserTest { @Test fun `throws for samt-invalid file`() { - val exception = assertThrows { + val exception = assertThrows { SamtConfigurationParser.parseConfiguration(testDirectory.resolve("samt-invalid.yaml")) } From 838338326d65c075f6ce92cb5414363cee7f3663 Mon Sep 17 00:00:00 2001 From: Marcel Joss Date: Thu, 25 May 2023 17:25:11 +0200 Subject: [PATCH 2/7] feat(ls): support single files when using samt.yaml --- .../main/kotlin/tools/samt/ls/SamtFolder.kt | 18 +++++++----------- .../kotlin/tools/samt/ls/SamtLanguageServer.kt | 10 +++++++--- .../tools/samt/ls/SamtTextDocumentService.kt | 5 +++++ .../main/kotlin/tools/samt/ls/SamtWorkspace.kt | 2 ++ .../tools/samt/ls/SamtWorkspaceService.kt | 11 ++++++++--- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt index ba854682..9587396a 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt @@ -6,7 +6,6 @@ import tools.samt.common.collectSamtFiles import tools.samt.common.readSamtSource import tools.samt.semantic.SemanticModel import java.net.URI -import kotlin.io.path.toPath class SamtFolder(val path: URI) : Iterable { private val files = mutableMapOf() @@ -48,17 +47,14 @@ class SamtFolder(val path: URI) : Iterable { } companion object { - fun fromDirectory(path: URI): List = path.toPath().let { - it.findSamtRoots().ifEmpty { listOf(it) }.map { root -> - val uri = root.toUri() - val controller = DiagnosticController(uri) - val folder = SamtFolder(uri) - val sourceFiles = collectSamtFiles(uri).readSamtSource(controller) - for (file in sourceFiles) { - folder.set(parseFile(file)) - } - folder + fun fromDirectory(path: URI): SamtFolder { + val controller = DiagnosticController(path) + val folder = SamtFolder(path) + val sourceFiles = collectSamtFiles(path).readSamtSource(controller) + for (file in sourceFiles) { + folder.set(parseFile(file)) } + return folder } } } diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt index 3cc2b505..d1fb17c8 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt @@ -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 { @@ -89,9 +90,12 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable { } private fun buildSamtModel(params: InitializeParams) { - params.workspaceFolders - ?.flatMap { SamtFolder.fromDirectory(it.uri.toPathUri()) } - ?.forEach(workspace::addFolder) + params.workspaceFolders?.forEach { folder -> + val path = folder.uri.toPathUri().toPath() + path.findSamtRoots() + .ifEmpty { listOf(path) } + .forEach { workspace.addFolder(SamtFolder.fromDirectory(it.toUri())) } + } } private fun registerFileWatchCapability() { diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt index 067391c6..901e2249 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt @@ -13,6 +13,7 @@ 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 { @@ -24,6 +25,10 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume val path = params.textDocument.uri.toPathUri() val text = params.textDocument.text + if (!workspace.containsFile(path)) { + val projectRoot = path.toPath().findSamtRoots().singleOrNull() + projectRoot?.let { workspace.addFolder(SamtFolder.fromDirectory(it.toUri())) } + } workspace.setFile(parseFile(SourceFile(path, text))) client.updateWorkspace(workspace) } diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt index c9d1f49a..b8927160 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt @@ -48,6 +48,8 @@ class SamtWorkspace { removedFiles.add(path) } + fun containsFile(path: URI) = folders.keys.any { path.startsWith(it) } + fun removeDirectory(path: URI) { val folder = getFolder(path) val files = folder?.getFilesIn(path)?.ifEmpty { null } ?: return diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt index c419e97c..a34d4e25 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt @@ -82,11 +82,16 @@ class SamtWorkspaceService(private val workspace: SamtWorkspace) : WorkspaceServ override fun didChangeWorkspaceFolders(params: DidChangeWorkspaceFoldersParams) { val event = params.event for (added in event.added) { - val path = added.uri.toPathUri() - SamtFolder.fromDirectory(path).forEach(workspace::addFolder) + val path = added.uri.toPathUri().toPath() + path.findSamtRoots() + .ifEmpty { listOf(path) } + .forEach { workspace.addFolder(SamtFolder.fromDirectory(it.toUri())) } } for (removed in event.removed) { - workspace.removeFolder(removed.uri.toPathUri()) + val path = removed.uri.toPathUri().toPath() + path.findSamtRoots() + .ifEmpty { listOf(path) } + .forEach { workspace.removeFolder(it.toUri()) } } client.updateWorkspace(workspace) } From 762b8b988dca29573076f596d9937561d91b6caa Mon Sep 17 00:00:00 2001 From: Marcel Joss Date: Fri, 26 May 2023 10:34:17 +0200 Subject: [PATCH 3/7] feat(ls): prevent adding outside files to SamtFolder --- .../main/kotlin/tools/samt/ls/SamtFolder.kt | 1 + .../kotlin/tools/samt/ls/SamtFolderTest.kt | 53 +++++++++++-------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt index 9587396a..22f07b5b 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt @@ -14,6 +14,7 @@ class SamtFolder(val path: URI) : Iterable { private var semanticController: DiagnosticController = DiagnosticController(path) fun set(fileInfo: FileInfo) { + require(fileInfo.sourceFile.path.startsWith(path)) files[fileInfo.sourceFile.path] = fileInfo } diff --git a/language-server/src/test/kotlin/tools/samt/ls/SamtFolderTest.kt b/language-server/src/test/kotlin/tools/samt/ls/SamtFolderTest.kt index 82b826e3..86130deb 100644 --- a/language-server/src/test/kotlin/tools/samt/ls/SamtFolderTest.kt +++ b/language-server/src/test/kotlin/tools/samt/ls/SamtFolderTest.kt @@ -1,58 +1,69 @@ package tools.samt.ls +import org.junit.jupiter.api.assertThrows import tools.samt.common.SourceFile import kotlin.test.* class SamtFolderTest { @Test fun `file can be retrieved with URI after set`() { - val workspace = SamtFolder("file:///tmp/test".toPathUri()) + val folder = SamtFolder("file:///tmp/test".toPathUri()) val uri = "file:///tmp/test/model.samt".toPathUri() val fileInfo = parseFile(SourceFile(uri, "package foo.bar")) - workspace.set(fileInfo) - assertSame(fileInfo, workspace[uri]) - assertTrue(uri in workspace) - assertContains(workspace as Iterable, fileInfo) + folder.set(fileInfo) + assertSame(fileInfo, folder[uri]) + assertTrue(uri in folder) + assertContains(folder as Iterable, fileInfo) } @Test fun `file cannot be retrieved after removal`() { - val workspace = SamtFolder("file:///tmp/test".toPathUri()) + val folder = SamtFolder("file:///tmp/test".toPathUri()) val uri = "file:///tmp/test/model.samt".toPathUri() val fileInfo = parseFile(SourceFile(uri, "package foo.bar")) - workspace.set(fileInfo) - workspace.remove(uri) - assertNull(workspace[uri]) - assertFalse(uri in workspace) - assertFalse(fileInfo in workspace) + folder.set(fileInfo) + folder.remove(uri) + assertNull(folder[uri]) + assertFalse(uri in folder) + assertFalse(fileInfo in folder) } @Test fun `getFilesIn returns files in directory`() { - val workspace = SamtFolder("file:///tmp/test".toPathUri()) + val folder = SamtFolder("file:///tmp/test".toPathUri()) val file1 = "file:///tmp/test/dir/foo.samt".toPathUri() val file2 = "file:///tmp/test/dir/bar.samt".toPathUri() val file3 = "file:///tmp/test/baz.samt".toPathUri() val fileInfo1 = parseFile(SourceFile(file1, "package foo.bar")) val fileInfo2 = parseFile(SourceFile(file2, "package foo.bar")) val fileInfo3 = parseFile(SourceFile(file3, "package foo.bar")) - workspace.set(fileInfo1) - workspace.set(fileInfo2) - workspace.set(fileInfo3) - assertEquals(setOf(file1, file2), workspace.getFilesIn("file:///tmp/test/dir".toPathUri()).toSet()) + folder.set(fileInfo1) + folder.set(fileInfo2) + folder.set(fileInfo3) + assertEquals(setOf(file1, file2), folder.getFilesIn("file:///tmp/test/dir".toPathUri()).toSet()) } @Test fun `getMessages includes parser and semantic messages`() { - val workspace = SamtFolder("file:///tmp/test".toPathUri()) + val folder = SamtFolder("file:///tmp/test".toPathUri()) val uri1 = "file:///tmp/test/record.samt".toPathUri() val uri2 = "file:///tmp/test/service.samt".toPathUri() val fileInfo1 = parseFile(SourceFile(uri1, "package foo.bar record Foo { ")) val fileInfo2 = parseFile(SourceFile(uri2, "package foo.bar service FooService { getService(): Foo }")) - workspace.set(fileInfo1) - workspace.set(fileInfo2) - workspace.buildSemanticModel() - val messages = workspace.getAllMessages() + folder.set(fileInfo1) + folder.set(fileInfo2) + folder.buildSemanticModel() + val messages = folder.getAllMessages() assertEquals(2, messages.size) } + + @Test + fun `setting file with path outside of folder throws IllegalArgumentException`() { + val folder = SamtFolder("file:///tmp/test".toPathUri()) + val uri = "file:///tmp/other/model.samt".toPathUri() + val fileInfo = parseFile(SourceFile(uri, "package foo.bar")) + assertThrows { + folder.set(fileInfo) + } + } } From fd8fac8bcb7b15d5c40191c40511d7fd510d18fe Mon Sep 17 00:00:00 2001 From: Marcel Joss Date: Fri, 26 May 2023 13:39:58 +0200 Subject: [PATCH 4/7] feat(ls): fully rely on samt.yaml for finding source directories --- .../src/main/kotlin/tools/samt/ls/Path.kt | 29 -------- .../main/kotlin/tools/samt/ls/SamtConfig.kt | 20 ++++++ .../main/kotlin/tools/samt/ls/SamtFolder.kt | 24 ++++--- .../tools/samt/ls/SamtLanguageServer.kt | 11 ++-- .../tools/samt/ls/SamtTextDocumentService.kt | 4 +- .../kotlin/tools/samt/ls/SamtWorkspace.kt | 24 +++---- .../tools/samt/ls/SamtWorkspaceService.kt | 9 +-- .../ls/{PathTest.kt => SamtConfigTest.kt} | 18 ++--- .../kotlin/tools/samt/ls/SamtFolderTest.kt | 26 ++++---- .../kotlin/tools/samt/ls/SamtWorkspaceTest.kt | 66 +++++++++---------- 10 files changed, 114 insertions(+), 117 deletions(-) delete mode 100644 language-server/src/main/kotlin/tools/samt/ls/Path.kt create mode 100644 language-server/src/main/kotlin/tools/samt/ls/SamtConfig.kt rename language-server/src/test/kotlin/tools/samt/ls/{PathTest.kt => SamtConfigTest.kt} (65%) diff --git a/language-server/src/main/kotlin/tools/samt/ls/Path.kt b/language-server/src/main/kotlin/tools/samt/ls/Path.kt deleted file mode 100644 index 3034cf1c..00000000 --- a/language-server/src/main/kotlin/tools/samt/ls/Path.kt +++ /dev/null @@ -1,29 +0,0 @@ -package tools.samt.ls - -import tools.samt.config.SamtConfigurationParser -import java.nio.file.Path -import kotlin.io.path.isRegularFile - - -internal fun Path.findSamtRoots(): List { - fun Path.parents(): Sequence = generateSequence(this) { it.parent } - fun getSourceDirectory(yamlPath: Path) = try { - val config = SamtConfigurationParser.parseConfiguration(yamlPath) - yamlPath.resolveSibling(config.source).normalize() - } catch (e: SamtConfigurationParser.ParseException) { - null - } - - return toFile().walkTopDown() - .map { it.toPath() } - .filter { it.endsWith("samt.yaml") && it.isRegularFile() } - .mapNotNull { getSourceDirectory(it) } - .ifEmpty { - parents() - .map { it.resolve("samt.yaml") } - .filter { it.isRegularFile() } - .mapNotNull { getSourceDirectory(it) } - .take(1) - } - .toList() -} \ No newline at end of file diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtConfig.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtConfig.kt new file mode 100644 index 00000000..b3aff5ef --- /dev/null +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtConfig.kt @@ -0,0 +1,20 @@ +package tools.samt.ls + +import java.nio.file.Path +import kotlin.io.path.isRegularFile + +const val SAMT_CONFIG_FILE_NAME = "samt.yaml" + +fun findSamtConfigs(path: Path): List { + fun Path.parents(): Sequence = generateSequence(parent) { it.parent } + + return path.toFile().walkTopDown() + .map { it.toPath() } + .filter { it.fileName.toString() == SAMT_CONFIG_FILE_NAME && it.isRegularFile() } + .ifEmpty { + path.parents() + .map { it.resolve(SAMT_CONFIG_FILE_NAME) } + .filter { it.isRegularFile() } + .take(1) + }.toList() +} \ No newline at end of file diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt index 22f07b5b..8f45a44c 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt @@ -4,17 +4,23 @@ 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 { +class SamtFolder(val configPath: URI, val sourcePath: URI) : Iterable { private val files = mutableMapOf() var semanticModel: SemanticModel? = null private set - private var semanticController: DiagnosticController = DiagnosticController(path) + private var semanticController: DiagnosticController = DiagnosticController(sourcePath) + + init { + require(configPath.toPath().fileName.toString() == SAMT_CONFIG_FILE_NAME) + } fun set(fileInfo: FileInfo) { - require(fileInfo.sourceFile.path.startsWith(path)) + require(fileInfo.sourceFile.path.startsWith(sourcePath)) files[fileInfo.sourceFile.path] = fileInfo } @@ -33,7 +39,7 @@ class SamtFolder(val path: URI) : Iterable { } fun buildSemanticModel() { - semanticController = DiagnosticController(path) + semanticController = DiagnosticController(sourcePath) semanticModel = SemanticModel.build(mapNotNull { it.fileNode }, semanticController) } @@ -48,10 +54,12 @@ class SamtFolder(val path: URI) : Iterable { } companion object { - fun fromDirectory(path: URI): SamtFolder { - val controller = DiagnosticController(path) - val folder = SamtFolder(path) - val sourceFiles = collectSamtFiles(path).readSamtSource(controller) + fun fromConfig(configPath: URI): SamtFolder { + val folder = configPath.toPath().let { + val config = SamtConfigurationParser.parseConfiguration(it) + SamtFolder(configPath.normalize(), it.resolveSibling(config.source).normalize().toUri()) + } + val sourceFiles = collectSamtFiles(folder.sourcePath).readSamtSource(DiagnosticController(folder.sourcePath)) for (file in sourceFiles) { folder.set(parseFile(file)) } diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt index d1fb17c8..adf35e49 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt @@ -90,12 +90,11 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable { } private fun buildSamtModel(params: InitializeParams) { - params.workspaceFolders?.forEach { folder -> - val path = folder.uri.toPathUri().toPath() - path.findSamtRoots() - .ifEmpty { listOf(path) } - .forEach { workspace.addFolder(SamtFolder.fromDirectory(it.toUri())) } - } + params.workspaceFolders + ?.flatMap { folder -> + val path = folder.uri.toPathUri().toPath() + findSamtConfigs(path).map { SamtFolder.fromConfig(it.toUri()) } + }?.forEach(workspace::addFolder) } private fun registerFileWatchCapability() { diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt index 901e2249..6c5fb035 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt @@ -26,8 +26,8 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume val text = params.textDocument.text if (!workspace.containsFile(path)) { - val projectRoot = path.toPath().findSamtRoots().singleOrNull() - projectRoot?.let { workspace.addFolder(SamtFolder.fromDirectory(it.toUri())) } + val configPath = findSamtConfigs(path.toPath().parent).singleOrNull() + configPath?.let { workspace.addFolder(SamtFolder.fromConfig(it.toUri())) } } workspace.setFile(parseFile(SourceFile(path, text))) client.updateWorkspace(workspace) diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt index b8927160..54180806 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt @@ -7,26 +7,28 @@ import tools.samt.semantic.SemanticModel import java.net.URI class SamtWorkspace { - private val folders = mutableMapOf() + /** + * Maps the path of the config file to the folder + */ + private val foldersByConfigPath = mutableMapOf() private val changedFolders = mutableSetOf() private val removedFiles = mutableSetOf() - fun getFolderSnapshot(path: URI): FolderSnapshot? = getFolder(path)?.let { FolderSnapshot(it.path, it.toList(), it.semanticModel) } + fun getFolderSnapshot(path: URI): FolderSnapshot? = getFolder(path)?.let { FolderSnapshot(it.sourcePath, it.toList(), it.semanticModel) } fun addFolder(folder: SamtFolder) { - val newPath = folder.path // folder is contained in other folder, ignore - if (folders.keys.any { newPath.startsWith(it) }) { + if (foldersByConfigPath.values.any { folder.sourcePath.startsWith(it.sourcePath) }) { return } // remove folders contained in new folder - folders.keys.removeIf { it.startsWith(newPath) } - folders[folder.path] = folder + foldersByConfigPath.values.removeIf { it.sourcePath.startsWith(folder.sourcePath) } + foldersByConfigPath[folder.configPath] = folder changedFolders.add(folder) } - fun removeFolder(path: URI) { - folders.remove(path)?.let { folder -> + fun removeFolder(configPath: URI) { + foldersByConfigPath.remove(configPath)?.let { folder -> removedFiles.addAll(folder.map { it.path }) } } @@ -48,7 +50,7 @@ class SamtWorkspace { removedFiles.add(path) } - fun containsFile(path: URI) = folders.keys.any { path.startsWith(it) } + fun containsFile(path: URI) = foldersByConfigPath.keys.any { path.startsWith(it) } fun removeDirectory(path: URI) { val folder = getFolder(path) @@ -76,10 +78,10 @@ class SamtWorkspace { } private fun getFolder(path: URI): SamtFolder? = - folders[path] ?: folders.values.singleOrNull { path.startsWith(it.path) } + foldersByConfigPath[path] ?: foldersByConfigPath.values.singleOrNull { path.startsWith(it.sourcePath) } } -data class FolderSnapshot(val path: URI, val files: List, val semanticModel: SemanticModel?) +data class FolderSnapshot(val sourcePath: URI, val files: List, val semanticModel: SemanticModel?) fun LanguageClient.updateWorkspace(workspace: SamtWorkspace) { diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt index a34d4e25..882cbef5 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt @@ -83,14 +83,11 @@ class SamtWorkspaceService(private val workspace: SamtWorkspace) : WorkspaceServ val event = params.event for (added in event.added) { val path = added.uri.toPathUri().toPath() - path.findSamtRoots() - .ifEmpty { listOf(path) } - .forEach { workspace.addFolder(SamtFolder.fromDirectory(it.toUri())) } + findSamtConfigs(path).forEach { workspace.addFolder(SamtFolder.fromConfig(it.toUri())) } } for (removed in event.removed) { val path = removed.uri.toPathUri().toPath() - path.findSamtRoots() - .ifEmpty { listOf(path) } + findSamtConfigs(path) .forEach { workspace.removeFolder(it.toUri()) } } client.updateWorkspace(workspace) @@ -101,7 +98,7 @@ class SamtWorkspaceService(private val workspace: SamtWorkspace) : WorkspaceServ } private fun parseFilesInDirectory(path: URI): List { - val folderPath = checkNotNull(workspace.getFolderSnapshot(path)).path + val folderPath = checkNotNull(workspace.getFolderSnapshot(path)).sourcePath val sourceFiles = collectSamtFiles(path).readSamtSource(DiagnosticController(folderPath)) return sourceFiles.map(::parseFile) } diff --git a/language-server/src/test/kotlin/tools/samt/ls/PathTest.kt b/language-server/src/test/kotlin/tools/samt/ls/SamtConfigTest.kt similarity index 65% rename from language-server/src/test/kotlin/tools/samt/ls/PathTest.kt rename to language-server/src/test/kotlin/tools/samt/ls/SamtConfigTest.kt index dd644c70..8c6c867b 100644 --- a/language-server/src/test/kotlin/tools/samt/ls/PathTest.kt +++ b/language-server/src/test/kotlin/tools/samt/ls/SamtConfigTest.kt @@ -8,7 +8,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class PathTest { +class SamtConfigTest { private val testDirectory = Path("src/test/resources/path-test") @BeforeTest fun setup() { @@ -18,28 +18,28 @@ class PathTest { @Test fun `no samt yaml`() { val workDir = testDirectory.resolve("no-yaml") - val roots = workDir.findSamtRoots() - assertEquals(emptyList(), roots) + val configs = findSamtConfigs(workDir) + assertEquals(emptyList(), configs) } @Test fun `samt yaml in workdir`() { val workDir = testDirectory.resolve("yaml-in-workdir") - val roots = workDir.findSamtRoots() - assertEquals(listOf(workDir.resolve("src")), roots) + val configs = findSamtConfigs(workDir) + assertEquals(listOf(workDir.resolve("samt.yaml")), configs) } @Test fun `samt yaml in subdir`() { val workDir = testDirectory.resolve("yaml-in-subdir") - val roots = workDir.findSamtRoots() - assertEquals(listOf(workDir.resolve("sub/src")), roots) + val configs = findSamtConfigs(workDir) + assertEquals(listOf(workDir.resolve("sub/samt.yaml")), configs) } @Test fun `samt yaml in parent dir`() { val workDir = testDirectory.resolve("yaml-in-parentdir/parent/work") - val roots = workDir.findSamtRoots() - assertEquals(listOf(workDir.resolve("src")), roots) + val configs = findSamtConfigs(workDir) + assertEquals(listOf(workDir.resolveSibling("samt.yaml")), configs) } } \ No newline at end of file diff --git a/language-server/src/test/kotlin/tools/samt/ls/SamtFolderTest.kt b/language-server/src/test/kotlin/tools/samt/ls/SamtFolderTest.kt index 86130deb..56c10f18 100644 --- a/language-server/src/test/kotlin/tools/samt/ls/SamtFolderTest.kt +++ b/language-server/src/test/kotlin/tools/samt/ls/SamtFolderTest.kt @@ -7,8 +7,8 @@ import kotlin.test.* class SamtFolderTest { @Test fun `file can be retrieved with URI after set`() { - val folder = SamtFolder("file:///tmp/test".toPathUri()) - val uri = "file:///tmp/test/model.samt".toPathUri() + val folder = SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri()) + val uri = "file:///tmp/test/src/model.samt".toPathUri() val fileInfo = parseFile(SourceFile(uri, "package foo.bar")) folder.set(fileInfo) assertSame(fileInfo, folder[uri]) @@ -18,8 +18,8 @@ class SamtFolderTest { @Test fun `file cannot be retrieved after removal`() { - val folder = SamtFolder("file:///tmp/test".toPathUri()) - val uri = "file:///tmp/test/model.samt".toPathUri() + val folder = SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri()) + val uri = "file:///tmp/test/src/model.samt".toPathUri() val fileInfo = parseFile(SourceFile(uri, "package foo.bar")) folder.set(fileInfo) folder.remove(uri) @@ -30,24 +30,24 @@ class SamtFolderTest { @Test fun `getFilesIn returns files in directory`() { - val folder = SamtFolder("file:///tmp/test".toPathUri()) - val file1 = "file:///tmp/test/dir/foo.samt".toPathUri() - val file2 = "file:///tmp/test/dir/bar.samt".toPathUri() - val file3 = "file:///tmp/test/baz.samt".toPathUri() + val folder = SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri()) + val file1 = "file:///tmp/test/src/dir/foo.samt".toPathUri() + val file2 = "file:///tmp/test/src/dir/bar.samt".toPathUri() + val file3 = "file:///tmp/test/src/baz.samt".toPathUri() val fileInfo1 = parseFile(SourceFile(file1, "package foo.bar")) val fileInfo2 = parseFile(SourceFile(file2, "package foo.bar")) val fileInfo3 = parseFile(SourceFile(file3, "package foo.bar")) folder.set(fileInfo1) folder.set(fileInfo2) folder.set(fileInfo3) - assertEquals(setOf(file1, file2), folder.getFilesIn("file:///tmp/test/dir".toPathUri()).toSet()) + assertEquals(setOf(file1, file2), folder.getFilesIn("file:///tmp/test/src/dir".toPathUri()).toSet()) } @Test fun `getMessages includes parser and semantic messages`() { - val folder = SamtFolder("file:///tmp/test".toPathUri()) - val uri1 = "file:///tmp/test/record.samt".toPathUri() - val uri2 = "file:///tmp/test/service.samt".toPathUri() + val folder = SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri()) + val uri1 = "file:///tmp/test/src/record.samt".toPathUri() + val uri2 = "file:///tmp/test/src/service.samt".toPathUri() val fileInfo1 = parseFile(SourceFile(uri1, "package foo.bar record Foo { ")) val fileInfo2 = parseFile(SourceFile(uri2, "package foo.bar service FooService { getService(): Foo }")) folder.set(fileInfo1) @@ -59,7 +59,7 @@ class SamtFolderTest { @Test fun `setting file with path outside of folder throws IllegalArgumentException`() { - val folder = SamtFolder("file:///tmp/test".toPathUri()) + val folder = SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri()) val uri = "file:///tmp/other/model.samt".toPathUri() val fileInfo = parseFile(SourceFile(uri, "package foo.bar")) assertThrows { diff --git a/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt b/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt index 6ff0556c..c887a9e9 100644 --- a/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt +++ b/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt @@ -7,9 +7,9 @@ import kotlin.test.* class SamtWorkspaceTest { @Test fun `getFile retrieves file`() { - val folder = SamtFolder("file:///tmp/test".toPathUri()) + val folder = SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri()) val workspace = SamtWorkspace() - val uri = "file:///tmp/test/model.samt".toPathUri() + val uri = "file:///tmp/test/src/model.samt".toPathUri() workspace.addFolder(folder) val fileInfo = parseFile(SourceFile (uri, "package foo.bar")) folder.set(fileInfo) @@ -18,43 +18,43 @@ class SamtWorkspaceTest { @Test fun `file is in folder snapshot`() { - val folder = SamtFolder("file:///tmp/test".toPathUri()) + val folder = SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri()) val workspace = SamtWorkspace() - val uri = "file:///tmp/test/model.samt".toPathUri() + val uri = "file:///tmp/test/src/model.samt".toPathUri() workspace.addFolder(folder) val fileInfo = parseFile(SourceFile (uri, "package foo.bar")) folder.set(fileInfo) - val snapshot = workspace.getFolderSnapshot("file:///tmp/test".toPathUri()) + val snapshot = workspace.getFolderSnapshot("file:///tmp/test/src".toPathUri()) assertEquals(listOf(fileInfo), snapshot?.files) } @Test fun `folder which is already contained in other folder is ignored`() { - val outer = SamtFolder("file:///tmp/test".toPathUri()) - val inner = SamtFolder("file:///tmp/test/inner".toPathUri()) + val outer = SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri()) + val inner = SamtFolder("file:///tmp/test/src/inner/samt.yaml".toPathUri(), "file:///tmp/test/src/inner/src".toPathUri()) val workspace = SamtWorkspace() workspace.addFolder(outer) workspace.addFolder(inner) - val snapshot = workspace.getFolderSnapshot("file:///tmp/test/inner".toPathUri()) - assertEquals(outer.path, snapshot?.path) + val snapshot = workspace.getFolderSnapshot("file:///tmp/test/src/inner/src".toPathUri()) + assertEquals(outer.sourcePath, snapshot?.sourcePath) } @Test fun `folder which contains other folder overwrites it`() { - val inner = SamtFolder("file:///tmp/test/inner".toPathUri()) - val outer = SamtFolder("file:///tmp/test".toPathUri()) + val inner = SamtFolder("file:///tmp/test/src/inner/samt.yaml".toPathUri(), "file:///tmp/test/src/inner/src".toPathUri()) + val outer = SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri()) val workspace = SamtWorkspace() workspace.addFolder(inner) workspace.addFolder(outer) - val snapshot = workspace.getFolderSnapshot("file:///tmp/test/inner".toPathUri()) - assertEquals(outer.path, snapshot?.path) + val snapshot = workspace.getFolderSnapshot("file:///tmp/test/src/inner/src".toPathUri()) + assertEquals(outer.sourcePath, snapshot?.sourcePath) } @Test fun `getPendingMessages includes messages from new files`() { val workspace = SamtWorkspace() - workspace.addFolder(SamtFolder("file:///tmp/test".toPathUri())) - val uri = "file:///tmp/test/model.samt".toPathUri() + workspace.addFolder(SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri())) + val uri = "file:///tmp/test/src/model.samt".toPathUri() val fileInfo = parseFile(SourceFile (uri, "package foo.bar record Foo {")) workspace.setFile(fileInfo) val messages = workspace.getPendingMessages()[uri] @@ -64,8 +64,8 @@ class SamtWorkspaceTest { @Test fun `getPendingMessages includes emptyList for removed file`() { val workspace = SamtWorkspace() - workspace.addFolder(SamtFolder("file:///tmp/test".toPathUri())) - val uri = "file:///tmp/test/model.samt".toPathUri() + workspace.addFolder(SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri())) + val uri = "file:///tmp/test/src/model.samt".toPathUri() val fileInfo = parseFile(SourceFile (uri, "package foo.bar record Foo {")) workspace.setFile(fileInfo) workspace.removeFile(uri) @@ -76,12 +76,12 @@ class SamtWorkspaceTest { @Test fun `getPendingMessages includes empty list for every file in removed folder`() { val workspace = SamtWorkspace() - workspace.addFolder(SamtFolder("file:///tmp/test".toPathUri())) - val file1 = parseFile(SourceFile ("file:///tmp/test/foo.samt".toPathUri(), "package foo.bar record Foo {")) - val file2 = parseFile(SourceFile ("file:///tmp/test/bar.samt".toPathUri(), "package foo.bar record Bar {")) + workspace.addFolder(SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri())) + val file1 = parseFile(SourceFile ("file:///tmp/test/src/foo.samt".toPathUri(), "package foo.bar record Foo {")) + val file2 = parseFile(SourceFile ("file:///tmp/test/src/bar.samt".toPathUri(), "package foo.bar record Bar {")) workspace.setFile(file1) workspace.setFile(file2) - workspace.removeFolder("file:///tmp/test".toPathUri()) + workspace.removeFolder("file:///tmp/test/samt.yaml".toPathUri()) val messages = workspace.getPendingMessages() assertEquals(mapOf(file1.path to emptyList(), file2.path to emptyList()), messages) } @@ -89,7 +89,7 @@ class SamtWorkspaceTest { @Test fun `getPendingMessages is empty after clearing changes`() { val workspace = SamtWorkspace() - workspace.addFolder(SamtFolder("file:///tmp/test".toPathUri())) + workspace.addFolder(SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri())) val uri = "file:///tmp/test/model.samt".toPathUri() val fileInfo = parseFile(SourceFile (uri, "package foo.bar record Foo {")) workspace.setFile(fileInfo) @@ -101,9 +101,9 @@ class SamtWorkspaceTest { @Test fun `removing file triggers semantic error in dependent file`() { val workspace = SamtWorkspace() - workspace.addFolder(SamtFolder("file:///tmp/test".toPathUri())) - val file1 = parseFile(SourceFile ("file:///tmp/test/foo.samt".toPathUri(), "package foo.bar record Foo {}")) - val file2 = parseFile(SourceFile ("file:///tmp/test/bar.samt".toPathUri(), "package foo.bar record Bar { foo: Foo }")) + workspace.addFolder(SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri())) + val file1 = parseFile(SourceFile ("file:///tmp/test/src/foo.samt".toPathUri(), "package foo.bar record Foo {}")) + val file2 = parseFile(SourceFile ("file:///tmp/test/src/bar.samt".toPathUri(), "package foo.bar record Bar { foo: Foo }")) workspace.setFile(file1) workspace.setFile(file2) workspace.buildSemanticModel() @@ -122,12 +122,12 @@ class SamtWorkspaceTest { @Test fun `getPendingMessages includes empty list for every file in removed directory`() { val workspace = SamtWorkspace() - workspace.addFolder(SamtFolder("file:///tmp/test".toPathUri())) - val file1 = parseFile(SourceFile ("file:///tmp/test/subfolder/foo.samt".toPathUri(), "package foo.bar record Foo {")) - val file2 = parseFile(SourceFile ("file:///tmp/test/subfolder/bar.samt".toPathUri(), "package foo.bar record Bar {")) + workspace.addFolder(SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri())) + val file1 = parseFile(SourceFile ("file:///tmp/test/src/subfolder/foo.samt".toPathUri(), "package foo.bar record Foo {")) + val file2 = parseFile(SourceFile ("file:///tmp/test/src/subfolder/bar.samt".toPathUri(), "package foo.bar record Bar {")) workspace.setFile(file1) workspace.setFile(file2) - workspace.removeDirectory("file:///tmp/test/subfolder".toPathUri()) + workspace.removeDirectory("file:///tmp/test/src/subfolder".toPathUri()) val messages = workspace.getPendingMessages() assertEquals(mapOf(file1.path to emptyList(), file2.path to emptyList()), messages) } @@ -135,8 +135,8 @@ class SamtWorkspaceTest { @Test fun `semanticModel can be found after buildSemanticModel`() { val workspace = SamtWorkspace() - workspace.addFolder(SamtFolder("file:///tmp/test".toPathUri())) - val file1 = parseFile(SourceFile ("file:///tmp/test/foo.samt".toPathUri(), "package foo.bar record Foo {}")) + workspace.addFolder(SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri())) + val file1 = parseFile(SourceFile ("file:///tmp/test/src/foo.samt".toPathUri(), "package foo.bar record Foo {}")) workspace.setFile(file1) assertNull(workspace.getSemanticModel(file1.path)) workspace.buildSemanticModel() @@ -147,8 +147,8 @@ class SamtWorkspaceTest { @Test fun `if file has not changed pending messages don't change`() { val workspace = SamtWorkspace() - workspace.addFolder(SamtFolder("file:///tmp/test".toPathUri())) - val sourceFile = SourceFile("file:///tmp/test/foo.samt".toPathUri(), "package foo.bar record Foo {") + workspace.addFolder(SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri())) + val sourceFile = SourceFile("file:///tmp/test/src/foo.samt".toPathUri(), "package foo.bar record Foo {") workspace.setFile(parseFile(sourceFile)) assertEquals(1, workspace.getPendingMessages().size) workspace.clearChanges() From 03a3a83c43acf366a7892f6603bdb076412aa1b5 Mon Sep 17 00:00:00 2001 From: Marcel Joss Date: Fri, 26 May 2023 13:45:44 +0200 Subject: [PATCH 5/7] style(ls): fix inconsistent indentation --- .../tools/samt/ls/SamtTextDocumentService.kt | 176 +++++++++--------- 1 file changed, 89 insertions(+), 87 deletions(-) diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt index 6c5fb035..64561232 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt @@ -16,7 +16,7 @@ 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") @@ -57,110 +57,110 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume } override fun definition(params: DefinitionParams): CompletableFuture, List>> = - 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> = - 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 = - CompletableFuture.supplyAsync { - val path = params.textDocument.uri.toPathUri() - - val fileInfo = workspace.getFile(path) ?: return@supplyAsync SemanticTokens(emptyList()) - - val tokens: List = 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 = 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 = CompletableFuture.supplyAsync { val path = params.textDocument.uri.toPathUri() @@ -190,7 +190,8 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume } private fun UserDeclared.peekDeclaration(): String { - fun List.toParameterList(): String = joinToString(", ") { it.peekDeclaration() } + fun List.toParameterList(): String = + joinToString(", ") { it.peekDeclaration() } return when (this) { is AliasType -> "${getHumanReadableName()} $humanReadableName" @@ -217,6 +218,7 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume raisesTypes.joinTo(this, ", ") { it.humanReadableName } } } + is ServiceType.Operation.Parameter -> "$name: ${type.humanReadableName}" is RecordType -> "${getHumanReadableName()} $humanReadableName" is ServiceType -> "${getHumanReadableName()} $humanReadableName" From b9fc1072baaca6949cdbe79959359a77625a6915 Mon Sep 17 00:00:00 2001 From: Marcel Joss Date: Fri, 26 May 2023 14:22:29 +0200 Subject: [PATCH 6/7] feat(ls): watch samt.yaml files for source directory changes --- .../main/kotlin/tools/samt/ls/SamtConfig.kt | 5 +-- .../main/kotlin/tools/samt/ls/SamtFolder.kt | 6 ++-- .../tools/samt/ls/SamtLanguageServer.kt | 3 ++ .../kotlin/tools/samt/ls/SamtWorkspace.kt | 2 ++ .../tools/samt/ls/SamtWorkspaceService.kt | 17 ++++++---- .../kotlin/tools/samt/ls/SamtWorkspaceTest.kt | 33 +++++++++++++++++++ 6 files changed, 55 insertions(+), 11 deletions(-) diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtConfig.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtConfig.kt index b3aff5ef..c2af0bc6 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtConfig.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtConfig.kt @@ -1,16 +1,17 @@ package tools.samt.ls import java.nio.file.Path +import kotlin.io.path.Path import kotlin.io.path.isRegularFile -const val SAMT_CONFIG_FILE_NAME = "samt.yaml" +val SAMT_CONFIG_FILE_NAME = Path("samt.yaml") fun findSamtConfigs(path: Path): List { fun Path.parents(): Sequence = generateSequence(parent) { it.parent } return path.toFile().walkTopDown() .map { it.toPath() } - .filter { it.fileName.toString() == SAMT_CONFIG_FILE_NAME && it.isRegularFile() } + .filter { it.fileName == SAMT_CONFIG_FILE_NAME && it.isRegularFile() } .ifEmpty { path.parents() .map { it.resolve(SAMT_CONFIG_FILE_NAME) } diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt index 8f45a44c..0c07f8de 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt @@ -16,12 +16,12 @@ class SamtFolder(val configPath: URI, val sourcePath: URI) : Iterable private var semanticController: DiagnosticController = DiagnosticController(sourcePath) init { - require(configPath.toPath().fileName.toString() == SAMT_CONFIG_FILE_NAME) + require(configPath.toPath().fileName == SAMT_CONFIG_FILE_NAME) } fun set(fileInfo: FileInfo) { - require(fileInfo.sourceFile.path.startsWith(sourcePath)) - files[fileInfo.sourceFile.path] = fileInfo + require(fileInfo.path.startsWith(sourcePath)) + files[fileInfo.path] = fileInfo } fun remove(fileUri: URI) { diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt index adf35e49..2ae3efcf 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt @@ -109,6 +109,9 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable { globPattern = Either.forLeft("**/") kind = WatchKind.Create or WatchKind.Delete }, + FileSystemWatcher().apply { + globPattern = Either.forLeft("**/$SAMT_CONFIG_FILE_NAME") + } ) }) ))) diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt index 54180806..75dae1be 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt @@ -25,6 +25,7 @@ class SamtWorkspace { foldersByConfigPath.values.removeIf { it.sourcePath.startsWith(folder.sourcePath) } foldersByConfigPath[folder.configPath] = folder changedFolders.add(folder) + removedFiles.removeAll(folder.map { it.path }.toSet()) } fun removeFolder(configPath: URI) { @@ -41,6 +42,7 @@ class SamtWorkspace { val folder = getFolder(file.path) ?: return folder.set(file) changedFolders.add(folder) + removedFiles.remove(file.path) } fun removeFile(path: URI) { diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt index 882cbef5..dd40494c 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt @@ -23,18 +23,23 @@ class SamtWorkspaceService(private val workspace: SamtWorkspace) : WorkspaceServ for (change in params.changes) { val uri = change.uri.toPathUri() val path = uri.toPath() - if (path.isDirectory()) { - when (change.type) { + val changeType = checkNotNull(change.type) + when { + path.isDirectory() -> when (changeType) { FileChangeType.Created -> parseFilesInDirectory(uri).forEach(workspace::setFile) FileChangeType.Changed -> error("Directory changes should not be watched") FileChangeType.Deleted -> workspace.removeDirectory(uri) - null -> error("Unexpected null value for change.type") } - } else if (path.extension == "samt") { - when (change.type) { + path.fileName == SAMT_CONFIG_FILE_NAME -> when (changeType) { + FileChangeType.Created, FileChangeType.Changed -> { + workspace.removeFolder(uri) + workspace.addFolder(SamtFolder.fromConfig(uri)) + } + FileChangeType.Deleted -> workspace.removeFolder(uri) + } + path.extension == "samt" -> when (changeType) { FileChangeType.Created, FileChangeType.Changed -> workspace.setFile(readAndParseFile(uri)) FileChangeType.Deleted -> workspace.removeFile(uri) - null -> error("Unexpected null value for change.type") } } } diff --git a/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt b/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt index c887a9e9..099fe25b 100644 --- a/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt +++ b/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt @@ -155,4 +155,37 @@ class SamtWorkspaceTest { workspace.setFile(parseFile(sourceFile)) assertEquals(emptyMap(), workspace.getPendingMessages()) } + + @Test + fun `if file is removed and re-added pending messages are not empty`() { + val workspace = SamtWorkspace() + val sourceFile = SourceFile("file:///tmp/test/src/foo.samt".toPathUri(), "package foo.bar record Foo {") + workspace.addFolder(SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri())) + + workspace.setFile(parseFile(sourceFile)) + assertEquals(DiagnosticSeverity.Error, workspace.getPendingMessages()[sourceFile.path]?.single()?.severity) + + workspace.removeFile(sourceFile.path) + assertEquals(mapOf(sourceFile.path to emptyList()), workspace.getPendingMessages()) + + workspace.setFile(parseFile(sourceFile)) + assertEquals(DiagnosticSeverity.Error, workspace.getPendingMessages()[sourceFile.path]?.single()?.severity) + } + + @Test + fun `if folder is removed and re-added pending messages are not empty`() { + val workspace = SamtWorkspace() + val folder = SamtFolder("file:///tmp/test/samt.yaml".toPathUri(), "file:///tmp/test/src".toPathUri()) + val sourceFile = SourceFile("file:///tmp/test/src/foo.samt".toPathUri(), "package foo.bar record Foo {") + workspace.addFolder(folder) + + workspace.setFile(parseFile(sourceFile)) + assertEquals(DiagnosticSeverity.Error, workspace.getPendingMessages()[sourceFile.path]?.single()?.severity) + + workspace.removeFolder(folder.configPath) + assertEquals(mapOf(sourceFile.path to emptyList()), workspace.getPendingMessages()) + + workspace.addFolder(folder) + assertEquals(DiagnosticSeverity.Error, workspace.getPendingMessages()[sourceFile.path]?.single()?.severity) + } } From aae8ef443f471e42ae41d4af6deda047bb252006 Mon Sep 17 00:00:00 2001 From: Marcel Joss Date: Fri, 26 May 2023 16:32:19 +0200 Subject: [PATCH 7/7] feat(ls): handle samt.yaml configuration errors --- .../src/main/kotlin/tools/samt/ls/SamtFolder.kt | 15 ++++++++++----- .../kotlin/tools/samt/ls/SamtLanguageServer.kt | 4 +++- .../tools/samt/ls/SamtTextDocumentService.kt | 4 +++- .../kotlin/tools/samt/ls/SamtWorkspaceService.kt | 6 ++++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt index 0c07f8de..81c6f76d 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtFolder.kt @@ -54,11 +54,16 @@ class SamtFolder(val configPath: URI, val sourcePath: URI) : Iterable } companion object { - fun fromConfig(configPath: URI): SamtFolder { - val folder = configPath.toPath().let { - val config = SamtConfigurationParser.parseConfiguration(it) - SamtFolder(configPath.normalize(), it.resolveSibling(config.source).normalize().toUri()) - } + 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)) diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt index 2ae3efcf..72b14191 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt @@ -93,7 +93,9 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable { params.workspaceFolders ?.flatMap { folder -> val path = folder.uri.toPathUri().toPath() - findSamtConfigs(path).map { SamtFolder.fromConfig(it.toUri()) } + findSamtConfigs(path).mapNotNull { + SamtFolder.fromConfig(it.toUri()) + } }?.forEach(workspace::addFolder) } diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt index 64561232..dfe8103f 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt @@ -27,7 +27,9 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume if (!workspace.containsFile(path)) { val configPath = findSamtConfigs(path.toPath().parent).singleOrNull() - configPath?.let { workspace.addFolder(SamtFolder.fromConfig(it.toUri())) } + configPath + ?.let { SamtFolder.fromConfig(it.toUri()) } + ?.let { workspace.addFolder(it) } } workspace.setFile(parseFile(SourceFile(path, text))) client.updateWorkspace(workspace) diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt index dd40494c..4f89c8f1 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspaceService.kt @@ -33,7 +33,7 @@ class SamtWorkspaceService(private val workspace: SamtWorkspace) : WorkspaceServ path.fileName == SAMT_CONFIG_FILE_NAME -> when (changeType) { FileChangeType.Created, FileChangeType.Changed -> { workspace.removeFolder(uri) - workspace.addFolder(SamtFolder.fromConfig(uri)) + SamtFolder.fromConfig(uri)?.let { workspace.addFolder(it) } } FileChangeType.Deleted -> workspace.removeFolder(uri) } @@ -88,7 +88,9 @@ class SamtWorkspaceService(private val workspace: SamtWorkspace) : WorkspaceServ val event = params.event for (added in event.added) { val path = added.uri.toPathUri().toPath() - findSamtConfigs(path).forEach { workspace.addFolder(SamtFolder.fromConfig(it.toUri())) } + findSamtConfigs(path).forEach { configPath -> + SamtFolder.fromConfig(configPath.toUri())?.let { workspace.addFolder(it) } + } } for (removed in event.removed) { val path = removed.uri.toPathUri().toPath()