1 parent
c35d684df1
commit
708b908415
9 files changed
+415
-27
No files matched your search
@@ -68,8 +68,14 @@ object BuiltinDocRegistry : BuiltinDocSource {
|
||||
}
|
||||
|
||||
override fun docsForModule(moduleName: String): List<MiniDecl> {
|
||||
modules[moduleName]?.let { return it }
|
||||
// Try lazy supplier once
|
||||
// If module already present but we also have a lazy supplier for it, merge supplier once
|
||||
modules[moduleName]?.let { existing ->
|
||||
lazySuppliers.remove(moduleName)?.invoke()?.let { built ->
|
||||
existing += built
|
||||
}
|
||||
return existing
|
||||
}
|
||||
// Try lazy supplier once when module is not present
|
||||
val built = lazySuppliers.remove(moduleName)?.invoke()
|
||||
if (built != null) {
|
||||
val list = modules.getOrPut(moduleName) { mutableListOf() }
|
||||
|
||||
@@ -41,6 +41,8 @@ object CompletionEngineLight {
|
||||
}
|
||||
|
||||
suspend fun completeSuspend(text: String, caret: Int): List<CompletionItem> {
|
||||
// Ensure stdlib Obj*-defined docs (e.g., String methods) are initialized before registry lookup
|
||||
StdlibDocsBootstrap.ensure()
|
||||
val prefix = prefixAt(text, caret)
|
||||
val mini = buildMiniAst(text)
|
||||
// Build imported modules as a UNION of MiniAst-derived and textual extraction, always including stdlib
|
||||
@@ -158,7 +160,13 @@ object CompletionEngineLight {
|
||||
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>) {
|
||||
for (name in map.keys.sortedBy { it.lowercase() }) {
|
||||
val variants = map[name] ?: continue
|
||||
val rep = variants.firstOrNull { it is MiniMemberFunDecl } ?: variants.first()
|
||||
// Prefer a method with a known return type; else any method; else first variant
|
||||
val rep =
|
||||
variants.asSequence()
|
||||
.filterIsInstance<MiniMemberFunDecl>()
|
||||
.firstOrNull { it.returnType != null }
|
||||
?: variants.firstOrNull { it is MiniMemberFunDecl }
|
||||
?: variants.first()
|
||||
when (rep) {
|
||||
is MiniMemberFunDecl -> {
|
||||
val params = rep.params.joinToString(", ") { it.name }
|
||||
@@ -168,7 +176,11 @@ object CompletionEngineLight {
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
}
|
||||
is MiniMemberValDecl -> {
|
||||
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(rep.type))
|
||||
// Prefer a field variant with known type if available
|
||||
val chosen = variants.asSequence()
|
||||
.filterIsInstance<MiniMemberValDecl>()
|
||||
.firstOrNull { it.type != null } ?: rep
|
||||
val ci = CompletionItem(name, Kind.Field, typeText = typeOf((chosen as MiniMemberValDecl).type))
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
}
|
||||
}
|
||||
@@ -177,18 +189,70 @@ object CompletionEngineLight {
|
||||
|
||||
emitGroup(directMap)
|
||||
emitGroup(inheritedMap)
|
||||
|
||||
// Supplement with stdlib extension-like methods defined in root.lyng (e.g., fun String.re(...))
|
||||
run {
|
||||
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
||||
val ext = BuiltinDocRegistry.extensionMethodNamesFor(className)
|
||||
for (name in ext) {
|
||||
if (already.contains(name)) continue
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name)
|
||||
if (resolved != null) {
|
||||
when (val member = resolved.second) {
|
||||
is MiniMemberFunDecl -> {
|
||||
val params = member.params.joinToString(", ") { it.name }
|
||||
val ci = CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(member.returnType))
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
already.add(name)
|
||||
}
|
||||
is MiniMemberValDecl -> {
|
||||
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(member.type))
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
already.add(name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: emit simple method name without detailed types
|
||||
val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null)
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
already.add(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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 }
|
||||
val i = prevNonWs(text, dotPos - 1)
|
||||
var i = prevNonWs(text, dotPos - 1)
|
||||
if (i >= 0) {
|
||||
when (text[i]) {
|
||||
'"' -> return "String"
|
||||
']' -> return "List"
|
||||
'}' -> return "Dict"
|
||||
')' -> {
|
||||
// Parenthesized expression: walk back to matching '(' and inspect the inner expression
|
||||
var j = i - 1
|
||||
var depth = 0
|
||||
while (j >= 0) {
|
||||
when (text[j]) {
|
||||
')' -> depth++
|
||||
'(' -> if (depth == 0) break else depth--
|
||||
}
|
||||
j--
|
||||
}
|
||||
if (j >= 0 && text[j] == '(') {
|
||||
val innerS = (j + 1).coerceAtLeast(0)
|
||||
val innerE = i.coerceAtMost(text.length)
|
||||
if (innerS < innerE) {
|
||||
val inner = text.substring(innerS, innerE).trim()
|
||||
if (inner.startsWith('"') && inner.endsWith('"')) return "String"
|
||||
if (inner.startsWith('[') && inner.endsWith(']')) return "List"
|
||||
if (inner.startsWith('{') && inner.endsWith('}')) return "Dict"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Numeric literal: decimal/int/hex/scientific
|
||||
var j = i
|
||||
|
||||
@@ -13,26 +13,63 @@ object DocLookupUtils {
|
||||
*/
|
||||
fun canonicalImportedModules(mini: MiniScript): List<String> {
|
||||
val raw = mini.imports.map { it.segments.joinToString(".") { s -> s.name } }
|
||||
if (raw.isEmpty()) return emptyList()
|
||||
val result = LinkedHashSet<String>()
|
||||
for (name in raw) {
|
||||
val canon = if (name.startsWith("lyng.")) name else "lyng.$name"
|
||||
result.add(canon)
|
||||
}
|
||||
// Always make stdlib available as a fallback context for common types
|
||||
// Always make stdlib available as a fallback context for common types,
|
||||
// even when there are no explicit imports in the file
|
||||
result.add("lyng.stdlib")
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
fun aggregateClasses(importedModules: List<String>): Map<String, MiniClassDecl> {
|
||||
val map = LinkedHashMap<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) {
|
||||
val docs = BuiltinDocRegistry.docsForModule(mod)
|
||||
docs.filterIsInstance<MiniClassDecl>().forEach { cls ->
|
||||
if (!map.containsKey(cls.name)) map[cls.name] = cls
|
||||
for (cls in docs.filterIsInstance<MiniClassDecl>()) {
|
||||
buckets.getOrPut(cls.name) { mutableListOf() }.add(cls)
|
||||
}
|
||||
}
|
||||
return map
|
||||
|
||||
fun mergeClassDecls(name: String, list: List<MiniClassDecl>): MiniClassDecl {
|
||||
if (list.isEmpty()) throw IllegalArgumentException("empty class list for $name")
|
||||
if (list.size == 1) return list.first()
|
||||
// Choose a representative for non-merge fields (range/nameStart/bodyRange): take the first
|
||||
val rep = list.first()
|
||||
val bases = LinkedHashSet<String>()
|
||||
val members = LinkedHashMap<String, MutableList<MiniMemberDecl>>()
|
||||
var doc: MiniDoc? = null
|
||||
for (c in list) {
|
||||
bases.addAll(c.bases)
|
||||
if (doc == null && c.doc != null && c.doc.raw.isNotBlank()) doc = c.doc
|
||||
for (m in c.members) {
|
||||
// Group by name to keep overloads together
|
||||
members.getOrPut(m.name) { mutableListOf() }.add(m)
|
||||
}
|
||||
}
|
||||
// Flatten grouped members back to a list; keep stable name order
|
||||
val mergedMembers = members.keys.sortedBy { it.lowercase() }.flatMap { members[it] ?: emptyList() }
|
||||
return MiniClassDecl(
|
||||
range = rep.range,
|
||||
name = rep.name,
|
||||
bases = bases.toList(),
|
||||
bodyRange = rep.bodyRange,
|
||||
ctorFields = rep.ctorFields,
|
||||
classFields = rep.classFields,
|
||||
doc = doc,
|
||||
nameStart = rep.nameStart,
|
||||
members = mergedMembers
|
||||
)
|
||||
}
|
||||
|
||||
val result = LinkedHashMap<String, MiniClassDecl>()
|
||||
for ((name, list) in buckets) {
|
||||
result[name] = mergeClassDecls(name, list)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String): Pair<String, MiniMemberDecl>? {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Ensure stdlib Obj*-defined docs (like String methods added via ObjString.addFnDoc)
|
||||
* are initialized before registry lookups for completion/quick docs.
|
||||
*/
|
||||
package net.sergeych.lyng.miniast
|
||||
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
|
||||
object StdlibDocsBootstrap {
|
||||
// Simple idempotent guard; races are harmless as initializer side-effects are idempotent
|
||||
private var ensured = false
|
||||
|
||||
fun ensure() {
|
||||
if (ensured) return
|
||||
try {
|
||||
// Touch core Obj* types whose docs are registered via addFnDoc/addConstDoc
|
||||
// Accessing .type forces their static initializers to run and register docs.
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val _string = ObjString.type
|
||||
} catch (_: Throwable) {
|
||||
// Best-effort; absence should not break consumers
|
||||
} finally {
|
||||
ensured = true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user