parent
c35d684df1
commit
708b908415
@ -9,6 +9,7 @@
|
|||||||
- Heuristics: handles literals (`"…"` → `String`, numbers → `Int/Real`, `[...]` → `List`, `{...}` → `Dict`) and static `Namespace.` members.
|
- 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.
|
- Performance: capped results, early prefix filtering, per‑document MiniAst cache, cancellation checks.
|
||||||
- Toggle: Settings | Lyng Formatter → "Enable Lyng autocompletion (experimental)" (default ON).
|
- 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
|
- Language: Named arguments and named splats
|
||||||
- New call-site syntax for named arguments using colon: `name: value`.
|
- 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.icons.AllIcons
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.editor.Document
|
import com.intellij.openapi.editor.Document
|
||||||
import com.intellij.openapi.progress.ProgressManager
|
|
||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
import com.intellij.patterns.PlatformPatterns
|
import com.intellij.patterns.PlatformPatterns
|
||||||
import com.intellij.psi.PsiFile
|
import com.intellij.psi.PsiFile
|
||||||
@ -36,7 +35,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
|
|
||||||
private object Provider : CompletionProvider<CompletionParameters>() {
|
private object Provider : CompletionProvider<CompletionParameters>() {
|
||||||
private val log = Logger.getInstance(LyngCompletionContributor::class.java)
|
private val log = Logger.getInstance(LyngCompletionContributor::class.java)
|
||||||
private const val DEBUG_COMPLETION = true
|
private const val DEBUG_COMPLETION = false
|
||||||
|
|
||||||
override fun addCompletions(
|
override fun addCompletions(
|
||||||
parameters: CompletionParameters,
|
parameters: CompletionParameters,
|
||||||
@ -45,6 +44,8 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
) {
|
) {
|
||||||
// Ensure external/bundled docs are registered (e.g., lyng.io.fs with Path)
|
// Ensure external/bundled docs are registered (e.g., lyng.io.fs with Path)
|
||||||
DocsBootstrap.ensure()
|
DocsBootstrap.ensure()
|
||||||
|
// Ensure stdlib Obj*-defined docs (e.g., String methods via ObjString.addFnDoc) are initialized
|
||||||
|
StdlibDocsBootstrap.ensure()
|
||||||
val file: PsiFile = parameters.originalFile
|
val file: PsiFile = parameters.originalFile
|
||||||
if (file.language != LyngLanguage) return
|
if (file.language != LyngLanguage) return
|
||||||
// Feature toggle: allow turning completion off from settings
|
// Feature toggle: allow turning completion off from settings
|
||||||
@ -150,6 +151,60 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
}
|
}
|
||||||
emit(builder)
|
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 in member context and engine items are suspiciously sparse, try to enrich via local inference + offerMembers
|
||||||
if (memberDotPos != null && engineItems.size < 3) {
|
if (memberDotPos != null && engineItems.size < 3) {
|
||||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Engine produced only ${engineItems.size} items in member context — trying enrichment")
|
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>>) {
|
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>) {
|
||||||
val keys = map.keys.sortedBy { it.lowercase() }
|
val keys = map.keys.sortedBy { it.lowercase() }
|
||||||
for (name in keys) {
|
for (name in keys) {
|
||||||
ProgressManager.checkCanceled()
|
|
||||||
val list = map[name] ?: continue
|
val list = map[name] ?: continue
|
||||||
// Choose a representative (prefer method over value for typical UX)
|
// Choose a representative for display:
|
||||||
val rep = list.firstOrNull { it is MiniMemberFunDecl } ?: list.first()
|
// 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) {
|
when (rep) {
|
||||||
is MiniMemberFunDecl -> {
|
is MiniMemberFunDecl -> {
|
||||||
val params = rep.params.joinToString(", ") { it.name }
|
val params = rep.params.joinToString(", ") { it.name }
|
||||||
@ -333,9 +395,13 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
}
|
}
|
||||||
is MiniMemberValDecl -> {
|
is MiniMemberValDecl -> {
|
||||||
val icon = if (rep.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field
|
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)
|
val builder = LookupElementBuilder.create(name)
|
||||||
.withIcon(icon)
|
.withIcon(icon)
|
||||||
.withTypeText(typeOf(rep.type), true)
|
.withTypeText(typeOf((chosen as MiniMemberValDecl).type), true)
|
||||||
emit(builder)
|
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 {
|
run {
|
||||||
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
||||||
val ext = BuiltinDocRegistry.extensionMethodNamesFor(className)
|
val ext = BuiltinDocRegistry.extensionMethodNamesFor(className)
|
||||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Extensions for $className: count=${ext.size} -> ${ext}")
|
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Extensions for $className: count=${ext.size} -> ${ext}")
|
||||||
for (name in ext) {
|
for (name in ext) {
|
||||||
if (already.contains(name)) continue
|
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)
|
val builder = LookupElementBuilder.create(name)
|
||||||
.withIcon(AllIcons.Nodes.Method)
|
.withIcon(AllIcons.Nodes.Method)
|
||||||
.withTailText("()", true)
|
.withTailText("()", true)
|
||||||
@ -558,7 +651,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported)?.let { return it }
|
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported)?.let { return it }
|
||||||
|
|
||||||
// 2) Literal heuristics based on the immediate char before '.'
|
// 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) {
|
if (i >= 0) {
|
||||||
when (text[i]) {
|
when (text[i]) {
|
||||||
'"' -> {
|
'"' -> {
|
||||||
@ -567,6 +660,28 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
}
|
}
|
||||||
']' -> return "List" // very rough heuristic
|
']' -> return "List" // very rough heuristic
|
||||||
'}' -> return "Dict" // map/dictionary literal 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)
|
// Numeric literal: support decimal, hex (0x..), and scientific notation (1e-3)
|
||||||
var j = i
|
var j = i
|
||||||
|
|||||||
@ -43,9 +43,17 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
private val log = Logger.getInstance(LyngDocumentationProvider::class.java)
|
private val log = Logger.getInstance(LyngDocumentationProvider::class.java)
|
||||||
// Toggle to trace inheritance-based resolutions in Quick Docs. Keep false for normal use.
|
// Toggle to trace inheritance-based resolutions in Quick Docs. Keep false for normal use.
|
||||||
private val DEBUG_INHERITANCE = false
|
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? {
|
override fun generateDoc(element: PsiElement?, originalElement: PsiElement?): String? {
|
||||||
// Try load external docs registrars (e.g., lyngio) if present on classpath
|
// Try load external docs registrars (e.g., lyngio) if present on classpath
|
||||||
ensureExternalDocsRegistered()
|
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
|
if (element == null) return null
|
||||||
val file: PsiFile = element.containingFile ?: return null
|
val file: PsiFile = element.containingFile ?: return null
|
||||||
val document: Document = file.viewProvider.document ?: 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
|
// Determine caret/lookup offset from the element range
|
||||||
val offset = originalElement?.textRange?.startOffset ?: element.textRange.startOffset
|
val offset = originalElement?.textRange?.startOffset ?: element.textRange.startOffset
|
||||||
val idRange = TextCtx.wordRangeAt(text, offset) ?: run {
|
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
|
return null
|
||||||
}
|
}
|
||||||
if (idRange.isEmpty) return null
|
if (idRange.isEmpty) return null
|
||||||
val ident = text.substring(idRange.startOffset, idRange.endOffset)
|
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.
|
// Build MiniAst for this file (fast and resilient). Best-effort; on failure continue with registry lookup only.
|
||||||
val sink = MiniAstBuilder()
|
val sink = MiniAstBuilder()
|
||||||
@ -71,7 +79,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
sink.build()
|
sink.build()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
// Do not bail out completely: we still can resolve built-in and imported docs (e.g., println)
|
// 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
|
null
|
||||||
}
|
}
|
||||||
val haveMini = mini != null
|
val haveMini = mini != null
|
||||||
@ -87,7 +95,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val s = source.offsetOf(d.nameStart)
|
val s = source.offsetOf(d.nameStart)
|
||||||
val e = (s + d.name.length).coerceAtMost(text.length)
|
val e = (s + d.name.length).coerceAtMost(text.length)
|
||||||
if (offset in s until e) {
|
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)
|
return renderDeclDoc(d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,12 +105,72 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val s = source.offsetOf(p.nameStart)
|
val s = source.offsetOf(p.nameStart)
|
||||||
val e = (s + p.name.length).coerceAtMost(text.length)
|
val e = (s + p.name.length).coerceAtMost(text.length)
|
||||||
if (offset in s until e) {
|
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)
|
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 {
|
if (haveMini) mini.declarations.firstOrNull { it.name == ident }?.let {
|
||||||
log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}")
|
log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}")
|
||||||
return renderDeclDoc(it)
|
return renderDeclDoc(it)
|
||||||
@ -177,6 +245,32 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
// Search across classes; prefer Iterable, then Iterator, then List for common ops
|
||||||
DocLookupUtils.findMemberAcrossClasses(importedModules, ident)?.let { (owner, member) ->
|
DocLookupUtils.findMemberAcrossClasses(importedModules, ident)?.let { (owner, member) ->
|
||||||
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Cross-class '$ident' resolved to $owner.${member.name}")
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,8 +68,14 @@ object BuiltinDocRegistry : BuiltinDocSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun docsForModule(moduleName: String): List<MiniDecl> {
|
override fun docsForModule(moduleName: String): List<MiniDecl> {
|
||||||
modules[moduleName]?.let { return it }
|
// If module already present but we also have a lazy supplier for it, merge supplier once
|
||||||
// Try lazy 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()
|
val built = lazySuppliers.remove(moduleName)?.invoke()
|
||||||
if (built != null) {
|
if (built != null) {
|
||||||
val list = modules.getOrPut(moduleName) { mutableListOf() }
|
val list = modules.getOrPut(moduleName) { mutableListOf() }
|
||||||
|
|||||||
@ -41,6 +41,8 @@ object CompletionEngineLight {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun completeSuspend(text: String, caret: Int): List<CompletionItem> {
|
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 prefix = prefixAt(text, caret)
|
||||||
val mini = buildMiniAst(text)
|
val mini = buildMiniAst(text)
|
||||||
// Build imported modules as a UNION of MiniAst-derived and textual extraction, always including stdlib
|
// 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>>) {
|
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>) {
|
||||||
for (name in map.keys.sortedBy { it.lowercase() }) {
|
for (name in map.keys.sortedBy { it.lowercase() }) {
|
||||||
val variants = map[name] ?: continue
|
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) {
|
when (rep) {
|
||||||
is MiniMemberFunDecl -> {
|
is MiniMemberFunDecl -> {
|
||||||
val params = rep.params.joinToString(", ") { it.name }
|
val params = rep.params.joinToString(", ") { it.name }
|
||||||
@ -168,7 +176,11 @@ object CompletionEngineLight {
|
|||||||
if (ci.name.startsWith(prefix, true)) out += ci
|
if (ci.name.startsWith(prefix, true)) out += ci
|
||||||
}
|
}
|
||||||
is MiniMemberValDecl -> {
|
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
|
if (ci.name.startsWith(prefix, true)) out += ci
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,18 +189,70 @@ object CompletionEngineLight {
|
|||||||
|
|
||||||
emitGroup(directMap)
|
emitGroup(directMap)
|
||||||
emitGroup(inheritedMap)
|
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) ---
|
// --- Inference helpers (text-only, PSI-free) ---
|
||||||
|
|
||||||
private fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>): String? {
|
private fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>): String? {
|
||||||
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported)?.let { return it }
|
DocLookupUtils.guessClassFromCallBefore(text, dotPos, imported)?.let { return it }
|
||||||
val i = prevNonWs(text, dotPos - 1)
|
var i = prevNonWs(text, dotPos - 1)
|
||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
when (text[i]) {
|
when (text[i]) {
|
||||||
'"' -> return "String"
|
'"' -> return "String"
|
||||||
']' -> return "List"
|
']' -> return "List"
|
||||||
'}' -> return "Dict"
|
'}' -> 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
|
// Numeric literal: decimal/int/hex/scientific
|
||||||
var j = i
|
var j = i
|
||||||
|
|||||||
@ -13,26 +13,63 @@ object DocLookupUtils {
|
|||||||
*/
|
*/
|
||||||
fun canonicalImportedModules(mini: MiniScript): List<String> {
|
fun canonicalImportedModules(mini: MiniScript): List<String> {
|
||||||
val raw = mini.imports.map { it.segments.joinToString(".") { s -> s.name } }
|
val raw = mini.imports.map { it.segments.joinToString(".") { s -> s.name } }
|
||||||
if (raw.isEmpty()) return emptyList()
|
|
||||||
val result = LinkedHashSet<String>()
|
val result = LinkedHashSet<String>()
|
||||||
for (name in raw) {
|
for (name in raw) {
|
||||||
val canon = if (name.startsWith("lyng.")) name else "lyng.$name"
|
val canon = if (name.startsWith("lyng.")) name else "lyng.$name"
|
||||||
result.add(canon)
|
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")
|
result.add("lyng.stdlib")
|
||||||
return result.toList()
|
return result.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aggregateClasses(importedModules: List<String>): Map<String, MiniClassDecl> {
|
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) {
|
for (mod in importedModules) {
|
||||||
val docs = BuiltinDocRegistry.docsForModule(mod)
|
val docs = BuiltinDocRegistry.docsForModule(mod)
|
||||||
docs.filterIsInstance<MiniClassDecl>().forEach { cls ->
|
for (cls in docs.filterIsInstance<MiniClassDecl>()) {
|
||||||
if (!map.containsKey(cls.name)) map[cls.name] = cls
|
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>? {
|
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"))
|
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
|
@Test
|
||||||
fun shebang_and_fs_import_iterator_after_lines() = runBlocking {
|
fun shebang_and_fs_import_iterator_after_lines() = runBlocking {
|
||||||
TestDocsBootstrap.ensure("lyng.stdlib", "lyng.io.fs")
|
TestDocsBootstrap.ensure("lyng.stdlib", "lyng.io.fs")
|
||||||
|
|||||||
@ -192,6 +192,6 @@ fun Exception.printStackTrace() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Compile this string into a regular expression. */
|
/* 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