plugin no wworks with .lyng.d files. hurray
This commit is contained in:
parent
fdc044d1e0
commit
aba0048a83
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng.idea
|
||||
|
||||
import com.intellij.openapi.fileTypes.FileTypeConsumer
|
||||
import com.intellij.openapi.fileTypes.FileTypeFactory
|
||||
import com.intellij.openapi.fileTypes.WildcardFileNameMatcher
|
||||
|
||||
/**
|
||||
* Legacy way to register file type matchers, used here to robustly match *.lyng.d
|
||||
* without conflicting with standard .d extensions from other plugins.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
class LyngFileTypeFactory : FileTypeFactory() {
|
||||
override fun createFileTypes(consumer: FileTypeConsumer) {
|
||||
// Register the multi-dot pattern explicitly
|
||||
consumer.consume(LyngFileType, WildcardFileNameMatcher("*.lyng.d"))
|
||||
}
|
||||
}
|
||||
@ -25,9 +25,6 @@ import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiFile
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.ScriptError
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.binding.Binder
|
||||
import net.sergeych.lyng.binding.SymbolKind
|
||||
@ -35,7 +32,7 @@ import net.sergeych.lyng.highlight.HighlightKind
|
||||
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
import net.sergeych.lyng.idea.highlight.LyngHighlighterColors
|
||||
import net.sergeych.lyng.idea.util.IdeLenientImportProvider
|
||||
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||
import net.sergeych.lyng.miniast.*
|
||||
|
||||
/**
|
||||
@ -43,7 +40,7 @@ import net.sergeych.lyng.miniast.*
|
||||
* and applies semantic highlighting comparable with the web highlighter.
|
||||
*/
|
||||
class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, LyngExternalAnnotator.Result>() {
|
||||
data class Input(val text: String, val modStamp: Long, val previousSpans: List<Span>?)
|
||||
data class Input(val text: String, val modStamp: Long, val previousSpans: List<Span>?, val file: PsiFile)
|
||||
|
||||
data class Span(val start: Int, val end: Int, val key: com.intellij.openapi.editor.colors.TextAttributesKey)
|
||||
data class Error(val start: Int, val end: Int, val message: String)
|
||||
@ -55,43 +52,24 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
override fun collectInformation(file: PsiFile): Input? {
|
||||
val doc: Document = file.viewProvider.document ?: return null
|
||||
val cached = file.getUserData(CACHE_KEY)
|
||||
// Fast fix (1): reuse cached spans only if they were computed for the same modification stamp
|
||||
val prev = if (cached != null && cached.modStamp == doc.modificationStamp) cached.spans else null
|
||||
return Input(doc.text, doc.modificationStamp, prev)
|
||||
val combinedStamp = LyngAstManager.getCombinedStamp(file)
|
||||
|
||||
val prev = if (cached != null && cached.modStamp == combinedStamp) cached.spans else null
|
||||
return Input(doc.text, combinedStamp, prev, file)
|
||||
}
|
||||
|
||||
override fun doAnnotate(collectedInfo: Input?): Result? {
|
||||
if (collectedInfo == null) return null
|
||||
ProgressManager.checkCanceled()
|
||||
val text = collectedInfo.text
|
||||
// Build Mini-AST using the same mechanism as web highlighter
|
||||
val sink = MiniAstBuilder()
|
||||
val source = Source("<ide>", text)
|
||||
try {
|
||||
// Call suspend API from blocking context
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
runBlocking { Compiler.compileWithMini(source, provider, sink) }
|
||||
} catch (e: Throwable) {
|
||||
if (e is com.intellij.openapi.progress.ProcessCanceledException) throw e
|
||||
// On script parse error: keep previous spans and report the error location
|
||||
if (e is ScriptError) {
|
||||
val off = try { source.offsetOf(e.pos) } catch (_: Throwable) { -1 }
|
||||
val start0 = off.coerceIn(0, text.length.coerceAtLeast(0))
|
||||
val (start, end) = expandErrorRange(text, start0)
|
||||
// Fast fix (5): clear cached highlighting after the error start position
|
||||
val trimmed = collectedInfo.previousSpans?.filter { it.end <= start } ?: emptyList()
|
||||
return Result(
|
||||
collectedInfo.modStamp,
|
||||
trimmed,
|
||||
Error(start, end, e.errorMessage)
|
||||
)
|
||||
}
|
||||
// Other failures: keep previous spans without error
|
||||
return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList(), null)
|
||||
}
|
||||
|
||||
// Use LyngAstManager to get the (potentially merged) Mini-AST
|
||||
val mini = LyngAstManager.getMiniAst(collectedInfo.file)
|
||||
?: return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList())
|
||||
|
||||
ProgressManager.checkCanceled()
|
||||
val mini = sink.build() ?: return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList())
|
||||
|
||||
val source = Source(collectedInfo.file.name, text)
|
||||
|
||||
val out = ArrayList<Span>(256)
|
||||
|
||||
fun isFollowedByParenOrBlock(rangeEnd: Int): Boolean {
|
||||
@ -118,7 +96,8 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
}
|
||||
|
||||
// Declarations
|
||||
for (d in mini.declarations) {
|
||||
mini.declarations.forEach { d ->
|
||||
if (d.nameStart.source != source) return@forEach
|
||||
when (d) {
|
||||
is MiniFunDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.FUNCTION_DECLARATION)
|
||||
is MiniClassDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE)
|
||||
@ -132,19 +111,22 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
}
|
||||
|
||||
// Imports: each segment as namespace/path
|
||||
for (imp in mini.imports) {
|
||||
for (seg in imp.segments) putMiniRange(seg.range, LyngHighlighterColors.NAMESPACE)
|
||||
mini.imports.forEach { imp ->
|
||||
if (imp.range.start.source != source) return@forEach
|
||||
imp.segments.forEach { seg -> putMiniRange(seg.range, LyngHighlighterColors.NAMESPACE) }
|
||||
}
|
||||
|
||||
// Parameters
|
||||
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||
for (p in fn.params) putName(p.nameStart, p.name, LyngHighlighterColors.PARAMETER)
|
||||
mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
|
||||
if (fn.nameStart.source != source) return@forEach
|
||||
fn.params.forEach { p -> putName(p.nameStart, p.name, LyngHighlighterColors.PARAMETER) }
|
||||
}
|
||||
|
||||
// Type name segments (including generics base & args)
|
||||
fun addTypeSegments(t: MiniTypeRef?) {
|
||||
when (t) {
|
||||
is MiniTypeName -> t.segments.forEach { seg ->
|
||||
if (seg.range.start.source != source) return@forEach
|
||||
val s = source.offsetOf(seg.range.start)
|
||||
putRange(s, (s + seg.name.length).coerceAtMost(text.length), LyngHighlighterColors.TYPE)
|
||||
}
|
||||
@ -158,12 +140,14 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
addTypeSegments(t.returnType)
|
||||
}
|
||||
is MiniTypeVar -> { /* name is in range; could be highlighted as TYPE as well */
|
||||
putMiniRange(t.range, LyngHighlighterColors.TYPE)
|
||||
if (t.range.start.source == source)
|
||||
putMiniRange(t.range, LyngHighlighterColors.TYPE)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
for (d in mini.declarations) {
|
||||
mini.declarations.forEach { d ->
|
||||
if (d.nameStart.source != source) return@forEach
|
||||
when (d) {
|
||||
is MiniFunDecl -> {
|
||||
addTypeSegments(d.returnType)
|
||||
@ -190,7 +174,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
|
||||
// Map declaration ranges to avoid duplicating them as usages
|
||||
val declKeys = HashSet<Pair<Int, Int>>(binding.symbols.size * 2)
|
||||
for (sym in binding.symbols) declKeys += (sym.declStart to sym.declEnd)
|
||||
binding.symbols.forEach { sym -> declKeys += (sym.declStart to sym.declEnd) }
|
||||
|
||||
fun keyForKind(k: SymbolKind) = when (k) {
|
||||
SymbolKind.Function -> LyngHighlighterColors.FUNCTION
|
||||
@ -203,13 +187,16 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
// Track covered ranges to not override later heuristics
|
||||
val covered = HashSet<Pair<Int, Int>>()
|
||||
|
||||
for (ref in binding.references) {
|
||||
binding.references.forEach { ref ->
|
||||
val key = ref.start to ref.end
|
||||
if (declKeys.contains(key)) continue
|
||||
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId } ?: continue
|
||||
val color = keyForKind(sym.kind)
|
||||
putRange(ref.start, ref.end, color)
|
||||
covered += key
|
||||
if (!declKeys.contains(key)) {
|
||||
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
|
||||
if (sym != null) {
|
||||
val color = keyForKind(sym.kind)
|
||||
putRange(ref.start, ref.end, color)
|
||||
covered += key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Heuristics on top of binder: function call-sites and simple name-based roles
|
||||
@ -219,32 +206,41 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
|
||||
// Build simple name -> role map for top-level vals/vars and parameters
|
||||
val nameRole = HashMap<String, com.intellij.openapi.editor.colors.TextAttributesKey>(8)
|
||||
for (d in mini.declarations) when (d) {
|
||||
is MiniValDecl -> nameRole[d.name] = if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE
|
||||
is MiniFunDecl -> d.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER }
|
||||
else -> {}
|
||||
mini.declarations.forEach { d ->
|
||||
when (d) {
|
||||
is MiniValDecl -> nameRole[d.name] =
|
||||
if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE
|
||||
|
||||
is MiniFunDecl -> d.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER }
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
for (s in tokens) if (s.kind == HighlightKind.Identifier) {
|
||||
val start = s.range.start
|
||||
val end = s.range.endExclusive
|
||||
val key = start to end
|
||||
if (key in covered || key in declKeys) continue
|
||||
|
||||
// Call-site detection first so it wins over var/param role
|
||||
if (isFollowedByParenOrBlock(end)) {
|
||||
putRange(start, end, LyngHighlighterColors.FUNCTION)
|
||||
covered += key
|
||||
continue
|
||||
}
|
||||
|
||||
// Simple role by known names
|
||||
val ident = try { text.substring(start, end) } catch (_: Throwable) { null }
|
||||
if (ident != null) {
|
||||
val roleKey = nameRole[ident]
|
||||
if (roleKey != null) {
|
||||
putRange(start, end, roleKey)
|
||||
covered += key
|
||||
tokens.forEach { s ->
|
||||
if (s.kind == HighlightKind.Identifier) {
|
||||
val start = s.range.start
|
||||
val end = s.range.endExclusive
|
||||
val key = start to end
|
||||
if (key !in covered && key !in declKeys) {
|
||||
// Call-site detection first so it wins over var/param role
|
||||
if (isFollowedByParenOrBlock(end)) {
|
||||
putRange(start, end, LyngHighlighterColors.FUNCTION)
|
||||
covered += key
|
||||
} else {
|
||||
// Simple role by known names
|
||||
val ident = try {
|
||||
text.substring(start, end)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
if (ident != null) {
|
||||
val roleKey = nameRole[ident]
|
||||
if (roleKey != null) {
|
||||
putRange(start, end, roleKey)
|
||||
covered += key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,31 +252,37 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
// Add annotation/label coloring using token highlighter
|
||||
run {
|
||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||
for (s in tokens) if (s.kind == HighlightKind.Label) {
|
||||
val start = s.range.start
|
||||
val end = s.range.endExclusive
|
||||
if (start in 0..end && end <= text.length && start < end) {
|
||||
val lexeme = try { text.substring(start, end) } catch (_: Throwable) { null }
|
||||
if (lexeme != null) {
|
||||
// Heuristic: if it starts with @ and follows a control keyword, it's likely a label
|
||||
// Otherwise if it starts with @ it's an annotation.
|
||||
// If it ends with @ it's a loop label.
|
||||
when {
|
||||
lexeme.endsWith("@") -> putRange(start, end, LyngHighlighterColors.LABEL)
|
||||
lexeme.startsWith("@") -> {
|
||||
// Try to see if it's an exit label
|
||||
val prevNonWs = prevNonWs(text, start)
|
||||
val prevWord = if (prevNonWs >= 0) {
|
||||
var wEnd = prevNonWs + 1
|
||||
var wStart = prevNonWs
|
||||
while (wStart > 0 && text[wStart - 1].isLetter()) wStart--
|
||||
text.substring(wStart, wEnd)
|
||||
} else null
|
||||
|
||||
if (prevWord in setOf("return", "break", "continue") || isFollowedByParenOrBlock(end)) {
|
||||
putRange(start, end, LyngHighlighterColors.LABEL)
|
||||
} else {
|
||||
putRange(start, end, LyngHighlighterColors.ANNOTATION)
|
||||
tokens.forEach { s ->
|
||||
if (s.kind == HighlightKind.Label) {
|
||||
val start = s.range.start
|
||||
val end = s.range.endExclusive
|
||||
if (start in 0..end && end <= text.length && start < end) {
|
||||
val lexeme = try {
|
||||
text.substring(start, end)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
if (lexeme != null) {
|
||||
// Heuristic: if it starts with @ and follows a control keyword, it's likely a label
|
||||
// Otherwise if it starts with @ it's an annotation.
|
||||
// If it ends with @ it's a loop label.
|
||||
when {
|
||||
lexeme.endsWith("@") -> putRange(start, end, LyngHighlighterColors.LABEL)
|
||||
lexeme.startsWith("@") -> {
|
||||
// Try to see if it's an exit label
|
||||
val prevNonWs = prevNonWs(text, start)
|
||||
val prevWord = if (prevNonWs >= 0) {
|
||||
var wEnd = prevNonWs + 1
|
||||
var wStart = prevNonWs
|
||||
while (wStart > 0 && text[wStart - 1].isLetter()) wStart--
|
||||
text.substring(wStart, wEnd)
|
||||
} else null
|
||||
|
||||
if (prevWord in setOf("return", "break", "continue") || isFollowedByParenOrBlock(end)) {
|
||||
putRange(start, end, LyngHighlighterColors.LABEL)
|
||||
} else {
|
||||
putRange(start, end, LyngHighlighterColors.ANNOTATION)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -292,11 +294,13 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
// Map Enum constants from token highlighter to IDEA enum constant color
|
||||
run {
|
||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||
for (s in tokens) if (s.kind == HighlightKind.EnumConstant) {
|
||||
val start = s.range.start
|
||||
val end = s.range.endExclusive
|
||||
if (start in 0..end && end <= text.length && start < end) {
|
||||
putRange(start, end, LyngHighlighterColors.ENUM_CONSTANT)
|
||||
tokens.forEach { s ->
|
||||
if (s.kind == HighlightKind.EnumConstant) {
|
||||
val start = s.range.start
|
||||
val end = s.range.endExclusive
|
||||
if (start in 0..end && end <= text.length && start < end) {
|
||||
putRange(start, end, LyngHighlighterColors.ENUM_CONSTANT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -305,12 +309,14 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
val idRanges = mutableSetOf<IntRange>()
|
||||
try {
|
||||
val binding = Binder.bind(text, mini)
|
||||
for (sym in binding.symbols) {
|
||||
val s = sym.declStart; val e = sym.declEnd
|
||||
binding.symbols.forEach { sym ->
|
||||
val s = sym.declStart
|
||||
val e = sym.declEnd
|
||||
if (s in 0..e && e <= text.length && s < e) idRanges += (s until e)
|
||||
}
|
||||
for (ref in binding.references) {
|
||||
val s = ref.start; val e = ref.end
|
||||
binding.references.forEach { ref ->
|
||||
val s = ref.start
|
||||
val e = ref.end
|
||||
if (s in 0..e && e <= text.length && s < e) idRanges += (s until e)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
@ -329,12 +335,13 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
override fun apply(file: PsiFile, annotationResult: Result?, holder: AnnotationHolder) {
|
||||
if (annotationResult == null) return
|
||||
// Skip if cache is up-to-date
|
||||
val doc = file.viewProvider.document
|
||||
val currentStamp = doc?.modificationStamp
|
||||
val combinedStamp = LyngAstManager.getCombinedStamp(file)
|
||||
val cached = file.getUserData(CACHE_KEY)
|
||||
val result = if (cached != null && currentStamp != null && cached.modStamp == currentStamp) cached else annotationResult
|
||||
val result = if (cached != null && cached.modStamp == combinedStamp) cached else annotationResult
|
||||
file.putUserData(CACHE_KEY, result)
|
||||
|
||||
val doc = file.viewProvider.document
|
||||
|
||||
// Store spell index for spell/grammar engines to consume (suspend until ready)
|
||||
val ids = result.spellIdentifiers.map { TextRange(it.first, it.last + 1) }
|
||||
val coms = result.spellComments.map { TextRange(it.first, it.last + 1) }
|
||||
|
||||
@ -102,7 +102,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
|
||||
// Delegate computation to the shared engine to keep behavior in sync with tests
|
||||
val engineItems = try {
|
||||
runBlocking { CompletionEngineLight.completeSuspend(text, caret) }
|
||||
runBlocking { CompletionEngineLight.completeSuspend(text, caret, mini) }
|
||||
} catch (t: Throwable) {
|
||||
if (DEBUG_COMPLETION) log.warn("[LYNG_DEBUG] Engine completion failed: ${t.message}")
|
||||
emptyList()
|
||||
@ -452,7 +452,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
for (name in common) {
|
||||
if (name in already) continue
|
||||
// Try resolve across classes first to get types/params; if it fails, emit a synthetic safe suggestion.
|
||||
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, name)
|
||||
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, name, mini)
|
||||
if (resolved != null) {
|
||||
val member = resolved.second
|
||||
when (member) {
|
||||
@ -502,7 +502,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
for (name in ext) {
|
||||
if (already.contains(name)) continue
|
||||
// Try to resolve full signature via registry first to get params and return type
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name)
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini)
|
||||
if (resolved != null) {
|
||||
when (val member = resolved.second) {
|
||||
is MiniMemberFunDecl -> {
|
||||
|
||||
@ -24,13 +24,9 @@ import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.util.TextRange
|
||||
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
|
||||
import net.sergeych.lyng.idea.util.IdeLenientImportProvider
|
||||
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||
import net.sergeych.lyng.idea.util.TextCtx
|
||||
import net.sergeych.lyng.miniast.*
|
||||
|
||||
@ -69,80 +65,90 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
val ident = text.substring(idRange.startOffset, idRange.endOffset)
|
||||
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 partial AST.
|
||||
val sink = MiniAstBuilder()
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
val src = Source("<ide>", text)
|
||||
val mini = try {
|
||||
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
||||
sink.build()
|
||||
} catch (t: Throwable) {
|
||||
if (DEBUG_LOG) log.warn("[LYNG_DEBUG] QuickDoc: compileWithMini produced partial AST: ${t.message}")
|
||||
sink.build()
|
||||
} ?: MiniScript(MiniRange(Pos(src, 1, 1), Pos(src, 1, 1)))
|
||||
val source = src
|
||||
// 1. Get merged mini-AST from Manager (handles local + .lyng.d merged declarations)
|
||||
val mini = LyngAstManager.getMiniAst(file) ?: return null
|
||||
val miniSource = mini.range.start.source
|
||||
|
||||
// Try resolve to: function param at position, function/class/val declaration at position
|
||||
// 1) Use unified declaration detection
|
||||
DocLookupUtils.findDeclarationAt(mini, offset, ident)?.let { (name, kind) ->
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: matched declaration '$name' kind=$kind")
|
||||
// Find the actual declaration object to render
|
||||
for (d in mini.declarations) {
|
||||
if (d.name == name && source.offsetOf(d.nameStart) <= offset && source.offsetOf(d.nameStart) + d.name.length > offset) {
|
||||
return renderDeclDoc(d)
|
||||
mini.declarations.forEach { d ->
|
||||
if (d.name == name) {
|
||||
val s: Int = miniSource.offsetOf(d.nameStart)
|
||||
if (s <= offset && s + d.name.length > offset) {
|
||||
return renderDeclDoc(d)
|
||||
}
|
||||
}
|
||||
// Handle members if it was a member
|
||||
if (d is MiniClassDecl) {
|
||||
for (m in d.members) {
|
||||
if (m.name == name && source.offsetOf(m.nameStart) <= offset && source.offsetOf(m.nameStart) + m.name.length > offset) {
|
||||
return when (m) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(d.name, m)
|
||||
is MiniInitDecl -> null
|
||||
d.members.forEach { m ->
|
||||
if (m.name == name) {
|
||||
val s: Int = miniSource.offsetOf(m.nameStart)
|
||||
if (s <= offset && s + m.name.length > offset) {
|
||||
return when (m) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(d.name, m)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (cf in d.ctorFields) {
|
||||
if (cf.name == name && source.offsetOf(cf.nameStart) <= offset && source.offsetOf(cf.nameStart) + cf.name.length > offset) {
|
||||
// Render as a member val
|
||||
val mv = MiniMemberValDecl(
|
||||
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
return renderMemberValDoc(d.name, mv)
|
||||
d.ctorFields.forEach { cf ->
|
||||
if (cf.name == name) {
|
||||
val s: Int = miniSource.offsetOf(cf.nameStart)
|
||||
if (s <= offset && s + cf.name.length > offset) {
|
||||
// Render as a member val
|
||||
val mv = MiniMemberValDecl(
|
||||
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
return renderMemberValDoc(d.name, mv)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (cf in d.classFields) {
|
||||
if (cf.name == name && source.offsetOf(cf.nameStart) <= offset && source.offsetOf(cf.nameStart) + cf.name.length > offset) {
|
||||
// Render as a member val
|
||||
val mv = MiniMemberValDecl(
|
||||
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
return renderMemberValDoc(d.name, mv)
|
||||
d.classFields.forEach { cf ->
|
||||
if (cf.name == name) {
|
||||
val s: Int = miniSource.offsetOf(cf.nameStart)
|
||||
if (s <= offset && s + cf.name.length > offset) {
|
||||
// Render as a member val
|
||||
val mv = MiniMemberValDecl(
|
||||
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
return renderMemberValDoc(d.name, mv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (d is MiniEnumDecl) {
|
||||
if (d.entries.contains(name) && offset >= source.offsetOf(d.range.start) && offset <= source.offsetOf(d.range.end)) {
|
||||
// For enum constant, we don't have detailed docs in MiniAst yet, but we can render a title
|
||||
return "<div class='doc-title'>enum constant ${d.name}.${name}</div>"
|
||||
if (d.entries.contains(name)) {
|
||||
val s: Int = miniSource.offsetOf(d.range.start)
|
||||
val e: Int = miniSource.offsetOf(d.range.end)
|
||||
if (offset >= s && offset <= e) {
|
||||
// For enum constant, we don't have detailed docs in MiniAst yet, but we can render a title
|
||||
return "<div class='doc-title'>enum constant ${d.name}.${name}</div>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check parameters
|
||||
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||
for (p in fn.params) {
|
||||
if (p.name == name && source.offsetOf(p.nameStart) <= offset && source.offsetOf(p.nameStart) + p.name.length > offset) {
|
||||
return renderParamDoc(fn, p)
|
||||
mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
|
||||
fn.params.forEach { p ->
|
||||
if (p.name == name) {
|
||||
val s: Int = miniSource.offsetOf(p.nameStart)
|
||||
if (s <= offset && s + p.name.length > offset) {
|
||||
return renderParamDoc(fn, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -156,62 +162,75 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
|
||||
if (sym != null) {
|
||||
// Find local declaration that matches this symbol
|
||||
val ds = mini.declarations.firstOrNull { decl ->
|
||||
val s = source.offsetOf(decl.nameStart)
|
||||
decl.name == sym.name && s == sym.declStart
|
||||
var dsFound: MiniDecl? = null
|
||||
mini.declarations.forEach { decl ->
|
||||
if (decl.name == sym.name) {
|
||||
val sOffset: Int = miniSource.offsetOf(decl.nameStart)
|
||||
if (sOffset == sym.declStart) {
|
||||
dsFound = decl
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ds != null) return renderDeclDoc(ds)
|
||||
if (dsFound != null) return renderDeclDoc(dsFound)
|
||||
|
||||
// Check parameters
|
||||
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) {
|
||||
for (p in fn.params) {
|
||||
val s = source.offsetOf(p.nameStart)
|
||||
if (p.name == sym.name && s == sym.declStart) {
|
||||
return renderParamDoc(fn, p)
|
||||
mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
|
||||
fn.params.forEach { p ->
|
||||
if (p.name == sym.name) {
|
||||
val sOffset: Int = miniSource.offsetOf(p.nameStart)
|
||||
if (sOffset == sym.declStart) {
|
||||
return renderParamDoc(fn, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check class members (fields/functions)
|
||||
for (cls in mini.declarations.filterIsInstance<MiniClassDecl>()) {
|
||||
for (m in cls.members) {
|
||||
val s = source.offsetOf(m.nameStart)
|
||||
if (m.name == sym.name && s == sym.declStart) {
|
||||
return when (m) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(cls.name, m)
|
||||
is MiniInitDecl -> null
|
||||
mini.declarations.filterIsInstance<MiniClassDecl>().forEach { cls ->
|
||||
cls.members.forEach { m ->
|
||||
if (m.name == sym.name) {
|
||||
val sOffset: Int = miniSource.offsetOf(m.nameStart)
|
||||
if (sOffset == sym.declStart) {
|
||||
return when (m) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(cls.name, m)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (cf in cls.ctorFields) {
|
||||
val s = source.offsetOf(cf.nameStart)
|
||||
if (cf.name == sym.name && s == sym.declStart) {
|
||||
// Render as a member val
|
||||
val mv = MiniMemberValDecl(
|
||||
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
return renderMemberValDoc(cls.name, mv)
|
||||
cls.ctorFields.forEach { cf ->
|
||||
if (cf.name == sym.name) {
|
||||
val sOffset: Int = miniSource.offsetOf(cf.nameStart)
|
||||
if (sOffset == sym.declStart) {
|
||||
// Render as a member val
|
||||
val mv = MiniMemberValDecl(
|
||||
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
return renderMemberValDoc(cls.name, mv)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (cf in cls.classFields) {
|
||||
val s = source.offsetOf(cf.nameStart)
|
||||
if (cf.name == sym.name && s == sym.declStart) {
|
||||
// Render as a member val
|
||||
val mv = MiniMemberValDecl(
|
||||
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
return renderMemberValDoc(cls.name, mv)
|
||||
cls.classFields.forEach { cf ->
|
||||
if (cf.name == sym.name) {
|
||||
val sOffset: Int = miniSource.offsetOf(cf.nameStart)
|
||||
if (sOffset == sym.declStart) {
|
||||
// Render as a member val
|
||||
val mv = MiniMemberValDecl(
|
||||
range = MiniRange(cf.nameStart, cf.nameStart), // dummy
|
||||
name = cf.name,
|
||||
mutable = cf.mutable,
|
||||
type = cf.type,
|
||||
doc = null,
|
||||
nameStart = cf.nameStart
|
||||
)
|
||||
return renderMemberValDoc(cls.name, mv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -260,30 +279,30 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
} else null
|
||||
}
|
||||
else -> {
|
||||
val guessed = DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules)
|
||||
if (guessed != null) guessed
|
||||
else {
|
||||
// handle this@Type or as Type
|
||||
val i2 = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
if (i2 >= 0) {
|
||||
val identRange = TextCtx.wordRangeAt(text, i2 + 1)
|
||||
if (identRange != null) {
|
||||
val id = text.substring(identRange.startOffset, identRange.endOffset)
|
||||
val k = TextCtx.prevNonWs(text, identRange.startOffset - 1)
|
||||
if (k >= 1 && text[k] == 's' && text[k-1] == 'a' && (k-1 == 0 || !text[k-2].isLetterOrDigit())) {
|
||||
id
|
||||
} else if (k >= 0 && text[k] == '@') {
|
||||
val k2 = TextCtx.prevNonWs(text, k - 1)
|
||||
if (k2 >= 3 && text.substring(k2 - 3, k2 + 1) == "this") id else null
|
||||
DocLookupUtils.guessReceiverClassViaMini(mini, text, dotPos, importedModules)
|
||||
?: DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules, mini)
|
||||
?: run {
|
||||
// handle this@Type or as Type
|
||||
val i2 = TextCtx.prevNonWs(text, dotPos - 1)
|
||||
if (i2 >= 0) {
|
||||
val identRange = TextCtx.wordRangeAt(text, i2 + 1)
|
||||
if (identRange != null) {
|
||||
val id = text.substring(identRange.startOffset, identRange.endOffset)
|
||||
val k = TextCtx.prevNonWs(text, identRange.startOffset - 1)
|
||||
if (k >= 1 && text[k] == 's' && text[k - 1] == 'a' && (k - 1 == 0 || !text[k - 2].isLetterOrDigit())) {
|
||||
id
|
||||
} else if (k >= 0 && text[k] == '@') {
|
||||
val k2 = TextCtx.prevNonWs(text, k - 1)
|
||||
if (k2 >= 3 && text.substring(k2 - 3, k2 + 1) == "this") id else null
|
||||
} else null
|
||||
} else null
|
||||
} else null
|
||||
} else null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: memberCtx dotPos=${dotPos} chBeforeDot='${if (dotPos>0) text[dotPos-1] else ' '}' classGuess=${className} imports=${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) ->
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, className, ident, mini)?.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)
|
||||
@ -339,7 +358,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
val lhs = previousWordBefore(text, idRange.startOffset)
|
||||
if (lhs != null && hasDotBetween(text, lhs.endOffset, idRange.startOffset)) {
|
||||
val className = text.substring(lhs.startOffset, lhs.endOffset)
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, className, ident)?.let { (owner, member) ->
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, className, ident, mini)?.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)
|
||||
@ -355,10 +374,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
if (dotPos != null) {
|
||||
val guessed = when {
|
||||
looksLikeListLiteralBefore(text, dotPos) -> "List"
|
||||
else -> DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules)
|
||||
else -> DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules, mini)
|
||||
}
|
||||
if (guessed != null) {
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, guessed, ident)?.let { (owner, member) ->
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, guessed, ident, mini)?.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)
|
||||
@ -371,7 +390,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
run {
|
||||
val candidates = listOf("String", "Iterable", "Iterator", "List", "Collection", "Array", "Dict", "Regex")
|
||||
for (c in candidates) {
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, c, ident)?.let { (owner, member) ->
|
||||
DocLookupUtils.resolveMemberWithInheritance(importedModules, c, ident, mini)?.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)
|
||||
@ -383,7 +402,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
// As a last resort try aggregated String members (extensions from stdlib text)
|
||||
run {
|
||||
val classes = DocLookupUtils.aggregateClasses(importedModules)
|
||||
val classes = DocLookupUtils.aggregateClasses(importedModules, mini)
|
||||
val stringCls = classes["String"]
|
||||
val m = stringCls?.members?.firstOrNull { it.name == ident }
|
||||
if (m != null) {
|
||||
@ -396,7 +415,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
}
|
||||
// Search across classes; prefer Iterable, then Iterator, then List for common ops
|
||||
DocLookupUtils.findMemberAcrossClasses(importedModules, ident)?.let { (owner, member) ->
|
||||
DocLookupUtils.findMemberAcrossClasses(importedModules, ident, mini)?.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)
|
||||
@ -609,10 +628,14 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
}
|
||||
|
||||
private fun previousWordBefore(text: String, offset: Int): TextRange? {
|
||||
// skip spaces and dots to the left, but stop after hitting a non-identifier or dot boundary
|
||||
// skip spaces and the dot to the left, but stop after hitting a non-identifier boundary
|
||||
var i = (offset - 1).coerceAtLeast(0)
|
||||
// first, move left past spaces
|
||||
while (i > 0 && text[i].isWhitespace()) i--
|
||||
// skip trailing spaces
|
||||
while (i >= 0 && text[i].isWhitespace()) i--
|
||||
// skip the dot if present
|
||||
if (i >= 0 && text[i] == '.') i--
|
||||
// skip spaces before the dot
|
||||
while (i >= 0 && text[i].isWhitespace()) i--
|
||||
// remember position to check for dot between words
|
||||
val end = i + 1
|
||||
// now find the start of the identifier
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
package net.sergeych.lyng.idea.util
|
||||
|
||||
import com.intellij.openapi.application.runReadAction
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiManager
|
||||
@ -33,14 +34,15 @@ object LyngAstManager {
|
||||
private val BINDING_KEY = Key.create<BindingSnapshot>("lyng.binding.cache")
|
||||
private val STAMP_KEY = Key.create<Long>("lyng.mini.cache.stamp")
|
||||
|
||||
fun getMiniAst(file: PsiFile): MiniScript? {
|
||||
val doc = file.viewProvider.document ?: return null
|
||||
val stamp = doc.modificationStamp
|
||||
fun getMiniAst(file: PsiFile): MiniScript? = runReadAction {
|
||||
val vFile = file.virtualFile ?: return@runReadAction null
|
||||
val combinedStamp = getCombinedStamp(file)
|
||||
|
||||
val prevStamp = file.getUserData(STAMP_KEY)
|
||||
val cached = file.getUserData(MINI_KEY)
|
||||
if (cached != null && prevStamp != null && prevStamp == stamp) return cached
|
||||
if (cached != null && prevStamp != null && prevStamp == combinedStamp) return@runReadAction cached
|
||||
|
||||
val text = doc.text
|
||||
val text = file.viewProvider.contents.toString()
|
||||
val sink = MiniAstBuilder()
|
||||
val built = try {
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
@ -48,7 +50,14 @@ object LyngAstManager {
|
||||
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
||||
val script = sink.build()
|
||||
if (script != null && !file.name.endsWith(".lyng.d")) {
|
||||
mergeDeclarationFiles(file, script)
|
||||
val dFiles = collectDeclarationFiles(file)
|
||||
for (df in dFiles) {
|
||||
val scriptD = getMiniAst(df)
|
||||
if (scriptD != null) {
|
||||
script.declarations.addAll(scriptD.declarations)
|
||||
script.imports.addAll(scriptD.imports)
|
||||
}
|
||||
}
|
||||
}
|
||||
script
|
||||
} catch (_: Throwable) {
|
||||
@ -57,53 +66,68 @@ object LyngAstManager {
|
||||
|
||||
if (built != null) {
|
||||
file.putUserData(MINI_KEY, built)
|
||||
file.putUserData(STAMP_KEY, stamp)
|
||||
file.putUserData(STAMP_KEY, combinedStamp)
|
||||
// Invalidate binding too
|
||||
file.putUserData(BINDING_KEY, null)
|
||||
}
|
||||
return built
|
||||
built
|
||||
}
|
||||
|
||||
private fun mergeDeclarationFiles(file: PsiFile, mainScript: MiniScript) {
|
||||
fun getCombinedStamp(file: PsiFile): Long = runReadAction {
|
||||
var combinedStamp = file.viewProvider.modificationStamp
|
||||
if (!file.name.endsWith(".lyng.d")) {
|
||||
collectDeclarationFiles(file).forEach { df ->
|
||||
combinedStamp += df.viewProvider.modificationStamp
|
||||
}
|
||||
}
|
||||
combinedStamp
|
||||
}
|
||||
|
||||
private fun collectDeclarationFiles(file: PsiFile): List<PsiFile> = runReadAction {
|
||||
val psiManager = PsiManager.getInstance(file.project)
|
||||
var current = file.virtualFile?.parent
|
||||
val seen = mutableSetOf<String>()
|
||||
val result = mutableListOf<PsiFile>()
|
||||
|
||||
while (current != null) {
|
||||
for (child in current.children) {
|
||||
if (child.name.endsWith(".lyng.d") && child != file.virtualFile && seen.add(child.path)) {
|
||||
val psiD = psiManager.findFile(child) ?: continue
|
||||
val scriptD = getMiniAst(psiD)
|
||||
if (scriptD != null) {
|
||||
mainScript.declarations.addAll(scriptD.declarations)
|
||||
mainScript.imports.addAll(scriptD.imports)
|
||||
}
|
||||
result.add(psiD)
|
||||
}
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fun getBinding(file: PsiFile): BindingSnapshot? {
|
||||
val doc = file.viewProvider.document ?: return null
|
||||
val stamp = doc.modificationStamp
|
||||
fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction {
|
||||
val vFile = file.virtualFile ?: return@runReadAction null
|
||||
var combinedStamp = file.viewProvider.modificationStamp
|
||||
|
||||
val dFiles = if (!file.name.endsWith(".lyng.d")) collectDeclarationFiles(file) else emptyList()
|
||||
for (df in dFiles) {
|
||||
combinedStamp += df.viewProvider.modificationStamp
|
||||
}
|
||||
|
||||
val prevStamp = file.getUserData(STAMP_KEY)
|
||||
val cached = file.getUserData(BINDING_KEY)
|
||||
|
||||
if (cached != null && prevStamp != null && prevStamp == stamp) return cached
|
||||
|
||||
val mini = getMiniAst(file) ?: return null
|
||||
val text = doc.text
|
||||
|
||||
if (cached != null && prevStamp != null && prevStamp == combinedStamp) return@runReadAction cached
|
||||
|
||||
val mini = getMiniAst(file) ?: return@runReadAction null
|
||||
val text = file.viewProvider.contents.toString()
|
||||
val binding = try {
|
||||
Binder.bind(text, mini)
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
|
||||
if (binding != null) {
|
||||
file.putUserData(BINDING_KEY, binding)
|
||||
// stamp is already set by getMiniAst
|
||||
// stamp is already set by getMiniAst or we set it here if getMiniAst was cached
|
||||
file.putUserData(STAMP_KEY, combinedStamp)
|
||||
}
|
||||
return binding
|
||||
binding
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<!-- Open-ended compatibility: 2024.3+ (build 243 and newer) -->
|
||||
<idea-version since-build="243"/>
|
||||
<id>net.sergeych.lyng.idea</id>
|
||||
<name>Lyng Language Support</name>
|
||||
<name>Lyng</name>
|
||||
<vendor email="real.sergeych@gmail.com">Sergey Chernov</vendor>
|
||||
|
||||
<description>
|
||||
@ -42,7 +42,8 @@
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<!-- Language and file type -->
|
||||
<fileType implementationClass="net.sergeych.lyng.idea.LyngFileType" name="Lyng" extensions="lyng;lyng.d" fieldName="INSTANCE" language="Lyng"/>
|
||||
<fileType implementationClass="net.sergeych.lyng.idea.LyngFileType" name="Lyng" extensions="lyng" fieldName="INSTANCE" language="Lyng"/>
|
||||
<fileTypeFactory implementation="net.sergeych.lyng.idea.LyngFileTypeFactory"/>
|
||||
|
||||
<!-- Minimal parser/PSI to fully wire editor services for the language -->
|
||||
<lang.parserDefinition language="Lyng" implementationClass="net.sergeych.lyng.idea.psi.LyngParserDefinition"/>
|
||||
|
||||
@ -2936,7 +2936,9 @@ class Compiler(
|
||||
returnType = returnTypeMini,
|
||||
body = bodyRange?.let { MiniBlock(it) },
|
||||
doc = declDocLocal,
|
||||
nameStart = nameStartPos
|
||||
nameStart = nameStartPos,
|
||||
receiver = receiverMini,
|
||||
isExtern = actualExtern
|
||||
)
|
||||
miniSink?.onFunDecl(node)
|
||||
}
|
||||
|
||||
@ -58,11 +58,11 @@ object CompletionEngineLight {
|
||||
return completeSuspend(text, idx)
|
||||
}
|
||||
|
||||
suspend fun completeSuspend(text: String, caret: Int): List<CompletionItem> {
|
||||
suspend fun completeSuspend(text: String, caret: Int, providedMini: MiniScript? = null): List<CompletionItem> {
|
||||
// Ensure stdlib Obj*-defined docs (e.g., String methods) are initialized before registry lookup
|
||||
StdlibDocsBootstrap.ensure()
|
||||
val prefix = prefixAt(text, caret)
|
||||
val mini = buildMiniAst(text)
|
||||
val mini = providedMini ?: buildMiniAst(text)
|
||||
val imported: List<String> = DocLookupUtils.canonicalImportedModules(mini ?: return emptyList(), text)
|
||||
|
||||
val cap = 200
|
||||
|
||||
@ -430,6 +430,7 @@ object DocLookupUtils {
|
||||
|
||||
fun scanLocalClassMembersFromText(mini: MiniScript, text: String, cls: MiniClassDecl): Map<String, ScannedSig> {
|
||||
val src = mini.range.start.source
|
||||
if (cls.nameStart.source != src) return emptyMap()
|
||||
val start = src.offsetOf(cls.bodyRange?.start ?: cls.range.start)
|
||||
val end = src.offsetOf(cls.bodyRange?.end ?: cls.range.end).coerceAtMost(text.length)
|
||||
if (start !in 0..end) return emptyMap()
|
||||
|
||||
@ -273,4 +273,42 @@ class MiniAstTest {
|
||||
assertNotNull(e1)
|
||||
assertEquals("Doc6", e1.doc?.summary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun miniAst_captures_user_sample_extern_doc() = runTest {
|
||||
val code = """
|
||||
/*
|
||||
the plugin testing .d sample
|
||||
*/
|
||||
extern fun test(value: Int): String
|
||||
""".trimIndent()
|
||||
val (_, sink) = compileWithMini(code)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini)
|
||||
val test = mini.declarations.filterIsInstance<MiniFunDecl>().firstOrNull { it.name == "test" }
|
||||
assertNotNull(test, "function 'test' should be captured")
|
||||
assertNotNull(test.doc, "doc for 'test' should be captured")
|
||||
assertEquals("the plugin testing .d sample", test.doc.summary)
|
||||
assertTrue(test.isExtern, "function 'test' should be extern")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolve_object_member_doc() = runTest {
|
||||
val code = """
|
||||
object O3 {
|
||||
/* doc for name */
|
||||
fun name() = "ozone"
|
||||
}
|
||||
""".trimIndent()
|
||||
val (_, sink) = compileWithMini(code)
|
||||
val mini = sink.build()
|
||||
assertNotNull(mini)
|
||||
|
||||
val imported = listOf("lyng.stdlib")
|
||||
// Simulate looking up O3.name
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, "O3", "name", mini)
|
||||
assertNotNull(resolved)
|
||||
assertEquals("O3", resolved.first)
|
||||
assertEquals("doc for name", resolved.second.doc?.summary)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user