plugin: autocomplete for enums and members, better support for local symbols; fixed bug in space normalization

This commit is contained in:
sergeych committed 2026-01-02 16:03:57 +01:00
1 parent 22f6c149db
commit fac58675d5
12 files changed
+581 -187

No files matched your search

@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1750,6 +1750,9 @@ class Compiler(
}
private fun parseEnumDeclaration(): Statement {
val startPos = pendingDeclStart ?: cc.currentPos()
val doc = pendingDeclDoc ?: consumePendingDoc()
pendingDeclDoc = null
val nameToken = cc.requireToken(Token.Type.ID)
// so far only simplest enums:
val names = mutableListOf<String>()
@@ -1777,6 +1780,16 @@ class Compiler(
}
} while (true)
miniSink?.onEnumDecl(
MiniEnumDecl(
range = MiniRange(startPos, cc.currentPos()),
name = nameToken.value,
entries = names,
doc = doc,
nameStart = nameToken.pos
)
)
return statement {
ObjEnumClass.createSimpleEnum(nameToken.value, names).also {
addItem(nameToken.value, false, it, recordType = ObjRecord.Type.Enum)
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,10 +25,7 @@ import net.sergeych.lyng.Source
import net.sergeych.lyng.highlight.HighlightKind
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.miniast.MiniClassDecl
import net.sergeych.lyng.miniast.MiniFunDecl
import net.sergeych.lyng.miniast.MiniScript
import net.sergeych.lyng.miniast.MiniValDecl
import net.sergeych.lyng.miniast.*
enum class SymbolKind { Class, Enum, Function, Val, Var, Param }
@@ -160,6 +157,12 @@ object Binder {
topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id)
}
}
is MiniEnumDecl -> {
val (s, e) = nameOffsets(d.nameStart, d.name)
val sym = Symbol(nextId++, d.name, SymbolKind.Enum, s, e, containerId = null)
symbols += sym
topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id)
}
}
}
@@ -48,11 +48,6 @@ object LyngFormatter {
}
}
fun codePart(s: String): String {
val idx = s.indexOf("//")
return if (idx >= 0) s.substring(0, idx) else s
}
fun indentOf(level: Int, continuation: Int): String =
// Always produce spaces; tabs are not allowed in resulting code
" ".repeat(level * config.indentSize + continuation)
@@ -69,10 +64,10 @@ object LyngFormatter {
}
for ((i, rawLine) in lines.withIndex()) {
val line = rawLine
val trimmedLine = line.trim()
val code = codePart(line)
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
val code = parts.filter { it.type == PartType.Code }.joinToString("") { it.text }
val trimmedStart = code.dropWhile { it == ' ' || it == '\t' }
val trimmedLine = rawLine.trim()
// Compute effective indent level for this line
var effectiveLevel = blockLevel
@@ -93,15 +88,7 @@ object LyngFormatter {
if (applyAwaiting) effectiveLevel += 1
val firstChar = trimmedStart.firstOrNull()
// Do not apply continuation on a line that starts with a closer ')' or ']'
val startsWithCloser = firstChar == ')' || firstChar == ']'
// Kotlin-like rule: continuation persists while inside parentheses; it applies
// even on the line that starts with ')'. For brackets, do not apply on the ']' line itself.
// Continuation rules:
// - For brackets: one-shot continuation for first element after '[', and while inside brackets
// (except on the ']' line) continuation equals one unit.
// - For parentheses: continuation depth scales with nested level; e.g., inside two nested
// parentheses lines get 2 * continuationIndentSize. No continuation on a line that starts with ')'.
// While inside parentheses, continuation applies scaled by nesting level
val parenContLevels = if (parenBalance > 0 && firstChar != ')') parenBalance else 0
val continuation = when {
// One-shot continuation when previous line ended with '[' to align first element
@@ -120,8 +107,8 @@ object LyngFormatter {
}
// Replace leading whitespace with the exact target indent; but keep fully blank lines truly empty
val contentStart = line.indexOfFirst { it != ' ' && it != '\t' }.let { if (it < 0) line.length else it }
var content = line.substring(contentStart)
val contentStart = rawLine.indexOfFirst { it != ' ' && it != '\t' }.let { if (it < 0) rawLine.length else it }
var content = rawLine.substring(contentStart)
// Collapse spaces right after an opening '[' to avoid "[ 1"; make it "[1"
if (content.startsWith("[")) {
content = "[" + content.drop(1).trimStart()
@@ -135,11 +122,7 @@ object LyngFormatter {
}
// Determine base indent using structural level and continuation only (spaces only)
val indentString = indentOf(effectiveLevel, continuation)
if (content.isEmpty()) {
// preserve truly blank line as empty to avoid trailing spaces on empty lines
// (also keeps continuation blocks visually clean)
// do nothing, just append nothing; newline will be appended below if needed
} else {
if (content.isNotEmpty()) {
sb.append(indentString).append(content)
}
@@ -147,34 +130,12 @@ object LyngFormatter {
if (i < lines.lastIndex) sb.append('\n')
// Update balances using this line's code content
if (!inBlockComment) {
val startIdx = code.indexOf("/*")
if (startIdx >= 0) {
val endIdx = code.indexOf("*/", startIdx + 2)
if (endIdx < 0) {
inBlockComment = true
// Process code before /*
val before = code.substring(0, startIdx)
for (ch in before) updateBalances(ch)
} else {
// Block comment starts and ends on the same line
val before = code.substring(0, startIdx)
val after = code.substring(endIdx + 2)
for (ch in before) updateBalances(ch)
for (ch in after) updateBalances(ch)
}
} else {
for (ch in code) updateBalances(ch)
}
} else {
val endIdx = line.indexOf("*/")
if (endIdx >= 0) {
inBlockComment = false
val after = line.substring(endIdx + 2)
val codeAfter = codePart(after)
for (ch in codeAfter) updateBalances(ch)
for (part in parts) {
if (part.type == PartType.Code) {
for (ch in part.text) updateBalances(ch)
}
}
inBlockComment = nextInBlockComment
// Update awaitingSingleIndent based on current line
if (applyAwaiting && trimmedStart.isNotEmpty()) {
@@ -189,70 +150,41 @@ object LyngFormatter {
}
// Prepare one-shot bracket continuation if the current line ends with '['
// (first element line gets continuation even before balances update propagate).
val endsWithBracket = code.trimEnd().endsWith("[")
// Reset one-shot flag after we used it on this line
if (prevBracketContinuation) prevBracketContinuation = false
// Set for the next iteration if current line ends with '['
// Record whether THIS line ends with an opening '[' so the NEXT line gets a one-shot
// continuation indent for the first element.
if (endsWithBracket) {
// One-shot continuation for the very next line
prevBracketContinuation = true
} else {
// Reset the one-shot flag if the previous line didn't end with '['
// Reset the one-shot flag if it was used or if line doesn't end with '['
prevBracketContinuation = false
}
}
return sb.toString()
}
/** Full format. Currently performs indentation only; spacing/wrapping can be added later. */
fun format(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
// Phase 1: indentation
val indented = reindent(text, config)
if (!config.applySpacing && !config.applyWrapping) return indented
// Phase 2: minimal, safe spacing (PSI-free). Skip block comments completely and
// only apply spacing to the part before '//' on each line.
// Phase 2: minimal, safe spacing (PSI-free).
val lines = indented.split('\n')
val out = StringBuilder(indented.length)
var inBlockComment = false
for ((i, rawLine) in lines.withIndex()) {
var line = rawLine
if (config.applySpacing) {
if (inBlockComment) {
// Pass-through until we see the end of the block comment on some line
val end = line.indexOf("*/")
if (end >= 0) {
inBlockComment = false
}
} else {
// If this line opens a block comment, apply spacing only before the opener
val startIdx = line.indexOf("/*")
val endIdx = line.indexOf("*/")
if (startIdx >= 0 && (endIdx < 0 || endIdx < startIdx)) {
val before = line.substring(0, startIdx)
val after = line.substring(startIdx)
val commentIdx = before.indexOf("//")
val code = if (commentIdx >= 0) before.substring(0, commentIdx) else before
val tail = if (commentIdx >= 0) before.substring(commentIdx) else ""
val spaced = applyMinimalSpacing(code)
line = (spaced + tail) + after
inBlockComment = true
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
val sb = StringBuilder()
for (part in parts) {
if (part.type == PartType.Code) {
sb.append(applyMinimalSpacingRules(part.text))
} else {
// Normal code line: respect single-line comments
val commentIdx = line.indexOf("//")
if (commentIdx >= 0) {
val code = line.substring(0, commentIdx)
val tail = line.substring(commentIdx)
val spaced = applyMinimalSpacing(code)
line = spaced + tail
} else {
line = applyMinimalSpacing(line)
}
sb.append(part.text)
}
}
line = sb.toString()
inBlockComment = nextInBlockComment
}
out.append(line.trimEnd())
if (i < lines.lastIndex) out.append('\n')
@@ -409,7 +341,80 @@ object LyngFormatter {
}
}
private fun applyMinimalSpacing(code: String): String {
private enum class PartType { Code, StringLiteral, BlockComment, LineComment }
private data class Part(val text: String, val type: PartType)
/**
* Split a line into parts: code, string literals, and comments.
* Tracks [inBlockComment] state across lines.
*/
private fun splitIntoParts(
text: String,
inBlockCommentInitial: Boolean
): Pair<List<Part>, Boolean> {
val result = mutableListOf<Part>()
var i = 0
var last = 0
var inBlockComment = inBlockCommentInitial
var inString = false
var quoteChar = ' '
while (i < text.length) {
if (inBlockComment) {
if (text.startsWith("*/", i)) {
result.add(Part(text.substring(last, i + 2), PartType.BlockComment))
inBlockComment = false
i += 2
last = i
} else {
i++
}
} else if (inString) {
if (text[i] == quoteChar) {
var escapeCount = 0
var j = i - 1
while (j >= 0 && text[j] == '\\') {
escapeCount++
j--
}
if (escapeCount % 2 == 0) {
inString = false
result.add(Part(text.substring(last, i + 1), PartType.StringLiteral))
last = i + 1
}
}
i++
} else {
if (text.startsWith("//", i)) {
if (i > last) result.add(Part(text.substring(last, i), PartType.Code))
result.add(Part(text.substring(i), PartType.LineComment))
last = text.length
break
} else if (text.startsWith("/*", i)) {
if (i > last) result.add(Part(text.substring(last, i), PartType.Code))
inBlockComment = true
last = i
i += 2
} else if (text[i] == '"' || text[i] == '\'') {
if (i > last) result.add(Part(text.substring(last, i), PartType.Code))
inString = true
quoteChar = text[i]
last = i
i++
} else {
i++
}
}
}
if (last < text.length) {
val leftover = text.substring(last)
val type = if (inBlockComment) PartType.BlockComment else PartType.Code
result.add(Part(leftover, type))
}
return result to inBlockComment
}
private fun applyMinimalSpacingRules(code: String): String {
var s = code
// Ensure space before '(' for control-flow keywords
s = s.replace(Regex("\\b(if|for|while)\\("), "$1 (")
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -113,7 +113,7 @@ object BuiltinDocRegistry : BuiltinDocSource {
val src = try { rootLyng } catch (_: Throwable) { null } ?: return emptyList()
val out = LinkedHashSet<String>()
// Match lines like: fun String.trim(...) or val Int.isEven = ...
val re = Regex("(?m)^\\s*(?:fun|val|var)\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\b")
val re = Regex("^\\s*(?:fun|val|var)\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\b", RegexOption.MULTILINE)
re.findAll(src).forEach { m ->
val name = m.groupValues.getOrNull(1)?.trim()
if (!name.isNullOrEmpty()) out.add(name)
@@ -604,6 +604,11 @@ private fun buildStdlibDocs(): List<MiniDecl> {
method(name = "printStackTrace", doc = StdlibInlineDocIndex.methodDoc("Exception", "printStackTrace") ?: "Print this exception and its stack trace to standard output.")
}
mod.classDoc(name = "Enum", doc = StdlibInlineDocIndex.classDoc("Enum") ?: "Base class for all enums.") {
method(name = "name", doc = "Returns the name of this enum constant.", returns = type("lyng.String"))
method(name = "ordinal", doc = "Returns the ordinal of this enum constant.", returns = type("lyng.Int"))
}
mod.classDoc(name = "String", doc = StdlibInlineDocIndex.classDoc("String") ?: "String helpers.") {
// Only include inline-source method here; Kotlin-embedded methods are now documented via DocHelpers near definitions.
method(name = "re", doc = StdlibInlineDocIndex.methodDoc("String", "re") ?: "Compile this string into a regular expression.", returns = type("lyng.Regex"))
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,7 +33,7 @@ data class CompletionItem(
val typeText: String? = null,
)
enum class Kind { Function, Class_, Value, Method, Field }
enum class Kind { Function, Class_, Enum, Value, Method, Field }
/**
* Platform-free, lenient import provider that never fails on unknown packages.
@@ -82,23 +82,23 @@ object CompletionEngineLight {
val memberDot = findDotLeft(text, word?.first ?: caret)
if (memberDot != null) {
// 0) Try chained member call return type inference
guessReturnClassFromMemberCallBefore(text, memberDot, imported)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls)
guessReturnClassFromMemberCallBefore(text, memberDot, imported, mini)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
// 0a) Top-level call before dot
guessReturnClassFromTopLevelCallBefore(text, memberDot, imported)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls)
guessReturnClassFromTopLevelCallBefore(text, memberDot, imported, mini)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
// 0b) Across-known-callees (Iterable/Iterator/List preference)
guessReturnClassAcrossKnownCallees(text, memberDot, imported)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls)
guessReturnClassAcrossKnownCallees(text, memberDot, imported, mini)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
// 1) Receiver inference fallback
guessReceiverClass(text, memberDot, imported)?.let { cls ->
offerMembersAdd(out, prefix, imported, cls)
(guessReceiverClassViaMini(mini, text, memberDot, imported) ?: guessReceiverClass(text, memberDot, imported, mini))?.let { cls ->
offerMembersAdd(out, prefix, imported, cls, mini)
return out
}
// In member context and unknown receiver/return type: show nothing (no globals after dot)
@@ -110,9 +110,11 @@ object CompletionEngineLight {
val decls = m.declarations
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
funs.forEach { offerDeclAdd(out, prefix, it) }
classes.forEach { offerDeclAdd(out, prefix, it) }
enums.forEach { offerDeclAdd(out, prefix, it) }
vals.forEach { offerDeclAdd(out, prefix, it) }
}
@@ -126,9 +128,11 @@ object CompletionEngineLight {
val decls = BuiltinDocRegistry.docsForModule(mod)
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
funs.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
classes.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
enums.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
vals.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
if (out.size >= cap || externalAdded >= budget) break
}
@@ -147,13 +151,14 @@ object CompletionEngineLight {
add(CompletionItem(d.name, Kind.Function, tailText = tail, typeText = typeOf(d.returnType)))
}
is MiniClassDecl -> add(CompletionItem(d.name, Kind.Class_))
is MiniEnumDecl -> add(CompletionItem(d.name, Kind.Enum))
is MiniValDecl -> add(CompletionItem(d.name, Kind.Value, typeText = typeOf(d.type)))
// else -> add(CompletionItem(d.name, Kind.Value))
}
}
private fun offerMembersAdd(out: MutableList<CompletionItem>, prefix: String, imported: List<String>, className: String) {
val classes = DocLookupUtils.aggregateClasses(imported)
private fun offerMembersAdd(out: MutableList<CompletionItem>, prefix: String, imported: List<String>, className: String, mini: MiniScript? = null) {
val classes = DocLookupUtils.aggregateClasses(imported, mini)
val visited = mutableSetOf<String>()
val directMap = LinkedHashMap<String, MutableList<MiniMemberDecl>>()
val inheritedMap = LinkedHashMap<String, MutableList<MiniMemberDecl>>()
@@ -242,8 +247,51 @@ object CompletionEngineLight {
// --- Inference helpers (text-only, PSI-free) ---
private fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>): String? {
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported)?.let { return it }
private fun guessReceiverClassViaMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>): String? {
if (mini == null) return null
val i = prevNonWs(text, dotPos - 1)
if (i < 0) return null
val wordRange = wordRangeAt(text, i + 1) ?: return null
val ident = text.substring(wordRange.first, wordRange.second)
// 1) Global declarations in current file (val/var/fun/class/enum)
val d = mini.declarations.firstOrNull { it.name == ident }
if (d != null) {
return when (d) {
is MiniClassDecl -> d.name
is MiniEnumDecl -> d.name
is MiniValDecl -> simpleClassNameOf(d.type)
is MiniFunDecl -> simpleClassNameOf(d.returnType)
}
}
// 2) Recursive chaining: Base.ident.
val dotBefore = findDotLeft(text, wordRange.first)
if (dotBefore != null) {
val receiverClass = guessReceiverClassViaMini(mini, text, dotBefore, imported)
?: guessReceiverClass(text, dotBefore, imported, mini)
if (receiverClass != null) {
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, ident, mini)
if (resolved != null) {
val rt = when (val m = resolved.second) {
is MiniMemberFunDecl -> m.returnType
is MiniMemberValDecl -> m.type
else -> null
}
return simpleClassNameOf(rt)
}
}
}
// 3) Check if it's a known class (static access)
val classes = DocLookupUtils.aggregateClasses(imported, mini)
if (classes.containsKey(ident)) return ident
return null
}
private fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported, mini)?.let { return it }
var i = prevNonWs(text, dotPos - 1)
if (i >= 0) {
when (text[i]) {
@@ -304,12 +352,16 @@ object CompletionEngineLight {
return ident
}
}
// 4) Check if it's a known class (static access)
val classes = DocLookupUtils.aggregateClasses(imported, mini)
if (classes.containsKey(ident)) return ident
}
}
return null
}
private fun guessReturnClassFromMemberCallBefore(text: String, dotPos: Int, imported: List<String>): String? {
private fun guessReturnClassFromMemberCallBefore(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
var i = prevNonWs(text, dotPos - 1)
if (i < 0 || text[i] != ')') return null
i--
@@ -333,8 +385,8 @@ object CompletionEngineLight {
while (k >= 0 && text[k].isWhitespace()) k--
if (k < 0 || text[k] != '.') return null
val prevDot = k
val receiverClass = guessReceiverClass(text, prevDot, imported) ?: return null
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, callee) ?: return null
val receiverClass = guessReceiverClass(text, prevDot, imported, mini) ?: return null
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, callee, mini) ?: return null
val member = resolved.second
val ret = when (member) {
is MiniMemberFunDecl -> member.returnType
@@ -344,7 +396,7 @@ object CompletionEngineLight {
return simpleClassNameOf(ret)
}
private fun guessReturnClassFromTopLevelCallBefore(text: String, dotPos: Int, imported: List<String>): String? {
private fun guessReturnClassFromTopLevelCallBefore(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
var i = prevNonWs(text, dotPos - 1)
if (i < 0 || text[i] != ')') return null
i--
@@ -372,10 +424,12 @@ object CompletionEngineLight {
val fn = decls.asSequence().filterIsInstance<MiniFunDecl>().firstOrNull { it.name == callee }
if (fn != null) return simpleClassNameOf(fn.returnType)
}
// Also check local declarations
mini?.declarations?.filterIsInstance<MiniFunDecl>()?.firstOrNull { it.name == callee }?.let { return simpleClassNameOf(it.returnType) }
return null
}
private fun guessReturnClassAcrossKnownCallees(text: String, dotPos: Int, imported: List<String>): String? {
private fun guessReturnClassAcrossKnownCallees(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
var i = prevNonWs(text, dotPos - 1)
if (i < 0 || text[i] != ')') return null
i--
@@ -395,7 +449,7 @@ object CompletionEngineLight {
val start = j + 1
if (start >= end) return null
val callee = text.substring(start, end)
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, callee) ?: return null
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, callee, mini) ?: return null
val member = resolved.second
val ret = when (member) {
is MiniMemberFunDecl -> member.returnType
@@ -415,14 +469,16 @@ object CompletionEngineLight {
// --- MiniAst and small utils ---
private suspend fun buildMiniAst(text: String): MiniScript? = try {
private suspend fun buildMiniAst(text: String): MiniScript? {
val sink = MiniAstBuilder()
val src = Source("<engine>", text)
val provider = LenientImportProvider.create()
Compiler.compileWithMini(src, provider, sink)
sink.build()
} catch (_: Throwable) {
null
return try {
val src = Source("<engine>", text)
val provider = LenientImportProvider.create()
Compiler.compileWithMini(src, provider, sink)
sink.build()
} catch (_: Throwable) {
sink.build()
}
}
private fun typeOf(t: MiniTypeRef?): String = when (t) {
@@ -474,7 +530,7 @@ object CompletionEngineLight {
private fun extractImportsFromText(text: String): List<String> {
val result = LinkedHashSet<String>()
val re = Regex("(?m)^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)")
val re = Regex("^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", RegexOption.MULTILINE)
re.findAll(text).forEach { m ->
val raw = m.groupValues.getOrNull(1)?.trim().orEmpty()
if (raw.isNotEmpty()) result.add(if (raw.startsWith("lyng.")) raw else "lyng.$raw")
@@ -1,3 +1,20 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/*
* Shared QuickDoc lookup helpers reusable outside the IDEA plugin.
*/
@@ -24,7 +41,7 @@ object DocLookupUtils {
return result.toList()
}
fun aggregateClasses(importedModules: List<String>): Map<String, MiniClassDecl> {
fun aggregateClasses(importedModules: List<String>, localMini: MiniScript? = null): Map<String, MiniClassDecl> {
// Collect all class decls by name across modules, then merge duplicates by unioning members and bases.
val buckets = LinkedHashMap<String, MutableList<MiniClassDecl>>()
for (mod in importedModules) {
@@ -32,6 +49,23 @@ object DocLookupUtils {
for (cls in docs.filterIsInstance<MiniClassDecl>()) {
buckets.getOrPut(cls.name) { mutableListOf() }.add(cls)
}
for (en in docs.filterIsInstance<MiniEnumDecl>()) {
buckets.getOrPut(en.name) { mutableListOf() }.add(en.toSyntheticClass())
}
}
// Add local declarations
localMini?.declarations?.forEach { d ->
when (d) {
is MiniClassDecl -> {
buckets.getOrPut(d.name) { mutableListOf() }.add(d)
}
is MiniEnumDecl -> {
val syn = d.toSyntheticClass()
buckets.getOrPut(d.name) { mutableListOf() }.add(syn)
}
else -> {}
}
}
fun mergeClassDecls(name: String, list: List<MiniClassDecl>): MiniClassDecl {
@@ -72,8 +106,8 @@ object DocLookupUtils {
return result
}
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String): Pair<String, MiniMemberDecl>? {
val classes = aggregateClasses(importedModules)
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? {
val classes = aggregateClasses(importedModules, localMini)
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniMemberDecl>? {
val cls = classes[name] ?: return null
cls.members.firstOrNull { it.name == member }?.let { return name to it }
@@ -86,12 +120,12 @@ object DocLookupUtils {
return dfs(className, mutableSetOf())
}
fun findMemberAcrossClasses(importedModules: List<String>, member: String): Pair<String, MiniMemberDecl>? {
val classes = aggregateClasses(importedModules)
fun findMemberAcrossClasses(importedModules: List<String>, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? {
val classes = aggregateClasses(importedModules, localMini)
// Preferred order for ambiguous common ops
val preference = listOf("Iterable", "Iterator", "List")
for (name in preference) {
resolveMemberWithInheritance(importedModules, name, member)?.let { return it }
resolveMemberWithInheritance(importedModules, name, member, localMini)?.let { return it }
}
for ((name, cls) in classes) {
cls.members.firstOrNull { it.name == member }?.let { return name to it }
@@ -104,7 +138,7 @@ object DocLookupUtils {
* We walk left from the dot, find a matching `)` and then the identifier immediately before the `(`.
* If that identifier matches a known class in any of the imported modules, return it.
*/
fun guessClassFromCallBefore(text: String, dotPos: Int, importedModules: List<String>): String? {
fun guessClassFromCallBefore(text: String, dotPos: Int, importedModules: List<String>, localMini: MiniScript? = null): String? {
var i = (dotPos - 1).coerceAtLeast(0)
// Skip spaces
while (i >= 0 && text[i].isWhitespace()) i++
@@ -134,9 +168,32 @@ object DocLookupUtils {
if (start >= end) return null
val name = text.substring(start, end)
// Validate against imported classes
val classes = aggregateClasses(importedModules)
val classes = aggregateClasses(importedModules, localMini)
return if (classes.containsKey(name)) name else null
}
private fun isIdentChar(c: Char): Boolean = c == '_' || c.isLetterOrDigit()
private fun MiniEnumDecl.toSyntheticClass(): MiniClassDecl {
val staticMembers = mutableListOf<MiniMemberDecl>()
// entries: List
staticMembers.add(MiniMemberValDecl(range, "entries", false, null, null, nameStart, isStatic = true))
// valueOf(name: String): Enum
staticMembers.add(MiniMemberFunDecl(range, "valueOf", listOf(MiniParam("name", null, nameStart)), null, null, nameStart, isStatic = true))
// Also add each entry as a static member (const)
for (entry in entries) {
staticMembers.add(MiniMemberValDecl(range, entry, false, MiniTypeName(range, listOf(MiniTypeName.Segment(name, range)), false), null, nameStart, isStatic = true))
}
return MiniClassDecl(
range = range,
name = name,
bases = listOf("Enum"),
bodyRange = null,
doc = doc,
nameStart = nameStart,
members = staticMembers
)
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -137,6 +137,14 @@ data class MiniClassDecl(
val members: List<MiniMemberDecl> = emptyList()
) : MiniDecl
data class MiniEnumDecl(
override val range: MiniRange,
override val name: String,
val entries: List<String>,
override val doc: MiniDoc?,
override val nameStart: Pos
) : MiniDecl
data class MiniCtorField(
val name: String,
val mutable: Boolean,
@@ -206,6 +214,7 @@ interface MiniAstSink {
fun onValDecl(node: MiniValDecl) {}
fun onInitDecl(node: MiniInitDecl) {}
fun onClassDecl(node: MiniClassDecl) {}
fun onEnumDecl(node: MiniEnumDecl) {}
fun onBlock(node: MiniBlock) {}
fun onIdentifier(node: MiniIdentifier) {}
@@ -275,6 +284,12 @@ class MiniAstBuilder : MiniAstSink {
lastDoc = null
}
override fun onEnumDecl(node: MiniEnumDecl) {
val attach = node.copy(doc = node.doc ?: lastDoc)
currentScript?.declarations?.add(attach)
lastDoc = null
}
override fun onBlock(node: MiniBlock) {
blocks.addLast(node)
}