mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
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.
566 lines
16 KiB
Kotlin
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
|
|
}
|
|
})
|