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 72b14191..1e9bf138 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt @@ -59,6 +59,7 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable { definitionProvider = Either.forLeft(true) referencesProvider = Either.forLeft(true) hoverProvider = Either.forLeft(true) + documentSymbolProvider = Either.forLeft(true) } InitializeResult(capabilities) } 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 dfe8103f..39a60dd4 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt @@ -191,6 +191,13 @@ class SamtTextDocumentService(private val workspace: SamtWorkspace) : TextDocume } } + override fun documentSymbol(params: DocumentSymbolParams): CompletableFuture>> = CompletableFuture.supplyAsync { + val path = params.textDocument.uri.toPathUri() + val fileInfo = workspace.getFile(path) ?: return@supplyAsync emptyList() + + fileInfo.fileNode?.getSymbols()?.map { Either.forRight(it) }.orEmpty() + } + private fun UserDeclared.peekDeclaration(): String { fun List.toParameterList(): String = joinToString(", ") { it.peekDeclaration() } diff --git a/language-server/src/main/kotlin/tools/samt/ls/Symbols.kt b/language-server/src/main/kotlin/tools/samt/ls/Symbols.kt new file mode 100644 index 00000000..0131f752 --- /dev/null +++ b/language-server/src/main/kotlin/tools/samt/ls/Symbols.kt @@ -0,0 +1,40 @@ +package tools.samt.ls + +import org.eclipse.lsp4j.DocumentSymbol +import org.eclipse.lsp4j.SymbolKind +import tools.samt.parser.* + +fun FileNode.getSymbols(): List = buildList { + add(packageDeclaration.toSymbol()) + for (statement in statements) { + when (statement) { + is NamedDeclarationNode -> add(statement.toSymbol()) + is ConsumerDeclarationNode -> add(statement.toSymbol()) + is TypeImportNode, is WildcardImportNode -> {} + is PackageDeclarationNode -> error("Unexpected package declaration") + } + } +} + +private fun NamedDeclarationNode.toSymbol(): DocumentSymbol { + val kind = when (this) { + is EnumDeclarationNode -> SymbolKind.Enum + is ProviderDeclarationNode -> SymbolKind.Class + is RecordDeclarationNode -> SymbolKind.Struct + is ServiceDeclarationNode -> SymbolKind.Interface + is TypeAliasNode -> SymbolKind.Class + } + val children = when (this) { + is EnumDeclarationNode -> values.map { DocumentSymbol(it.name, SymbolKind.EnumMember, it.location.toRange(), it.location.toRange()) } + is RecordDeclarationNode -> fields.map { DocumentSymbol(it.name.name, SymbolKind.Property, it.location.toRange(), it.name.location.toRange()) } + is ServiceDeclarationNode -> operations.map { DocumentSymbol(it.name.name, SymbolKind.Method, it.location.toRange(), it.name.location.toRange()) } + is ProviderDeclarationNode, is TypeAliasNode -> emptyList() + } + return DocumentSymbol(name.name, kind, location.toRange(), name.location.toRange()).apply { + this.children = children + } +} + +private fun ConsumerDeclarationNode.toSymbol() = DocumentSymbol("Consumer for ${providerName.name}", SymbolKind.Class, location.toRange(), providerName.location.toRange()) + +private fun PackageDeclarationNode.toSymbol() = DocumentSymbol(name.name, SymbolKind.Package, location.toRange(), name.location.toRange()) diff --git a/language-server/src/test/kotlin/tools/samt/ls/SymbolsTest.kt b/language-server/src/test/kotlin/tools/samt/ls/SymbolsTest.kt new file mode 100644 index 00000000..76a20ca0 --- /dev/null +++ b/language-server/src/test/kotlin/tools/samt/ls/SymbolsTest.kt @@ -0,0 +1,106 @@ +package tools.samt.ls + +import org.eclipse.lsp4j.DocumentSymbol +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.SymbolKind +import tools.samt.common.SourceFile +import kotlin.test.Test +import kotlin.test.assertEquals + +class SymbolsTest { + @Test + fun `getSymbols returns correct symbols`() { + val source = """ + package foo.bar + enum Foo { + A + } + record Bar { + a: Int + } + service Baz { + a() + } + provide BazProvider { + implements Baz + + transport http + } + consume BazProvider { + uses Baz + } + """.trimIndent() + val sourceFile = SourceFile("file:///tmp/test/src/model.samt".toPathUri(), source) + val fileInfo = parseFile(sourceFile) + val symbols = fileInfo.fileNode?.getSymbols() + assertEquals( + listOf( + DocumentSymbol( + "foo.bar", + SymbolKind.Package, + Range(Position(0, 0), Position(0, 15)), + Range(Position(0, 8), Position(0, 15)) + ), + DocumentSymbol( + "Foo", + SymbolKind.Enum, + Range(Position(1, 0), Position(3, 1)), + Range(Position(1, 5), Position(1, 8)) + ).apply { + children = listOf( + DocumentSymbol( + "A", + SymbolKind.EnumMember, + Range(Position(2, 4), Position(2, 5)), + Range(Position(2, 4), Position(2, 5)) + ) + ) + }, + DocumentSymbol( + "Bar", + SymbolKind.Struct, + Range(Position(4, 0), Position(6, 1)), + Range(Position(4, 7), Position(4, 10)) + ).apply { + children = listOf( + DocumentSymbol( + "a", + SymbolKind.Property, + Range(Position(5, 4), Position(5, 10)), + Range(Position(5, 4), Position(5, 5)) + ) + ) + }, + DocumentSymbol( + "Baz", + SymbolKind.Interface, + Range(Position(7, 0), Position(9, 1)), + Range(Position(7, 8), Position(7, 11)) + ).apply { + children = listOf( + DocumentSymbol( + "a", + SymbolKind.Method, + Range(Position(8, 4), Position(8, 7)), + Range(Position(8, 4), Position(8, 5)) + ) + ) + }, + DocumentSymbol( + "BazProvider", + SymbolKind.Class, + Range(Position(10, 0), Position(14, 1)), + Range(Position(10, 8), Position(10, 19)) + ).apply { children = emptyList() }, + DocumentSymbol( + "Consumer for BazProvider", + SymbolKind.Class, + Range(Position(15, 0), Position(17, 1)), + Range(Position(15, 8), Position(15, 19)) + ) + ), + symbols + ) + } +}