plugin: autocomplete for enums and members, better support for local symbols; fixed bug in space normalization
This commit is contained in:
parent
22f6c149db
commit
fac58675d5
@ -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.
|
||||
@ -117,6 +117,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
d.name,
|
||||
if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE
|
||||
)
|
||||
is MiniEnumDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,6 +168,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
d.ctorFields.forEach { addTypeSegments(it.type) }
|
||||
d.classFields.forEach { addTypeSegments(it.type) }
|
||||
}
|
||||
is MiniEnumDecl -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
@ -124,10 +124,10 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported)
|
||||
?: guessReceiverClassViaMini(mini, text, memberDotPos, imported)
|
||||
?:
|
||||
guessReturnClassFromMemberCallBefore(text, memberDotPos, imported)
|
||||
?: guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported)
|
||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported)
|
||||
?: guessReceiverClass(text, memberDotPos, imported)
|
||||
guessReturnClassFromMemberCallBefore(text, memberDotPos, imported, mini)
|
||||
?: guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported, mini)
|
||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
||||
?: guessReceiverClass(text, memberDotPos, imported, mini)
|
||||
|
||||
if (inferred != null) {
|
||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Fallback inferred receiver/return class='$inferred' — offering its members")
|
||||
@ -159,6 +159,8 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
.withInsertHandler(ParenInsertHandler)
|
||||
Kind.Class_ -> LookupElementBuilder.create(ci.name)
|
||||
.withIcon(AllIcons.Nodes.Class)
|
||||
Kind.Enum -> LookupElementBuilder.create(ci.name)
|
||||
.withIcon(AllIcons.Nodes.Enum)
|
||||
Kind.Value -> LookupElementBuilder.create(ci.name)
|
||||
.withIcon(AllIcons.Nodes.Field)
|
||||
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
|
||||
@ -179,16 +181,16 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
val inferredClass =
|
||||
guessReturnClassFromMemberCallBeforeMini(mini, text, memberDotPos, imported)
|
||||
?: guessReceiverClassViaMini(mini, text, memberDotPos, imported)
|
||||
?: guessReturnClassFromMemberCallBefore(text, memberDotPos, imported)
|
||||
?: guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported)
|
||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported)
|
||||
?: guessReceiverClass(text, memberDotPos, imported)
|
||||
?: guessReturnClassFromMemberCallBefore(text, memberDotPos, imported, mini)
|
||||
?: guessReturnClassFromTopLevelCallBefore(text, memberDotPos, imported, mini)
|
||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
||||
?: guessReceiverClass(text, memberDotPos, imported, mini)
|
||||
if (!inferredClass.isNullOrBlank()) {
|
||||
val ext = BuiltinDocRegistry.extensionMemberNamesFor(inferredClass)
|
||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${'$'}{ext}")
|
||||
for (name in ext) {
|
||||
if (existing.contains(name)) continue
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name)
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name, mini)
|
||||
if (resolved != null) {
|
||||
when (val member = resolved.second) {
|
||||
is MiniMemberFunDecl -> {
|
||||
@ -267,7 +269,8 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
.withIcon(kindIcon)
|
||||
.withTypeText(typeOf(d.type), true)
|
||||
}
|
||||
else -> LookupElementBuilder.create(name)
|
||||
is MiniEnumDecl -> LookupElementBuilder.create(name)
|
||||
.withIcon(AllIcons.Nodes.Enum)
|
||||
}
|
||||
emit(builder)
|
||||
}
|
||||
@ -294,9 +297,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
, sourceText: String,
|
||||
mini: MiniScript? = null
|
||||
) {
|
||||
// Ensure modules are seeded in the registry (triggers lazy stdlib build too)
|
||||
for (m in imported) BuiltinDocRegistry.docsForModule(m)
|
||||
val classes = DocLookupUtils.aggregateClasses(imported)
|
||||
val classes = DocLookupUtils.aggregateClasses(imported, mini)
|
||||
if (DEBUG_COMPLETION) {
|
||||
val keys = classes.keys.joinToString(", ")
|
||||
log.info("[LYNG_DEBUG] offerMembers: imported=${imported} classes=[${keys}] target=${className}")
|
||||
@ -553,32 +554,51 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
|
||||
private fun guessReceiverClassViaMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>): String? {
|
||||
if (mini == null) return null
|
||||
val ident = previousIdentifierBeforeDot(text, dotPos) ?: return null
|
||||
// 1) Local val/var in the file
|
||||
val valDecl = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }
|
||||
val typeFromVal = valDecl?.type?.let { simpleClassNameOf(it) }
|
||||
if (!typeFromVal.isNullOrBlank()) return typeFromVal
|
||||
// If initializer exists, try to sniff ClassName(
|
||||
val initR = valDecl?.initRange
|
||||
if (initR != null) {
|
||||
val src = mini.range.start.source
|
||||
val s = src.offsetOf(initR.start)
|
||||
val e = src.offsetOf(initR.end).coerceAtMost(text.length)
|
||||
if (s in 0..e && e <= text.length) {
|
||||
val init = text.substring(s, e)
|
||||
Regex("([A-Za-z_][A-Za-z0-9_]*)\\s*\\(").find(init)?.let { m ->
|
||||
val cls = m.groupValues[1]
|
||||
return cls
|
||||
}
|
||||
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
if (i < 0) return null
|
||||
val wordRange = TextCtx.wordRangeAt(text, i + 1) ?: return null
|
||||
val ident = text.substring(wordRange.startOffset, wordRange.endOffset)
|
||||
|
||||
// 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) Parameters in any function (best-effort without scope mapping)
|
||||
val paramType = mini.declarations.filterIsInstance<MiniFunDecl>()
|
||||
.asSequence()
|
||||
.flatMap { it.params.asSequence() }
|
||||
.firstOrNull { it.name == ident }?.type
|
||||
val typeFromParam = simpleClassNameOf(paramType)
|
||||
if (!typeFromParam.isNullOrBlank()) return typeFromParam
|
||||
simpleClassNameOf(paramType)?.let { return it }
|
||||
|
||||
// 3) Recursive chaining: Base.ident.
|
||||
val dotBefore = TextCtx.findDotLeft(text, wordRange.startOffset)
|
||||
if (dotBefore != null) {
|
||||
val receiverClass = guessReceiverClassViaMini(mini, text, dotBefore, imported)
|
||||
?: guessReceiverClass(text, dotBefore, imported)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Check if it's a known class (static access)
|
||||
val classes = DocLookupUtils.aggregateClasses(imported, mini)
|
||||
if (classes.containsKey(ident)) return ident
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@ -631,7 +651,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
}
|
||||
}
|
||||
// Else fallback to registry-based resolution (covers imported classes)
|
||||
return DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, callee)?.second?.let { m ->
|
||||
return DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, callee, mini)?.second?.let { m ->
|
||||
val rt = when (m) {
|
||||
is MiniMemberFunDecl -> m.returnType
|
||||
is MiniMemberValDecl -> m.type
|
||||
@ -651,7 +671,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
val body = text.substring(start, end)
|
||||
val map = LinkedHashMap<String, ScannedSig>()
|
||||
// fun name(params): Type
|
||||
val funRe = Regex("(?m)^\\s*fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(([^)]*)\\)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?")
|
||||
val funRe = Regex("^\\s*fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(([^)]*)\\)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?", RegexOption.MULTILINE)
|
||||
for (m in funRe.findAll(body)) {
|
||||
val name = m.groupValues.getOrNull(1) ?: continue
|
||||
val params = m.groupValues.getOrNull(2)?.split(',')?.mapNotNull { it.trim().takeIf { it.isNotEmpty() } } ?: emptyList()
|
||||
@ -659,7 +679,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
map[name] = ScannedSig("fun", params, type)
|
||||
}
|
||||
// val/var name: Type
|
||||
val valRe = Regex("(?m)^\\s*(val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?")
|
||||
val valRe = Regex("^\\s*(val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?", RegexOption.MULTILINE)
|
||||
for (m in valRe.findAll(body)) {
|
||||
val kind = m.groupValues.getOrNull(1) ?: continue
|
||||
val name = m.groupValues.getOrNull(2) ?: continue
|
||||
@ -669,9 +689,9 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
return map
|
||||
}
|
||||
|
||||
private fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>): String? {
|
||||
private fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
|
||||
// 1) Try call-based: ClassName(...).
|
||||
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported)?.let { return it }
|
||||
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported, mini)?.let { return it }
|
||||
|
||||
// 2) Literal heuristics based on the immediate char before '.'
|
||||
var i = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
@ -706,6 +726,32 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it's an identifier, check if it's a known class (static access) or chain
|
||||
val wordRange = TextCtx.wordRangeAt(text, i + 1)
|
||||
if (wordRange != null) {
|
||||
val ident = text.substring(wordRange.startOffset, wordRange.endOffset)
|
||||
val classes = DocLookupUtils.aggregateClasses(imported, mini)
|
||||
if (classes.containsKey(ident)) return ident
|
||||
|
||||
// Chaining without MiniAst
|
||||
val dotBefore = TextCtx.findDotLeft(text, wordRange.startOffset)
|
||||
if (dotBefore != null) {
|
||||
val base = guessReceiverClass(text, dotBefore, imported, mini)
|
||||
if (base != null) {
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, base, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Numeric literal: support decimal, hex (0x..), and scientific notation (1e-3)
|
||||
var j = i
|
||||
var hasDigits = false
|
||||
@ -764,7 +810,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
* Try to infer the class of the return value of the member call immediately before the dot.
|
||||
* Example: `Path(".." ).lines().<caret>` → detects `lines()` on receiver class `Path` and returns `Iterator`.
|
||||
*/
|
||||
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 = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
if (i < 0) return null
|
||||
// We expect a call just before the dot, i.e., ')' ... '.'
|
||||
@ -795,9 +841,9 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
if (k < 0 || text[k] != '.') return null
|
||||
val prevDot = k
|
||||
// Infer receiver class at the previous dot
|
||||
val receiverClass = guessReceiverClass(text, prevDot, imported) ?: return null
|
||||
val receiverClass = guessReceiverClass(text, prevDot, imported, mini) ?: return null
|
||||
// Resolve the callee as a member of receiver class, including inheritance
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, callee) ?: return null
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, callee, mini) ?: return null
|
||||
val member = resolved.second
|
||||
val returnType = when (member) {
|
||||
is MiniMemberFunDecl -> member.returnType
|
||||
@ -811,7 +857,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
* Infer return class of a top-level call right before the dot: e.g., `files().<caret>`.
|
||||
* We extract callee name and resolve it among imported modules' top-level functions.
|
||||
*/
|
||||
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 = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
if (i < 0 || text[i] != ')') return null
|
||||
// Walk back to matching '('
|
||||
@ -845,6 +891,10 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
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
|
||||
}
|
||||
|
||||
@ -853,7 +903,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
* derive its return type using cross-class lookup (Iterable/Iterator/List preference). This ignores the receiver.
|
||||
* Example: `something.lines().<caret>` where `something` type is unknown, but `lines()` commonly returns Iterator<String>.
|
||||
*/
|
||||
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 = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
if (i < 0 || text[i] != ')') return null
|
||||
// Walk back to matching '('
|
||||
@ -877,7 +927,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
if (start >= end) return null
|
||||
val callee = text.substring(start, end)
|
||||
// Try cross-class resolution
|
||||
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, callee) ?: return null
|
||||
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, callee, mini) ?: return null
|
||||
val member = resolved.second
|
||||
val returnType = when (member) {
|
||||
is MiniMemberFunDecl -> member.returnType
|
||||
@ -949,7 +999,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
// Lenient textual import extractor (duplicated from QuickDoc privately)
|
||||
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()) {
|
||||
|
||||
@ -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.
|
||||
@ -110,7 +110,66 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3) Member-context resolution first (dot immediately before identifier): handle literals and calls
|
||||
// 3) usages in current file via Binder (resolves local variables, parameters, and classes)
|
||||
if (haveMini) {
|
||||
try {
|
||||
val binding = net.sergeych.lyng.binding.Binder.bind(text, mini)
|
||||
val ref = binding.references.firstOrNull { offset in it.start until it.end }
|
||||
if (ref != null) {
|
||||
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
|
||||
if (sym != null) {
|
||||
// Find local declaration that matches this symbol
|
||||
val ds = mini.declarations.firstOrNull { decl ->
|
||||
val s = source.offsetOf(decl.nameStart)
|
||||
decl.name == sym.name && s == sym.declStart
|
||||
}
|
||||
if (ds != null) return renderDeclDoc(ds)
|
||||
|
||||
// Check parameters
|
||||
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||
for (p in fn.params) {
|
||||
val s = source.offsetOf(p.nameStart)
|
||||
if (p.name == sym.name && s == sym.declStart) {
|
||||
return renderParamDoc(fn, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check class members (fields/functions)
|
||||
for (cls in mini.declarations.filterIsInstance<MiniClassDecl>()) {
|
||||
for (m in cls.members) {
|
||||
val s = source.offsetOf(m.nameStart)
|
||||
if (m.name == sym.name && s == sym.declStart) {
|
||||
return when (m) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(cls.name, m)
|
||||
is MiniInitDecl -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
for (cf in cls.ctorFields) {
|
||||
val s = source.offsetOf(cf.nameStart)
|
||||
if (cf.name == sym.name && s == sym.declStart) {
|
||||
// Render as a member val
|
||||
val mv = MiniMemberValDecl(
|
||||
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
return renderMemberValDoc(cls.name, mv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (DEBUG_LOG) log.warn("[LYNG_DEBUG] QuickDoc: local binder resolution failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
// 4) Member-context resolution first (dot immediately before identifier): handle literals and calls
|
||||
run {
|
||||
val dotPos = TextCtx.findDotLeft(text, idRange.startOffset)
|
||||
?: TextCtx.findDotLeft(text, offset)
|
||||
@ -225,8 +284,9 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
// Also allow values/consts
|
||||
docs.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
||||
// And classes
|
||||
// And classes/enums
|
||||
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
||||
docs.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
||||
}
|
||||
// Defensive fallback: if nothing found and it's a well-known stdlib function, render minimal inline docs
|
||||
if (ident == "println" || ident == "print") {
|
||||
@ -320,7 +380,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
*/
|
||||
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()) {
|
||||
@ -373,8 +433,8 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
val title = when (d) {
|
||||
is MiniFunDecl -> "function ${d.name}${signatureOf(d)}"
|
||||
is MiniClassDecl -> "class ${d.name}"
|
||||
is MiniEnumDecl -> "enum ${d.name} { ${d.entries.joinToString(", ")} }"
|
||||
is MiniValDecl -> if (d.mutable) "var ${d.name}${typeOf(d.type)}" else "val ${d.name}${typeOf(d.type)}"
|
||||
else -> d.name
|
||||
}
|
||||
// Show full detailed documentation, not just the summary
|
||||
val raw = d.doc?.raw
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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.
|
||||
@ -131,4 +131,78 @@ class MiniAstTest {
|
||||
// Bases captured as plain names for now
|
||||
assertEquals(listOf("Base1", "Base2"), cd.bases)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun miniAst_captures_enum_entries_and_doc() = runTest {
|
||||
val code = """
|
||||
/** Enum E docs */
|
||||
enum E {
|
||||
A,
|
||||
B,
|
||||
C
|
||||
}
|
||||
"""
|
||||
val (_, sink) = compileWithMini(code)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini)
|
||||
val ed = mini!!.declarations.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == "E" }
|
||||
assertNotNull(ed)
|
||||
assertNotNull(ed.doc)
|
||||
assertTrue(ed.doc!!.raw.contains("Enum E docs"))
|
||||
assertEquals(listOf("A", "B", "C"), ed.entries)
|
||||
assertEquals("E", ed.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun enum_to_synthetic_class_members() = runTest {
|
||||
val code = """
|
||||
enum MyEnum { V1, V2 }
|
||||
"""
|
||||
val (_, sink) = compileWithMini(code)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini)
|
||||
|
||||
// I'll check via aggregateClasses by mocking the registry or just checking it includes Enum base.
|
||||
val stdlib = BuiltinDocRegistry.docsForModule("lyng.stdlib")
|
||||
val enumBase = stdlib.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "Enum" }
|
||||
assertNotNull(enumBase, "Enum base class should be in stdlib")
|
||||
assertTrue(enumBase.members.any { it.name == "name" })
|
||||
assertTrue(enumBase.members.any { it.name == "ordinal" })
|
||||
|
||||
// Check if aggregateClasses handles enums from local MiniScript
|
||||
val classes = DocLookupUtils.aggregateClasses(listOf("lyng.stdlib"), mini)
|
||||
val myEnum = classes["MyEnum"]
|
||||
assertNotNull(myEnum, "Local enum should be aggregated as a class")
|
||||
assertEquals(listOf("Enum"), myEnum.bases)
|
||||
assertTrue(myEnum.members.any { it.name == "entries" }, "Should have entries")
|
||||
assertTrue(myEnum.members.any { it.name == "valueOf" }, "Should have valueOf")
|
||||
assertTrue(myEnum.members.any { it.name == "V1" }, "Should have V1")
|
||||
assertTrue(myEnum.members.any { it.name == "V2" }, "Should have V2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun complete_enum_members() = runTest {
|
||||
val code = """
|
||||
enum MyEnum { V1, V2 }
|
||||
val x = MyEnum.V1.<caret>
|
||||
"""
|
||||
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||
val names = items.map { it.name }.toSet()
|
||||
assertTrue(names.contains("name"), "Should contain name from Enum base")
|
||||
assertTrue(names.contains("ordinal"), "Should contain ordinal from Enum base")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun complete_enum_class_members() = runTest {
|
||||
val code = """
|
||||
enum MyEnum { V1, V2 }
|
||||
val x = MyEnum.<caret>
|
||||
"""
|
||||
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||
val names = items.map { it.name }.toSet()
|
||||
assertTrue(names.contains("entries"), "Should contain entries")
|
||||
assertTrue(names.contains("valueOf"), "Should contain valueOf")
|
||||
assertTrue(names.contains("V1"), "Should contain V1")
|
||||
assertTrue(names.contains("V2"), "Should contain V2")
|
||||
}
|
||||
}
|
||||
|
||||
@ -676,4 +676,58 @@ class LyngFormatterTest {
|
||||
// Idempotent
|
||||
assertEquals(expected, LyngFormatter.reindent(out, cfg))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stringLiterals_areNotNormalized() {
|
||||
val src = """
|
||||
val s = "a=b"
|
||||
val s2 = "a + b"
|
||||
val s3 = "a - b"
|
||||
val s4 = "a,b"
|
||||
val s5 = "if(x){}"
|
||||
""".trimIndent()
|
||||
|
||||
val cfg = LyngFormatConfig(applySpacing = true)
|
||||
val formatted = LyngFormatter.format(src, cfg)
|
||||
|
||||
assertEquals(src, formatted, "String literals should not be changed by space normalization")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mixedCodeAndStrings_normalization() {
|
||||
val src = "val x=1+\"a+b\"+2"
|
||||
val expected = "val x = 1 + \"a+b\" + 2"
|
||||
|
||||
val cfg = LyngFormatConfig(applySpacing = true)
|
||||
val formatted = LyngFormatter.format(src, cfg)
|
||||
|
||||
assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reindent_ignoresBracesInStrings() {
|
||||
val src = """
|
||||
fun test() {
|
||||
val s = "{"
|
||||
val s2 = "}"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val cfg = LyngFormatConfig(indentSize = 4)
|
||||
val formatted = LyngFormatter.reindent(src, cfg)
|
||||
|
||||
// If it fails, s2 will be indented differently
|
||||
assertEquals(src, formatted, "Braces in strings should not affect indentation")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun blockComments_spacing_robustness() {
|
||||
val src = "val x=1/*a=b*/+2"
|
||||
val expected = "val x = 1/*a=b*/ + 2"
|
||||
|
||||
val cfg = LyngFormatConfig(applySpacing = true)
|
||||
val formatted = LyngFormatter.format(src, cfg)
|
||||
|
||||
assertEquals(expected, formatted)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user