fix #83 import-aware quickdocs
This commit is contained in:
parent
40f11b6f29
commit
bfffea7e69
@ -20,3 +20,12 @@ plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||
alias(libs.plugins.vanniktech.mavenPublish) apply false
|
||||
}
|
||||
|
||||
// Convenience alias to run the IntelliJ IDE with the Lyng plugin from the project root.
|
||||
// Usage: ./gradlew runIde
|
||||
// It simply delegates to :lyng-idea:runIde provided by the Gradle IntelliJ Plugin.
|
||||
tasks.register<org.gradle.api.DefaultTask>("runIde") {
|
||||
group = "intellij"
|
||||
description = "Run IntelliJ IDEA with the Lyng plugin (:lyng-idea)"
|
||||
dependsOn(":lyng-idea:runIde")
|
||||
}
|
||||
|
||||
@ -7,6 +7,11 @@ val files = Path("../..").list().toList()
|
||||
// most long is longest?
|
||||
val longestNameLength = files.maxOf { it.name.length }
|
||||
|
||||
// testdoc
|
||||
fun test() {
|
||||
22
|
||||
}
|
||||
|
||||
|
||||
val format = "%"+(longestNameLength+1) +"s %d"
|
||||
for( f in files )
|
||||
|
||||
@ -37,6 +37,8 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lynglib"))
|
||||
// Include lyngio so Quick Docs can reflectively load fs docs registrar (FsBuiltinDocs)
|
||||
implementation(project(":lyngio"))
|
||||
// Rich Markdown renderer for Quick Docs
|
||||
implementation("com.vladsch.flexmark:flexmark-all:0.64.8")
|
||||
}
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Minimal fallback docs seeding for `lyng.io.fs` used only inside the IDEA plugin
|
||||
* when external docs module (lyngio) is not present on the classpath.
|
||||
*
|
||||
* We keep it tiny and plugin-local to avoid coupling core library to external packages.
|
||||
*/
|
||||
package net.sergeych.lyng.idea.docs
|
||||
|
||||
import net.sergeych.lyng.miniast.BuiltinDocRegistry
|
||||
import net.sergeych.lyng.miniast.ParamDoc
|
||||
import net.sergeych.lyng.miniast.TypeGenericDoc
|
||||
import net.sergeych.lyng.miniast.type
|
||||
|
||||
internal object FsDocsFallback {
|
||||
@Volatile
|
||||
private var seeded = false
|
||||
|
||||
fun ensureOnce(): Boolean {
|
||||
if (seeded) return true
|
||||
synchronized(this) {
|
||||
if (seeded) return true
|
||||
BuiltinDocRegistry.module("lyng.io.fs") {
|
||||
// Class Path summary and a few commonly used methods
|
||||
classDoc(name = "Path", doc = "Filesystem path class. Construct with a string: `Path(\"/tmp\")`.") {
|
||||
method(name = "exists", doc = "Whether the path exists on the filesystem.", returns = type("lyng.Bool"))
|
||||
method(name = "isFile", doc = "Whether the path exists and is a file.", returns = type("lyng.Bool"))
|
||||
method(name = "isDir", doc = "Whether the path exists and is a directory.", returns = type("lyng.Bool"))
|
||||
method(name = "readUtf8", doc = "Read the entire file as UTF-8 string.", returns = type("lyng.String"))
|
||||
method(
|
||||
name = "writeUtf8",
|
||||
doc = "Write UTF-8 string to the file (overwrite).",
|
||||
params = listOf(ParamDoc("text", type("lyng.String")))
|
||||
)
|
||||
method(
|
||||
name = "bytes",
|
||||
doc = "Iterate file content as `Buffer` chunks.",
|
||||
params = listOf(ParamDoc("size", type("lyng.Int"))),
|
||||
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Buffer")))
|
||||
)
|
||||
method(
|
||||
name = "lines",
|
||||
doc = "Iterate file as lines of text.",
|
||||
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String")))
|
||||
)
|
||||
}
|
||||
|
||||
// Top-level exported constants
|
||||
valDoc(name = "Path", doc = "Filesystem path class. Construct with a string: `Path(\"/tmp\")`.", type = type("Path"))
|
||||
valDoc(name = "Paths", doc = "Alias of `Path` for those who prefer plural form.", type = type("Path"))
|
||||
}
|
||||
seeded = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,7 @@ import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
import net.sergeych.lyng.idea.LyngLanguage
|
||||
@ -42,6 +43,8 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
// Toggle to trace inheritance-based resolutions in Quick Docs. Keep false for normal use.
|
||||
private val DEBUG_INHERITANCE = false
|
||||
override fun generateDoc(element: PsiElement?, originalElement: PsiElement?): String? {
|
||||
// Try load external docs registrars (e.g., lyngio) if present on classpath
|
||||
ensureExternalDocsRegistered()
|
||||
if (element == null) return null
|
||||
val file: PsiFile = element.containingFile ?: return null
|
||||
val document: Document = file.viewProvider.document ?: return null
|
||||
@ -57,23 +60,29 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
val ident = text.substring(idRange.startOffset, idRange.endOffset)
|
||||
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 return null.
|
||||
// Build MiniAst for this file (fast and resilient). Best-effort; on failure continue with registry lookup only.
|
||||
val sink = MiniAstBuilder()
|
||||
// Use lenient import provider so unresolved imports (e.g., lyng.io.fs) don't break docs
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
try {
|
||||
val src = Source("<ide>", text)
|
||||
val src = Source("<ide>", text)
|
||||
var mini: MiniScript? = try {
|
||||
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
||||
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}")
|
||||
return null
|
||||
null
|
||||
}
|
||||
val mini = sink.build() ?: return null
|
||||
val source = Source("<ide>", text)
|
||||
val haveMini = mini != null
|
||||
if (mini == null) {
|
||||
// Ensure we have a dummy script object to avoid NPE in downstream helpers that expect a MiniScript
|
||||
mini = MiniScript(MiniRange(Pos(src, 1, 1), Pos(src, 1, 1)))
|
||||
}
|
||||
val source = src
|
||||
|
||||
// Try resolve to: function param at position, function/class/val declaration at position
|
||||
// 1) Check declarations whose name range contains offset
|
||||
for (d in mini.declarations) {
|
||||
if (haveMini) for (d in mini.declarations) {
|
||||
val s = source.offsetOf(d.nameStart)
|
||||
val e = (s + d.name.length).coerceAtMost(text.length)
|
||||
if (offset in s until e) {
|
||||
@ -82,7 +91,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
}
|
||||
// 2) Check parameters of functions
|
||||
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||
if (haveMini) for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||
for (p in fn.params) {
|
||||
val s = source.offsetOf(p.nameStart)
|
||||
val e = (s + p.name.length).coerceAtMost(text.length)
|
||||
@ -93,17 +102,25 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
}
|
||||
// 3) As a fallback, if the caret is on an identifier text that matches any declaration name, show that
|
||||
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}")
|
||||
return renderDeclDoc(it)
|
||||
}
|
||||
|
||||
// 4) Consult BuiltinDocRegistry for imported modules (top-level and class members)
|
||||
var importedModules = mini.imports.map { it.segments.joinToString(".") { s -> s.name } }
|
||||
// Core-module fallback: in scratch/repl-like files without imports, consult stdlib by default
|
||||
if (importedModules.isEmpty()) importedModules = listOf("lyng.stdlib")
|
||||
// Canonicalize import names using ImportManager, as users may write shortened names (e.g., "io.fs")
|
||||
var importedModules = if (haveMini) DocLookupUtils.canonicalImportedModules(mini) else emptyList()
|
||||
// If MiniAst failed or captured no imports, try a lightweight textual import scan
|
||||
if (importedModules.isEmpty()) {
|
||||
val fromText = extractImportsFromText(text)
|
||||
if (fromText.isNotEmpty()) {
|
||||
importedModules = fromText
|
||||
}
|
||||
}
|
||||
// Always include stdlib as a fallback context
|
||||
if (!importedModules.contains("lyng.stdlib")) importedModules = importedModules + "lyng.stdlib"
|
||||
// 4a) try top-level decls
|
||||
for (mod in importedModules) {
|
||||
importedModules.forEach { mod ->
|
||||
val docs = BuiltinDocRegistry.docsForModule(mod)
|
||||
val matches = docs.filterIsInstance<MiniFunDecl>().filter { it.name == ident }
|
||||
if (matches.isNotEmpty()) {
|
||||
@ -121,11 +138,19 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
// And classes
|
||||
docs.filterIsInstance<MiniClassDecl>().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") {
|
||||
val fallback = if (ident == "println")
|
||||
"Print values to the standard output and append a newline. Accepts any number of arguments." else
|
||||
"Print values to the standard output without a trailing newline. Accepts any number of arguments."
|
||||
val title = "function $ident(values)"
|
||||
return "<div class='doc-title'>${htmlEscape(title)}</div>" + styledMarkdown(htmlEscape(fallback))
|
||||
}
|
||||
// 4b) try class members like ClassName.member with inheritance fallback
|
||||
val lhs = previousWordBefore(text, idRange.startOffset)
|
||||
if (lhs != null && hasDotBetween(text, lhs.endOffset, idRange.startOffset)) {
|
||||
val className = text.substring(lhs.startOffset, lhs.endOffset)
|
||||
resolveMemberWithInheritance(importedModules, className, ident)?.let { (owner, member) ->
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, className, ident)?.let { (owner, member) ->
|
||||
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Inheritance resolved $className.$ident to $owner.${member.name}")
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
@ -140,10 +165,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
if (dotPos != null) {
|
||||
val guessed = when {
|
||||
looksLikeListLiteralBefore(text, dotPos) -> "List"
|
||||
else -> null
|
||||
else -> DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules)
|
||||
}
|
||||
if (guessed != null) {
|
||||
resolveMemberWithInheritance(importedModules, guessed, ident)?.let { (owner, member) ->
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, guessed, ident)?.let { (owner, member) ->
|
||||
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Heuristic '$guessed.$ident' resolved via inheritance to $owner.${member.name}")
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
@ -152,7 +177,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
} else {
|
||||
// Search across classes; prefer Iterable, then Iterator, then List for common ops
|
||||
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}")
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
@ -167,6 +192,51 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Very lenient import extractor for cases when MiniAst is unavailable.
|
||||
* Looks for lines like `import xxx.yyy` and returns canonical module names
|
||||
* (prefixing with `lyng.` if missing).
|
||||
*/
|
||||
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_]*)*)")
|
||||
re.findAll(text).forEach { m ->
|
||||
val raw = m.groupValues.getOrNull(1)?.trim().orEmpty()
|
||||
if (raw.isNotEmpty()) {
|
||||
val canon = if (raw.startsWith("lyng.")) raw else "lyng.$raw"
|
||||
result.add(canon)
|
||||
}
|
||||
}
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
// External docs registrars discovery via reflection to avoid hard dependencies on optional modules
|
||||
private val externalDocsLoaded: Boolean by lazy { tryLoadExternalDocs() }
|
||||
|
||||
private fun ensureExternalDocsRegistered() { @Suppress("UNUSED_EXPRESSION") externalDocsLoaded }
|
||||
|
||||
private fun tryLoadExternalDocs(): Boolean {
|
||||
return try {
|
||||
// Try known registrars; ignore failures if module is absent
|
||||
val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs")
|
||||
val m = cls.getMethod("ensure")
|
||||
m.invoke(null)
|
||||
log.info("[LYNG_DEBUG] QuickDoc: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK")
|
||||
true
|
||||
} catch (_: Throwable) {
|
||||
// Seed a minimal plugin-local fallback so Path docs still work without lyngio
|
||||
val seeded = try {
|
||||
FsDocsFallback.ensureOnce()
|
||||
} catch (_: Throwable) { false }
|
||||
if (seeded) {
|
||||
log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found; seeded plugin fallback for lyng.io.fs")
|
||||
} else {
|
||||
log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found (lyngio absent on classpath)")
|
||||
}
|
||||
seeded
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCustomDocumentationElement(
|
||||
editor: Editor,
|
||||
file: PsiFile,
|
||||
@ -362,46 +432,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
|
||||
// --- Helpers for inheritance-aware and heuristic member lookup ---
|
||||
|
||||
private fun aggregateClasses(importedModules: List<String>): Map<String, MiniClassDecl> {
|
||||
val map = LinkedHashMap<String, MiniClassDecl>()
|
||||
for (mod in importedModules) {
|
||||
val docs = BuiltinDocRegistry.docsForModule(mod)
|
||||
docs.filterIsInstance<MiniClassDecl>().forEach { cls ->
|
||||
// Prefer the first occurrence; allow later duplicates to be ignored
|
||||
map.putIfAbsent(cls.name, cls)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
private fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String): Pair<String, MiniMemberDecl>? {
|
||||
val classes = aggregateClasses(importedModules)
|
||||
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 }
|
||||
if (!visited.add(name)) return null
|
||||
for (baseName in cls.bases) {
|
||||
dfs(baseName, visited)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
return dfs(className, mutableSetOf())
|
||||
}
|
||||
|
||||
private fun findMemberAcrossClasses(importedModules: List<String>, member: String): Pair<String, MiniMemberDecl>? {
|
||||
val classes = aggregateClasses(importedModules)
|
||||
// Preferred order for ambiguous common ops
|
||||
val preference = listOf("Iterable", "Iterator", "List")
|
||||
// First, try preference order
|
||||
for (name in preference) {
|
||||
resolveMemberWithInheritance(importedModules, name, member)?.let { return it }
|
||||
}
|
||||
// Then, scan all
|
||||
for ((name, cls) in classes) {
|
||||
cls.members.firstOrNull { it.name == member }?.let { return name to it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
// Removed: member/class resolution helpers moved to lynglib DocLookupUtils for reuse
|
||||
|
||||
private fun findDotLeft(text: String, rightStart: Int): Int? {
|
||||
var i = (rightStart - 1).coerceAtLeast(0)
|
||||
@ -433,4 +464,6 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Removed: guessClassFromCallBefore moved to DocLookupUtils
|
||||
}
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Filesystem module builtin docs registration, located in lyngio so core library
|
||||
* does not depend on external packages. The IDEA plugin (and any other tooling)
|
||||
* may reflectively call FsBuiltinDocs.ensure() to make sure docs are registered.
|
||||
*/
|
||||
package net.sergeych.lyngio.docs
|
||||
|
||||
import net.sergeych.lyng.miniast.BuiltinDocRegistry
|
||||
import net.sergeych.lyng.miniast.ParamDoc
|
||||
import net.sergeych.lyng.miniast.TypeGenericDoc
|
||||
import net.sergeych.lyng.miniast.type
|
||||
|
||||
object FsBuiltinDocs {
|
||||
private var registered = false
|
||||
|
||||
fun ensure() {
|
||||
if (registered) return
|
||||
// Register docs immediately (not lazy) so tooling can see them without executing module builders
|
||||
BuiltinDocRegistry.module("lyng.io.fs") {
|
||||
// Class Path with a short summary
|
||||
classDoc(
|
||||
name = "Path",
|
||||
doc = "Filesystem path class. Construct with a string: `Path(\"/tmp\")`."
|
||||
) {
|
||||
// Common instance methods (subset sufficient for Quick Docs)
|
||||
method(
|
||||
name = "exists",
|
||||
doc = "Whether the path exists on the filesystem.",
|
||||
returns = type("lyng.Bool")
|
||||
)
|
||||
method(
|
||||
name = "isFile",
|
||||
doc = "Whether the path exists and is a file.",
|
||||
returns = type("lyng.Bool")
|
||||
)
|
||||
method(
|
||||
name = "isDir",
|
||||
doc = "Whether the path exists and is a directory.",
|
||||
returns = type("lyng.Bool")
|
||||
)
|
||||
method(
|
||||
name = "readUtf8",
|
||||
doc = "Read the entire file as UTF-8 string.",
|
||||
returns = type("lyng.String")
|
||||
)
|
||||
method(
|
||||
name = "writeUtf8",
|
||||
doc = "Write UTF-8 string to the file (overwrite).",
|
||||
params = listOf(ParamDoc("text", type("lyng.String")))
|
||||
)
|
||||
method(
|
||||
name = "bytes",
|
||||
doc = "Iterate file content as `Buffer` chunks.",
|
||||
params = listOf(ParamDoc("size", type("lyng.Int"))),
|
||||
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Buffer")))
|
||||
)
|
||||
method(
|
||||
name = "lines",
|
||||
doc = "Iterate file as lines of text.",
|
||||
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String")))
|
||||
)
|
||||
}
|
||||
|
||||
// Top-level exported constants
|
||||
valDoc(
|
||||
name = "Path",
|
||||
doc = "Filesystem path class. Construct with a string: `Path(\"/tmp\")`.",
|
||||
type = type("Path")
|
||||
)
|
||||
valDoc(
|
||||
name = "Paths",
|
||||
doc = "Alias of `Path` for those who prefer plural form.",
|
||||
type = type("Path")
|
||||
)
|
||||
}
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
@ -608,4 +608,4 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
||||
return decls
|
||||
}
|
||||
|
||||
// (Registration is triggered from BuiltinDocRegistry.init)
|
||||
// (Registration for external modules is provided by their own libraries)
|
||||
|
||||
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Shared QuickDoc lookup helpers reusable outside the IDEA plugin.
|
||||
*/
|
||||
package net.sergeych.lyng.miniast
|
||||
|
||||
object DocLookupUtils {
|
||||
/**
|
||||
* Convert MiniAst imports to fully-qualified module names expected by BuiltinDocRegistry.
|
||||
* Heuristics:
|
||||
* - If an import does not start with "lyng.", prefix it with "lyng." (e.g., "io.fs" -> "lyng.io.fs").
|
||||
* - Keep original if it already starts with "lyng.".
|
||||
* - Always include "lyng.stdlib" to make builtins available for docs.
|
||||
*/
|
||||
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
|
||||
result.add("lyng.stdlib")
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
fun aggregateClasses(importedModules: List<String>): Map<String, MiniClassDecl> {
|
||||
val map = LinkedHashMap<String, MiniClassDecl>()
|
||||
for (mod in importedModules) {
|
||||
val docs = BuiltinDocRegistry.docsForModule(mod)
|
||||
docs.filterIsInstance<MiniClassDecl>().forEach { cls ->
|
||||
if (!map.containsKey(cls.name)) map[cls.name] = cls
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String): Pair<String, MiniMemberDecl>? {
|
||||
val classes = aggregateClasses(importedModules)
|
||||
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 }
|
||||
if (!visited.add(name)) return null
|
||||
for (baseName in cls.bases) {
|
||||
dfs(baseName, visited)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
return dfs(className, mutableSetOf())
|
||||
}
|
||||
|
||||
fun findMemberAcrossClasses(importedModules: List<String>, member: String): Pair<String, MiniMemberDecl>? {
|
||||
val classes = aggregateClasses(importedModules)
|
||||
// Preferred order for ambiguous common ops
|
||||
val preference = listOf("Iterable", "Iterator", "List")
|
||||
for (name in preference) {
|
||||
resolveMemberWithInheritance(importedModules, name, member)?.let { return it }
|
||||
}
|
||||
for ((name, cls) in classes) {
|
||||
cls.members.firstOrNull { it.name == member }?.let { return name to it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to guess a class name of the receiver when the receiver is a call like `ClassName(...)`.
|
||||
* 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? {
|
||||
var i = (dotPos - 1).coerceAtLeast(0)
|
||||
// Skip spaces
|
||||
while (i >= 0 && text[i].isWhitespace()) i++
|
||||
// Note: the previous line is wrong direction; correct implementation below
|
||||
i = (dotPos - 1).coerceAtLeast(0)
|
||||
while (i >= 0 && text[i].isWhitespace()) i--
|
||||
if (i < 0 || text[i] != ')') return null
|
||||
// Walk back to matching '(' accounting nested parentheses
|
||||
var depth = 0
|
||||
i--
|
||||
while (i >= 0) {
|
||||
val ch = text[i]
|
||||
when (ch) {
|
||||
')' -> depth++
|
||||
'(' -> if (depth == 0) break else depth--
|
||||
'\n' -> {}
|
||||
}
|
||||
i--
|
||||
}
|
||||
if (i < 0 || text[i] != '(') return null
|
||||
// Now find identifier immediately before '('
|
||||
var j = i - 1
|
||||
while (j >= 0 && text[j].isWhitespace()) j--
|
||||
val end = j + 1
|
||||
while (j >= 0 && isIdentChar(text[j])) j--
|
||||
val start = j + 1
|
||||
if (start >= end) return null
|
||||
val name = text.substring(start, end)
|
||||
// Validate against imported classes
|
||||
val classes = aggregateClasses(importedModules)
|
||||
return if (classes.containsKey(name)) name else null
|
||||
}
|
||||
|
||||
private fun isIdentChar(c: Char): Boolean = c == '_' || c.isLetterOrDigit()
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user