parent
c35d684df1
commit
708b908415
@ -9,6 +9,7 @@
|
||||
- Heuristics: handles literals (`"…"` → `String`, numbers → `Int/Real`, `[...]` → `List`, `{...}` → `Dict`) and static `Namespace.` members.
|
||||
- Performance: capped results, early prefix filtering, per‑document MiniAst cache, cancellation checks.
|
||||
- Toggle: Settings | Lyng Formatter → "Enable Lyng autocompletion (experimental)" (default ON).
|
||||
- Stabilization: DEBUG completion/Quick Doc logs are OFF by default; behavior aligned between IDE and isolated engine tests.
|
||||
|
||||
- Language: Named arguments and named splats
|
||||
- New call-site syntax for named arguments using colon: `name: value`.
|
||||
|
||||
@ -9,7 +9,6 @@ import com.intellij.codeInsight.lookup.LookupElementBuilder
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.patterns.PlatformPatterns
|
||||
import com.intellij.psi.PsiFile
|
||||
@ -36,7 +35,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
|
||||
private object Provider : CompletionProvider<CompletionParameters>() {
|
||||
private val log = Logger.getInstance(LyngCompletionContributor::class.java)
|
||||
private const val DEBUG_COMPLETION = true
|
||||
private const val DEBUG_COMPLETION = false
|
||||
|
||||
override fun addCompletions(
|
||||
parameters: CompletionParameters,
|
||||
@ -45,6 +44,8 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
) {
|
||||
// Ensure external/bundled docs are registered (e.g., lyng.io.fs with Path)
|
||||
DocsBootstrap.ensure()
|
||||
// Ensure stdlib Obj*-defined docs (e.g., String methods via ObjString.addFnDoc) are initialized
|
||||
StdlibDocsBootstrap.ensure()
|
||||
val file: PsiFile = parameters.originalFile
|
||||
if (file.language != LyngLanguage) return
|
||||
// Feature toggle: allow turning completion off from settings
|
||||
@ -150,6 +151,60 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
}
|
||||
emit(builder)
|
||||
}
|
||||
// In member context, ensure stdlib extension-like methods (e.g., String.re) are present
|
||||
if (memberDotPos != null) {
|
||||
val existing = engineItems.map { it.name }.toMutableSet()
|
||||
val fromText = extractImportsFromText(text)
|
||||
val imported = LinkedHashSet<String>().apply {
|
||||
fromText.forEach { add(it) }
|
||||
add("lyng.stdlib")
|
||||
}.toList()
|
||||
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)
|
||||
if (!inferredClass.isNullOrBlank()) {
|
||||
val ext = BuiltinDocRegistry.extensionMethodNamesFor(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)
|
||||
if (resolved != null) {
|
||||
when (val member = resolved.second) {
|
||||
is MiniMemberFunDecl -> {
|
||||
val params = member.params.joinToString(", ") { it.name }
|
||||
val ret = typeOf(member.returnType)
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(AllIcons.Nodes.Method)
|
||||
.withTailText("(${ '$' }params)", true)
|
||||
.withTypeText(ret, true)
|
||||
.withInsertHandler(ParenInsertHandler)
|
||||
emit(builder)
|
||||
existing.add(name)
|
||||
}
|
||||
is MiniMemberValDecl -> {
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||
.withTypeText(typeOf(member.type), true)
|
||||
emit(builder)
|
||||
existing.add(name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: emit simple method name without detailed types
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(AllIcons.Nodes.Method)
|
||||
.withTailText("()", true)
|
||||
.withInsertHandler(ParenInsertHandler)
|
||||
emit(builder)
|
||||
existing.add(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If in member context and engine items are suspiciously sparse, try to enrich via local inference + offerMembers
|
||||
if (memberDotPos != null && engineItems.size < 3) {
|
||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Engine produced only ${engineItems.size} items in member context — trying enrichment")
|
||||
@ -312,10 +367,17 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>) {
|
||||
val keys = map.keys.sortedBy { it.lowercase() }
|
||||
for (name in keys) {
|
||||
ProgressManager.checkCanceled()
|
||||
val list = map[name] ?: continue
|
||||
// Choose a representative (prefer method over value for typical UX)
|
||||
val rep = list.firstOrNull { it is MiniMemberFunDecl } ?: list.first()
|
||||
// Choose a representative for display:
|
||||
// 1) Prefer a method with a known return type
|
||||
// 2) Else any method
|
||||
// 3) Else the first variant
|
||||
val rep =
|
||||
list.asSequence()
|
||||
.filterIsInstance<MiniMemberFunDecl>()
|
||||
.firstOrNull { it.returnType != null }
|
||||
?: list.firstOrNull { it is MiniMemberFunDecl }
|
||||
?: list.first()
|
||||
when (rep) {
|
||||
is MiniMemberFunDecl -> {
|
||||
val params = rep.params.joinToString(", ") { it.name }
|
||||
@ -333,9 +395,13 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
}
|
||||
is MiniMemberValDecl -> {
|
||||
val icon = if (rep.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field
|
||||
// Prefer a field variant with known type if available
|
||||
val chosen = list.asSequence()
|
||||
.filterIsInstance<MiniMemberValDecl>()
|
||||
.firstOrNull { it.type != null } ?: rep
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(icon)
|
||||
.withTypeText(typeOf(rep.type), true)
|
||||
.withTypeText(typeOf((chosen as MiniMemberValDecl).type), true)
|
||||
emit(builder)
|
||||
}
|
||||
}
|
||||
@ -407,13 +473,40 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
}
|
||||
}
|
||||
|
||||
// Supplement with stdlib extension methods defined in root.lyng (e.g., fun String.trim(...))
|
||||
// Supplement with stdlib extension-like methods defined in root.lyng (e.g., fun String.trim(...))
|
||||
run {
|
||||
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
||||
val ext = BuiltinDocRegistry.extensionMethodNamesFor(className)
|
||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Extensions for $className: count=${ext.size} -> ${ext}")
|
||||
for (name in ext) {
|
||||
if (already.contains(name)) continue
|
||||
// Try to resolve full signature via registry first to get params and return type
|
||||
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 ret = typeOf(member.returnType)
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(AllIcons.Nodes.Method)
|
||||
.withTailText("(${params})", true)
|
||||
.withTypeText(ret, true)
|
||||
.withInsertHandler(ParenInsertHandler)
|
||||
emit(builder)
|
||||
already.add(name)
|
||||
continue
|
||||
}
|
||||
is MiniMemberValDecl -> {
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||
.withTypeText(typeOf(member.type), true)
|
||||
emit(builder)
|
||||
already.add(name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: emit without detailed types if we couldn't resolve
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(AllIcons.Nodes.Method)
|
||||
.withTailText("()", true)
|
||||
@ -558,7 +651,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported)?.let { return it }
|
||||
|
||||
// 2) Literal heuristics based on the immediate char before '.'
|
||||
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
var i = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
if (i >= 0) {
|
||||
when (text[i]) {
|
||||
'"' -> {
|
||||
@ -567,6 +660,28 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
}
|
||||
']' -> return "List" // very rough heuristic
|
||||
'}' -> return "Dict" // map/dictionary literal heuristic
|
||||
')' -> {
|
||||
// Parenthesized expression: walk back to matching '(' and inspect 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: support decimal, hex (0x..), and scientific notation (1e-3)
|
||||
var j = i
|
||||
|
||||
@ -43,9 +43,17 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
private val log = Logger.getInstance(LyngDocumentationProvider::class.java)
|
||||
// Toggle to trace inheritance-based resolutions in Quick Docs. Keep false for normal use.
|
||||
private val DEBUG_INHERITANCE = false
|
||||
// Global Quick Doc debug toggle (OFF by default). When false, [LYNG_DEBUG] logs are suppressed.
|
||||
private val DEBUG_LOG = false
|
||||
override fun generateDoc(element: PsiElement?, originalElement: PsiElement?): String? {
|
||||
// Try load external docs registrars (e.g., lyngio) if present on classpath
|
||||
ensureExternalDocsRegistered()
|
||||
// Ensure stdlib Obj*-defined docs (e.g., String methods via ObjString.addFnDoc) are initialized
|
||||
try {
|
||||
net.sergeych.lyng.miniast.StdlibDocsBootstrap.ensure()
|
||||
} catch (_: Throwable) {
|
||||
// best-effort; absence must not break Quick Doc
|
||||
}
|
||||
if (element == null) return null
|
||||
val file: PsiFile = element.containingFile ?: return null
|
||||
val document: Document = file.viewProvider.document ?: return null
|
||||
@ -54,12 +62,12 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
// Determine caret/lookup offset from the element range
|
||||
val offset = originalElement?.textRange?.startOffset ?: element.textRange.startOffset
|
||||
val idRange = TextCtx.wordRangeAt(text, offset) ?: run {
|
||||
log.info("[LYNG_DEBUG] QuickDoc: no word at offset=$offset in ${file.name}")
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: no word at offset=$offset in ${file.name}")
|
||||
return null
|
||||
}
|
||||
if (idRange.isEmpty) return null
|
||||
val ident = text.substring(idRange.startOffset, idRange.endOffset)
|
||||
log.info("[LYNG_DEBUG] QuickDoc: ident='$ident' at ${idRange.startOffset}..${idRange.endOffset} in ${file.name}")
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: ident='$ident' at ${idRange.startOffset}..${idRange.endOffset} in ${file.name}")
|
||||
|
||||
// Build MiniAst for this file (fast and resilient). Best-effort; on failure continue with registry lookup only.
|
||||
val sink = MiniAstBuilder()
|
||||
@ -71,7 +79,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
sink.build()
|
||||
} catch (t: Throwable) {
|
||||
// Do not bail out completely: we still can resolve built-in and imported docs (e.g., println)
|
||||
log.warn("[LYNG_DEBUG] QuickDoc: compileWithMini failed: ${t.message}")
|
||||
if (DEBUG_LOG) log.warn("[LYNG_DEBUG] QuickDoc: compileWithMini failed: ${t.message}")
|
||||
null
|
||||
}
|
||||
val haveMini = mini != null
|
||||
@ -87,7 +95,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
val s = source.offsetOf(d.nameStart)
|
||||
val e = (s + d.name.length).coerceAtMost(text.length)
|
||||
if (offset in s until e) {
|
||||
log.info("[LYNG_DEBUG] QuickDoc: matched decl '${d.name}' kind=${d::class.simpleName}")
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: matched decl '${d.name}' kind=${d::class.simpleName}")
|
||||
return renderDeclDoc(d)
|
||||
}
|
||||
}
|
||||
@ -97,12 +105,72 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
val s = source.offsetOf(p.nameStart)
|
||||
val e = (s + p.name.length).coerceAtMost(text.length)
|
||||
if (offset in s until e) {
|
||||
log.info("[LYNG_DEBUG] QuickDoc: matched param '${p.name}' in fun '${fn.name}'")
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: matched param '${p.name}' in fun '${fn.name}'")
|
||||
return renderParamDoc(fn, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3) As a fallback, if the caret is on an identifier text that matches any declaration name, show that
|
||||
// 3) Member-context resolution first (dot immediately before identifier): handle literals and calls
|
||||
run {
|
||||
val dotPos = TextCtx.findDotLeft(text, idRange.startOffset)
|
||||
?: TextCtx.findDotLeft(text, offset)
|
||||
if (dotPos != null) {
|
||||
// Build imported modules (MiniAst-derived if available, else lenient from text) and ensure stdlib is present
|
||||
var importedModules = if (haveMini) DocLookupUtils.canonicalImportedModules(mini) else emptyList()
|
||||
if (importedModules.isEmpty()) {
|
||||
val fromText = extractImportsFromText(text)
|
||||
importedModules = if (fromText.isEmpty()) listOf("lyng.stdlib") else fromText
|
||||
}
|
||||
if (!importedModules.contains("lyng.stdlib")) importedModules = importedModules + "lyng.stdlib"
|
||||
|
||||
// Try literal and call-based receiver inference around the dot
|
||||
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
val className: String? = when {
|
||||
i >= 0 && text[i] == '"' -> "String"
|
||||
i >= 0 && text[i] == ']' -> "List"
|
||||
i >= 0 && text[i] == '}' -> "Dict"
|
||||
i >= 0 && text[i] == ')' -> {
|
||||
// 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()
|
||||
when {
|
||||
inner.startsWith('"') && inner.endsWith('"') -> "String"
|
||||
inner.startsWith('[') && inner.endsWith(']') -> "List"
|
||||
inner.startsWith('{') && inner.endsWith('}') -> "Dict"
|
||||
else -> null
|
||||
}
|
||||
} else null
|
||||
} else null
|
||||
}
|
||||
else -> DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules)
|
||||
}
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: memberCtx dotPos=${dotPos} chBeforeDot='${if (dotPos>0) text[dotPos-1] else ' '}' classGuess=${className} imports=${importedModules}")
|
||||
if (className != null) {
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, className, ident)?.let { (owner, member) ->
|
||||
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] QuickDoc: literal/call '$ident' resolved to $owner.${member.name}")
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
}
|
||||
}
|
||||
log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) As a fallback, if the caret is on an identifier text that matches any declaration name, show that
|
||||
if (haveMini) mini.declarations.firstOrNull { it.name == ident }?.let {
|
||||
log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}")
|
||||
return renderDeclDoc(it)
|
||||
@ -177,6 +245,32 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Extra fallback: try a small set of known receiver classes (covers literals when guess failed)
|
||||
run {
|
||||
val candidates = listOf("String", "Iterable", "Iterator", "List", "Collection", "Array", "Dict", "Regex")
|
||||
for (c in candidates) {
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, c, ident)?.let { (owner, member) ->
|
||||
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Candidate '$c.$ident' resolved via inheritance to $owner.${member.name}")
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// As a last resort try aggregated String members (extensions from stdlib text)
|
||||
run {
|
||||
val classes = DocLookupUtils.aggregateClasses(importedModules)
|
||||
val stringCls = classes["String"]
|
||||
val m = stringCls?.members?.firstOrNull { it.name == ident }
|
||||
if (m != null) {
|
||||
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Aggregated fallback resolved String.$ident")
|
||||
return when (m) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc("String", m)
|
||||
is MiniMemberValDecl -> renderMemberValDoc("String", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Search across classes; prefer Iterable, then Iterator, then List for common ops
|
||||
DocLookupUtils.findMemberAcrossClasses(importedModules, ident)?.let { (owner, member) ->
|
||||
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Cross-class '$ident' resolved to $owner.${member.name}")
|
||||
@ -189,7 +283,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[LYNG_DEBUG] QuickDoc: nothing found for ident='$ident'")
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: nothing found for ident='$ident'")
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -79,6 +79,51 @@ class CompletionEngineLightTest {
|
||||
assertFalse(ns.contains("Path"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stringLiteral_re_hasReturnTypeRegex() = runBlocking {
|
||||
TestDocsBootstrap.ensure("lyng.stdlib")
|
||||
val code = """
|
||||
import lyng.stdlib
|
||||
|
||||
val s = "abc".<caret>
|
||||
""".trimIndent()
|
||||
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||
val reItem = items.firstOrNull { it.name == "re" }
|
||||
assertTrue(reItem != null, "Expected to find 're' in String members, got: ${items.map { it.name }}")
|
||||
// Type text should contain ": Regex"
|
||||
assertTrue(reItem!!.typeText?.contains("Regex") == true, "Expected type text to contain 'Regex', was: ${reItem.typeText}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stringLiteral_parenthesized_re_hasReturnTypeRegex() = runBlocking {
|
||||
TestDocsBootstrap.ensure("lyng.stdlib")
|
||||
val code = """
|
||||
// No imports on purpose; stdlib must still be available
|
||||
|
||||
val s = ("abc").<caret>
|
||||
""".trimIndent()
|
||||
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||
val names = items.map { it.name }
|
||||
assertTrue(names.isNotEmpty(), "Expected String members for parenthesized literal, got empty list")
|
||||
val reItem = items.firstOrNull { it.name == "re" }
|
||||
assertTrue(reItem != null, "Expected to find 're' for parenthesized String literal, got: $names")
|
||||
assertTrue(reItem!!.typeText?.contains("Regex") == true, "Expected ': Regex' for re(), was: ${reItem.typeText}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun stringLiteral_noImports_stillHasStringMembers() = runBlocking {
|
||||
TestDocsBootstrap.ensure("lyng.stdlib")
|
||||
val code = """
|
||||
val s = "super".<caret>
|
||||
""".trimIndent()
|
||||
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||
val names = items.map { it.name }
|
||||
assertTrue(names.isNotEmpty(), "Expected String members without explicit imports, got empty list")
|
||||
val reItem = items.firstOrNull { it.name == "re" }
|
||||
assertTrue(reItem != null, "Expected to find 're' without explicit imports, got: $names")
|
||||
assertTrue(reItem!!.typeText?.contains("Regex") == true, "Expected ': Regex' for re() without imports, was: ${reItem.typeText}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shebang_and_fs_import_iterator_after_lines() = runBlocking {
|
||||
TestDocsBootstrap.ensure("lyng.stdlib", "lyng.io.fs")
|
||||
|
||||
@ -192,6 +192,6 @@ fun Exception.printStackTrace() {
|
||||
}
|
||||
|
||||
/* Compile this string into a regular expression. */
|
||||
fun String.re() { Regex(this) }
|
||||
fun String.re(): Regex { Regex(this) }
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user