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.kotlinMultiplatform) apply false
|
||||||
alias(libs.plugins.vanniktech.mavenPublish) 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?
|
// most long is longest?
|
||||||
val longestNameLength = files.maxOf { it.name.length }
|
val longestNameLength = files.maxOf { it.name.length }
|
||||||
|
|
||||||
|
// testdoc
|
||||||
|
fun test() {
|
||||||
|
22
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val format = "%"+(longestNameLength+1) +"s %d"
|
val format = "%"+(longestNameLength+1) +"s %d"
|
||||||
for( f in files )
|
for( f in files )
|
||||||
|
|||||||
@ -37,6 +37,8 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":lynglib"))
|
implementation(project(":lynglib"))
|
||||||
|
// Include lyngio so Quick Docs can reflectively load fs docs registrar (FsBuiltinDocs)
|
||||||
|
implementation(project(":lyngio"))
|
||||||
// Rich Markdown renderer for Quick Docs
|
// Rich Markdown renderer for Quick Docs
|
||||||
implementation("com.vladsch.flexmark:flexmark-all:0.64.8")
|
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 com.intellij.psi.PsiFile
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.sergeych.lyng.Compiler
|
import net.sergeych.lyng.Compiler
|
||||||
|
import net.sergeych.lyng.Pos
|
||||||
import net.sergeych.lyng.Source
|
import net.sergeych.lyng.Source
|
||||||
import net.sergeych.lyng.highlight.offsetOf
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
import net.sergeych.lyng.idea.LyngLanguage
|
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.
|
// Toggle to trace inheritance-based resolutions in Quick Docs. Keep false for normal use.
|
||||||
private val DEBUG_INHERITANCE = false
|
private val DEBUG_INHERITANCE = 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
|
||||||
|
ensureExternalDocsRegistered()
|
||||||
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
|
||||||
@ -57,23 +60,29 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
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}")
|
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()
|
val sink = MiniAstBuilder()
|
||||||
// Use lenient import provider so unresolved imports (e.g., lyng.io.fs) don't break docs
|
// Use lenient import provider so unresolved imports (e.g., lyng.io.fs) don't break docs
|
||||||
val provider = IdeLenientImportProvider.create()
|
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) }
|
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
||||||
|
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)
|
||||||
log.warn("[LYNG_DEBUG] QuickDoc: compileWithMini failed: ${t.message}")
|
log.warn("[LYNG_DEBUG] QuickDoc: compileWithMini failed: ${t.message}")
|
||||||
return null
|
null
|
||||||
}
|
}
|
||||||
val mini = sink.build() ?: return null
|
val haveMini = mini != null
|
||||||
val source = Source("<ide>", text)
|
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
|
// Try resolve to: function param at position, function/class/val declaration at position
|
||||||
// 1) Check declarations whose name range contains offset
|
// 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 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) {
|
||||||
@ -82,7 +91,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 2) Check parameters of functions
|
// 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) {
|
for (p in fn.params) {
|
||||||
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)
|
||||||
@ -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
|
// 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}")
|
log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}")
|
||||||
return renderDeclDoc(it)
|
return renderDeclDoc(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Consult BuiltinDocRegistry for imported modules (top-level and class members)
|
// 4) Consult BuiltinDocRegistry for imported modules (top-level and class members)
|
||||||
var importedModules = mini.imports.map { it.segments.joinToString(".") { s -> s.name } }
|
// Canonicalize import names using ImportManager, as users may write shortened names (e.g., "io.fs")
|
||||||
// Core-module fallback: in scratch/repl-like files without imports, consult stdlib by default
|
var importedModules = if (haveMini) DocLookupUtils.canonicalImportedModules(mini) else emptyList()
|
||||||
if (importedModules.isEmpty()) importedModules = listOf("lyng.stdlib")
|
// 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
|
// 4a) try top-level decls
|
||||||
for (mod in importedModules) {
|
importedModules.forEach { mod ->
|
||||||
val docs = BuiltinDocRegistry.docsForModule(mod)
|
val docs = BuiltinDocRegistry.docsForModule(mod)
|
||||||
val matches = docs.filterIsInstance<MiniFunDecl>().filter { it.name == ident }
|
val matches = docs.filterIsInstance<MiniFunDecl>().filter { it.name == ident }
|
||||||
if (matches.isNotEmpty()) {
|
if (matches.isNotEmpty()) {
|
||||||
@ -121,11 +138,19 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
// And classes
|
// And classes
|
||||||
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
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
|
// 4b) try class members like ClassName.member with inheritance fallback
|
||||||
val lhs = previousWordBefore(text, idRange.startOffset)
|
val lhs = previousWordBefore(text, idRange.startOffset)
|
||||||
if (lhs != null && hasDotBetween(text, lhs.endOffset, idRange.startOffset)) {
|
if (lhs != null && hasDotBetween(text, lhs.endOffset, idRange.startOffset)) {
|
||||||
val className = text.substring(lhs.startOffset, lhs.endOffset)
|
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}")
|
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Inheritance resolved $className.$ident to $owner.${member.name}")
|
||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
@ -140,10 +165,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
if (dotPos != null) {
|
if (dotPos != null) {
|
||||||
val guessed = when {
|
val guessed = when {
|
||||||
looksLikeListLiteralBefore(text, dotPos) -> "List"
|
looksLikeListLiteralBefore(text, dotPos) -> "List"
|
||||||
else -> null
|
else -> DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules)
|
||||||
}
|
}
|
||||||
if (guessed != null) {
|
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}")
|
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Heuristic '$guessed.$ident' resolved via inheritance to $owner.${member.name}")
|
||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
@ -152,7 +177,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Search across classes; prefer Iterable, then Iterator, then List for common ops
|
// 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}")
|
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Cross-class '$ident' resolved to $owner.${member.name}")
|
||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
@ -167,6 +192,51 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return null
|
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(
|
override fun getCustomDocumentationElement(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
file: PsiFile,
|
file: PsiFile,
|
||||||
@ -362,46 +432,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
|
|
||||||
// --- Helpers for inheritance-aware and heuristic member lookup ---
|
// --- Helpers for inheritance-aware and heuristic member lookup ---
|
||||||
|
|
||||||
private fun aggregateClasses(importedModules: List<String>): Map<String, MiniClassDecl> {
|
// Removed: member/class resolution helpers moved to lynglib DocLookupUtils for reuse
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findDotLeft(text: String, rightStart: Int): Int? {
|
private fun findDotLeft(text: String, rightStart: Int): Int? {
|
||||||
var i = (rightStart - 1).coerceAtLeast(0)
|
var i = (rightStart - 1).coerceAtLeast(0)
|
||||||
@ -433,4 +464,6 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
return false
|
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
|
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