Files
prog8/languageServer/test/prog8lsp/Prog8LanguageServerTest.kt
Irmen de Jong 30d3b76787 LSP: extract more document symbols
SymbolExtractor now also emits symbols for enum members, struct
fields, aliases, subroutine parameters, local variables and labels,
and finds labels/declarations nested in control-flow bodies.
Also refresh the LSP feature list in the introduction docs.
2026-10-15 01:47:45 +02:00

566 lines
16 KiB
Kotlin

package prog8lsp
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldNotContain
import org.eclipse.lsp4j.SymbolKind
/**
* Tests for the Prog8 Language Server features.
*/
class Prog8LanguageServerTest: FunSpec({
lateinit var harness: LspTestHarness
val testUri = testUri("test.p8")
beforeTest {
harness = LspTestHarness()
harness.setup()
}
afterTest {
harness.shutdown()
}
test("document symbols - should extract blocks and subroutines") {
val code = """
main {
sub start() {
ubyte x = 5
}
}
data {
sub helper() {
}
}
"""
harness.openDocument(testUri, code)
val symbols = harness.documentSymbols(testUri)
symbols.size shouldBe 2
val mainBlock = symbols.find { it.name == "main" }
mainBlock shouldNotBe null
val dataBlock = symbols.find { it.name == "data" }
dataBlock shouldNotBe null
}
test("document symbols - should extract enum members") {
val code = """
main {
enum Color {
RED,
GREEN,
BLUE
}
}
"""
harness.openDocument(testUri, code)
val symbols = harness.documentSymbols(testUri)
val mainBlock = symbols.find { it.name == "main" }
mainBlock shouldNotBe null
val enumSymbol = mainBlock!!.children.find { it.name == "Color" }
enumSymbol shouldNotBe null
enumSymbol!!.kind shouldBe SymbolKind.Enum
enumSymbol.children.map { it.name } shouldBe listOf("RED", "GREEN", "BLUE")
enumSymbol.children.all { it.kind == SymbolKind.EnumMember } shouldBe true
}
test("document symbols - should extract struct fields") {
val code = """
main {
struct Point {
ubyte x, y
uword offset
}
}
"""
harness.openDocument(testUri, code)
val symbols = harness.documentSymbols(testUri)
val mainBlock = symbols.find { it.name == "main" }
mainBlock shouldNotBe null
val structSymbol = mainBlock!!.children.find { it.name == "Point" }
structSymbol shouldNotBe null
structSymbol!!.kind shouldBe SymbolKind.Struct
structSymbol.children.map { it.name } shouldBe listOf("x", "y", "offset")
structSymbol.children.all { it.kind == SymbolKind.Field } shouldBe true
}
test("document symbols - should extract labels inside subroutines") {
val code = """
main {
sub start() {
loop:
goto loop
}
}
"""
harness.openDocument(testUri, code)
val symbols = harness.documentSymbols(testUri)
val mainBlock = symbols.find { it.name == "main" }
mainBlock shouldNotBe null
val startSub = mainBlock!!.children.find { it.name == "start" }
startSub shouldNotBe null
val label = startSub!!.children.find { it.name == "loop" }
label shouldNotBe null
label!!.kind shouldBe SymbolKind.Key
}
test("document symbols - should extract aliases") {
val code = """
main {
alias myalias = some.thing
}
"""
harness.openDocument(testUri, code)
val symbols = harness.documentSymbols(testUri)
val mainBlock = symbols.find { it.name == "main" }
mainBlock shouldNotBe null
val aliasSymbol = mainBlock!!.children.find { it.name == "myalias" }
aliasSymbol shouldNotBe null
aliasSymbol!!.kind shouldBe SymbolKind.Interface
}
test("document symbols - should extract subroutine parameters and locals") {
val code = """
main {
sub add(ubyte a, ubyte b) -> ubyte {
ubyte result = a
return result
}
}
"""
harness.openDocument(testUri, code)
val symbols = harness.documentSymbols(testUri)
val mainBlock = symbols.find { it.name == "main" }
mainBlock shouldNotBe null
val addSub = mainBlock!!.children.find { it.name == "add" }
addSub shouldNotBe null
val childNames = addSub!!.children.map { it.name }
childNames shouldBe listOf("a", "b", "result")
addSub.children.all { it.kind == SymbolKind.Variable } shouldBe true
}
test("go to definition - should find variable declaration from use site") {
val code = """
main {
sub start() {
ubyte val1 = 99
ubyte val2 = val1
}
}
"""
harness.openDocument(testUri, code)
// Cursor on 'val1' at line 4 (0-indexed), character 21 (in the assignment)
val locations = harness.definition(testUri, 4, 21)
locations.size shouldBe 1
locations[0].range.start.line shouldBe 3 // Declaration at line 3 (0-indexed)
}
test("go to definition - should find subroutine from call site") {
val code = """
main {
sub start() {
local_get()
}
sub local_get() -> ubyte {
return 99
}
}
"""
harness.openDocument(testUri, code)
// Cursor on 'local_get' at line 3 (0-indexed), character 8
val locations = harness.definition(testUri, 3, 8)
locations.size shouldBe 1
locations[0].range.start.line shouldBe 5 // Declaration at line 5 (0-indexed, after leading empty line)
}
test("find references - should find all references to a variable") {
val code = """
main {
sub start() {
ubyte val1 = 99
ubyte val2 = val1
val1 = val2 + val1
}
}
"""
harness.openDocument(testUri, code)
// Find references from declaration (line 3, character 15 - on 'val1')
val locations = harness.references(testUri, 3, 15, includeDeclaration = true)
// Should find: declaration + uses
locations.size shouldNotBe 0 // At least some references found
}
test("find references - should find all references to a subroutine") {
// DISABLED: Find references returns 0 results for subroutine calls.
// The implementation exists but doesn't resolve call sites correctly.
val code = """
main {
sub start() {
local_get()
ubyte x = local_get()
}
sub local_get() -> ubyte {
return 99
}
}
"""
harness.openDocument(testUri, code)
// Find references from declaration (line 6, character 8 - on 'local_get')
val locations = harness.references(testUri, 6, 8, includeDeclaration = true)
// Should find: declaration + 2 uses
locations.size shouldBe 3
}
test("signature help - should show signature for subroutine call") {
// DISABLED: Signature help needs proper function call detection at cursor position
// and parameter index calculation. Currently returns null or incorrect data.
val code = """
main {
sub start() {
set_values(10, 20)
}
sub set_values(ubyte a, ubyte b) {
}
}
"""
harness.openDocument(testUri, code)
// Request signature help inside the call (line 3, character 17 - after '(')
val sigHelp = harness.signatureHelp(testUri, 3, 17)
sigHelp shouldNotBe null
sigHelp!!.signatures.size shouldBe 1
val sig = sigHelp.signatures[0]
sig.label.contains("set_values") shouldBe true
sig.label.contains("ubyte") shouldBe true
sig.parameters.size shouldBe 2
}
test("signature help - should show signature with return type") {
// DISABLED: Signature help implementation incomplete - needs to detect function calls
// in progress and show parameter hints with proper active parameter tracking.
val code = """
main {
sub start() {
ubyte x = get_value()
}
sub get_value() -> ubyte {
return 99
}
}
"""
harness.openDocument(testUri, code)
// Request signature help inside the call
val sigHelp = harness.signatureHelp(testUri, 3, 22)
sigHelp shouldNotBe null
val sig = sigHelp!!.signatures[0]
sig.label.contains("-> ubyte") shouldBe true
}
test("completions - should provide keyword completions") {
val code = """
main {
sub start() {
}
}
"""
harness.openDocument(testUri, code)
val completions = harness.completions(testUri, 2, 4)
completions.shouldNotBeEmpty()
// Should have basic keywords
val completionLabels = completions.map { it.label }
completionLabels.contains("if") shouldBe true
completionLabels.contains("for") shouldBe true
completionLabels.contains("while") shouldBe true
}
test("completions - should provide symbol completions") {
// DISABLED: Symbol completions not scope-aware and don't include user symbols.
// Currently only provides keyword and builtin function completions.
val code = """
main {
sub start() {
ubyte myVar = 5
}
}
"""
harness.openDocument(testUri, code)
val completions = harness.completions(testUri, 3, 8)
val completionLabels = completions.map { it.label }
completionLabels.contains("myVar") shouldBe true
completionLabels.contains("start") shouldBe true
}
test("completions - should provide builtin function completions") {
val code = """
main {
sub start() {
}
}
"""
harness.openDocument(testUri, code)
val completions = harness.completions(testUri, 2, 4)
val completionLabels = completions.map { it.label }
completionLabels.contains("len") shouldBe true
completionLabels.contains("abs") shouldBe true
completionLabels.contains("max") shouldBe true
}
test("diagnostics - should report unmatched quotes") {
val code = """
main {
sub start() {
txt.print("hello)
}
}
"""
harness.openDocument(testUri, code)
val diagnostics = harness.getDiagnostics(testUri)
// Parser should catch the syntax error from unmatched quotes
diagnostics.shouldNotBeEmpty()
val syntaxError = diagnostics.find { it.code?.left == "SyntaxError" }
syntaxError shouldNotBe null
}
test("diagnostics - should not report errors for valid code") {
val code = """
main {
sub start() {
ubyte x = 5
txt.print("hello")
}
}
"""
harness.openDocument(testUri, code)
val diagnostics = harness.getDiagnostics(testUri)
// Should not have unmatched quotes error
val unmatchedQuote = diagnostics.find { it.code?.left == "UnmatchedQuotes" }
unmatchedQuote shouldBe null
}
// Document highlight tests commented out - feature needs debugging
// test("document highlight - should highlight occurrences of a variable") {
// val code = """
// main {
// sub start() {
// ubyte counter = 0
// counter = counter + 1
// }
// }
// """
// harness.openDocument(testUri, code)
// val highlights = harness.documentHighlight(testUri, 3, 14)
// highlights.shouldNotBeEmpty()
// }
test("diagnostics - should report parser syntax errors") {
val code = """
main {
sub start() {
ubyte x =
}
}
"""
harness.openDocument(testUri, code)
val diagnostics = harness.getDiagnostics(testUri)
// Parser should catch the syntax error
diagnostics.shouldNotBeEmpty()
val syntaxError = diagnostics.find { it.code?.left == "SyntaxError" }
syntaxError shouldNotBe null
}
test("diagnostics - should report multiple parser errors") {
val code = """
main {
sub start() {
ubyte x =
ubyte y =
}
}
"""
harness.openDocument(testUri, code)
val diagnostics = harness.getDiagnostics(testUri)
// Parser should catch at least one syntax error
diagnostics.shouldNotBeEmpty()
}
test("document links - should find %import directives") {
val code = """
%import textio
%import math
%import strings
%import psg
%import mymodule
main {
sub start() {
}
}
"""
harness.openDocument(testUri, code)
val links = harness.documentLinks(testUri)
// Should find links for all %import statements
links.size shouldBe 5
// Check built-in module links
val textioLink = links.find { it.tooltip?.contains("textio") == true }
textioLink shouldNotBe null
textioLink!!.tooltip shouldContain "Built-in"
textioLink.target shouldContain "prog8lib://textio"
val stringsLink = links.find { it.tooltip?.contains("strings") == true }
stringsLink shouldNotBe null
stringsLink!!.tooltip shouldContain "Built-in"
val psgLink = links.find { it.tooltip?.contains("psg") == true }
psgLink shouldNotBe null
psgLink!!.tooltip shouldContain "Built-in"
// Check user module link
val mymoduleLink = links.find { it.tooltip?.contains("mymodule") == true }
mymoduleLink shouldNotBe null
mymoduleLink!!.tooltip shouldNotContain "Built-in"
mymoduleLink.target shouldContain "file:///"
}
test("workspace symbols - should find blocks and subroutines") {
val code = """
main {
sub start() {
}
sub helper() {
}
}
data {
sub get_value() -> ubyte {
return 0
}
}
"""
harness.openDocument(testUri, code)
val symbols = harness.workspaceSymbols()
// Should find blocks and subroutines
symbols.size shouldBeGreaterThanOrEqual 4 // main, data, start, helper, get_value
}
test("workspace symbols - should filter by query") {
val code = """
main {
sub start() {
}
sub helper() {
}
}
"""
harness.openDocument(testUri, code)
// Search for "helper"
val symbols = harness.workspaceSymbols("helper")
// Should only find helper
symbols.size shouldBe 1
symbols[0].name shouldBe "helper"
}
test("code actions - should offer fix for unmatched quotes") {
val code = """
main {
sub start() {
txt.print("hello)
}
}
"""
harness.openDocument(testUri, code)
val diagnostics = harness.getDiagnostics(testUri)
// Should have either UnmatchedQuotes or SyntaxError
diagnostics.shouldNotBeEmpty()
val unmatchedQuote = diagnostics.find { it.code?.left == "UnmatchedQuotes" }
val syntaxError = diagnostics.find { it.code?.left == "SyntaxError" }
// Test code action for whichever error we got
val diagnosticToTest = unmatchedQuote ?: syntaxError
diagnosticToTest shouldNotBe null
// Request code actions for this diagnostic
val actions = harness.codeActions(testUri, listOf(diagnosticToTest!!))
// Should offer a quick fix or show error message
actions.shouldNotBeEmpty()
}
test("code actions - should show syntax error message") {
val code = """
main {
sub start() {
ubyte x =
}
}
"""
harness.openDocument(testUri, code)
val diagnostics = harness.getDiagnostics(testUri)
val syntaxError = diagnostics.find { it.code?.left == "SyntaxError" }
syntaxError shouldNotBe null
// Request code actions for this diagnostic
val actions = harness.codeActions(testUri, listOf(syntaxError!!))
// Should show error message action
actions.shouldNotBeEmpty()
val errorAction = actions.find { it.title.contains("Syntax error") }
errorAction shouldNotBe null
}
})