plugin no wworks with .lyng.d files. hurray

This commit is contained in:
Sergey Chernov 2026-01-06 19:32:40 +01:00
parent fdc044d1e0
commit aba0048a83
10 changed files with 400 additions and 271 deletions

View File

@ -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"))
}
}

View File

@ -25,9 +25,6 @@ import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile 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.Source
import net.sergeych.lyng.binding.Binder import net.sergeych.lyng.binding.Binder
import net.sergeych.lyng.binding.SymbolKind 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.SimpleLyngHighlighter
import net.sergeych.lyng.highlight.offsetOf import net.sergeych.lyng.highlight.offsetOf
import net.sergeych.lyng.idea.highlight.LyngHighlighterColors 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.* import net.sergeych.lyng.miniast.*
/** /**
@ -43,7 +40,7 @@ import net.sergeych.lyng.miniast.*
* and applies semantic highlighting comparable with the web highlighter. * and applies semantic highlighting comparable with the web highlighter.
*/ */
class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, LyngExternalAnnotator.Result>() { 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 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) 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? { override fun collectInformation(file: PsiFile): Input? {
val doc: Document = file.viewProvider.document ?: return null val doc: Document = file.viewProvider.document ?: return null
val cached = file.getUserData(CACHE_KEY) val cached = file.getUserData(CACHE_KEY)
// Fast fix (1): reuse cached spans only if they were computed for the same modification stamp val combinedStamp = LyngAstManager.getCombinedStamp(file)
val prev = if (cached != null && cached.modStamp == doc.modificationStamp) cached.spans else null
return Input(doc.text, doc.modificationStamp, prev) 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? { override fun doAnnotate(collectedInfo: Input?): Result? {
if (collectedInfo == null) return null if (collectedInfo == null) return null
ProgressManager.checkCanceled() ProgressManager.checkCanceled()
val text = collectedInfo.text val text = collectedInfo.text
// Build Mini-AST using the same mechanism as web highlighter
val sink = MiniAstBuilder() // Use LyngAstManager to get the (potentially merged) Mini-AST
val source = Source("<ide>", text) val mini = LyngAstManager.getMiniAst(collectedInfo.file)
try { ?: return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList())
// 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)
}
ProgressManager.checkCanceled() 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) val out = ArrayList<Span>(256)
fun isFollowedByParenOrBlock(rangeEnd: Int): Boolean { fun isFollowedByParenOrBlock(rangeEnd: Int): Boolean {
@ -118,7 +96,8 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
} }
// Declarations // Declarations
for (d in mini.declarations) { mini.declarations.forEach { d ->
if (d.nameStart.source != source) return@forEach
when (d) { when (d) {
is MiniFunDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.FUNCTION_DECLARATION) is MiniFunDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.FUNCTION_DECLARATION)
is MiniClassDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE) 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 // Imports: each segment as namespace/path
for (imp in mini.imports) { mini.imports.forEach { imp ->
for (seg in imp.segments) putMiniRange(seg.range, LyngHighlighterColors.NAMESPACE) if (imp.range.start.source != source) return@forEach
imp.segments.forEach { seg -> putMiniRange(seg.range, LyngHighlighterColors.NAMESPACE) }
} }
// Parameters // Parameters
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) { mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
for (p in fn.params) putName(p.nameStart, p.name, LyngHighlighterColors.PARAMETER) 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) // Type name segments (including generics base & args)
fun addTypeSegments(t: MiniTypeRef?) { fun addTypeSegments(t: MiniTypeRef?) {
when (t) { when (t) {
is MiniTypeName -> t.segments.forEach { seg -> is MiniTypeName -> t.segments.forEach { seg ->
if (seg.range.start.source != source) return@forEach
val s = source.offsetOf(seg.range.start) val s = source.offsetOf(seg.range.start)
putRange(s, (s + seg.name.length).coerceAtMost(text.length), LyngHighlighterColors.TYPE) putRange(s, (s + seg.name.length).coerceAtMost(text.length), LyngHighlighterColors.TYPE)
} }
@ -158,12 +140,14 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
addTypeSegments(t.returnType) addTypeSegments(t.returnType)
} }
is MiniTypeVar -> { /* name is in range; could be highlighted as TYPE as well */ 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 -> {} null -> {}
} }
} }
for (d in mini.declarations) { mini.declarations.forEach { d ->
if (d.nameStart.source != source) return@forEach
when (d) { when (d) {
is MiniFunDecl -> { is MiniFunDecl -> {
addTypeSegments(d.returnType) addTypeSegments(d.returnType)
@ -190,7 +174,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
// Map declaration ranges to avoid duplicating them as usages // Map declaration ranges to avoid duplicating them as usages
val declKeys = HashSet<Pair<Int, Int>>(binding.symbols.size * 2) 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) { fun keyForKind(k: SymbolKind) = when (k) {
SymbolKind.Function -> LyngHighlighterColors.FUNCTION SymbolKind.Function -> LyngHighlighterColors.FUNCTION
@ -203,13 +187,16 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
// Track covered ranges to not override later heuristics // Track covered ranges to not override later heuristics
val covered = HashSet<Pair<Int, Int>>() val covered = HashSet<Pair<Int, Int>>()
for (ref in binding.references) { binding.references.forEach { ref ->
val key = ref.start to ref.end val key = ref.start to ref.end
if (declKeys.contains(key)) continue if (!declKeys.contains(key)) {
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId } ?: continue val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
val color = keyForKind(sym.kind) if (sym != null) {
putRange(ref.start, ref.end, color) val color = keyForKind(sym.kind)
covered += key putRange(ref.start, ref.end, color)
covered += key
}
}
} }
// Heuristics on top of binder: function call-sites and simple name-based roles // 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 // Build simple name -> role map for top-level vals/vars and parameters
val nameRole = HashMap<String, com.intellij.openapi.editor.colors.TextAttributesKey>(8) val nameRole = HashMap<String, com.intellij.openapi.editor.colors.TextAttributesKey>(8)
for (d in mini.declarations) when (d) { mini.declarations.forEach { d ->
is MiniValDecl -> nameRole[d.name] = if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE when (d) {
is MiniFunDecl -> d.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER } is MiniValDecl -> nameRole[d.name] =
else -> {} 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) { tokens.forEach { s ->
val start = s.range.start if (s.kind == HighlightKind.Identifier) {
val end = s.range.endExclusive val start = s.range.start
val key = start to end val end = s.range.endExclusive
if (key in covered || key in declKeys) continue val key = start to end
if (key !in covered && key !in declKeys) {
// Call-site detection first so it wins over var/param role // Call-site detection first so it wins over var/param role
if (isFollowedByParenOrBlock(end)) { if (isFollowedByParenOrBlock(end)) {
putRange(start, end, LyngHighlighterColors.FUNCTION) putRange(start, end, LyngHighlighterColors.FUNCTION)
covered += key covered += key
continue } else {
} // Simple role by known names
val ident = try {
// Simple role by known names text.substring(start, end)
val ident = try { text.substring(start, end) } catch (_: Throwable) { null } } catch (_: Throwable) {
if (ident != null) { null
val roleKey = nameRole[ident] }
if (roleKey != null) { if (ident != null) {
putRange(start, end, roleKey) val roleKey = nameRole[ident]
covered += key 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 // Add annotation/label coloring using token highlighter
run { run {
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() } val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
for (s in tokens) if (s.kind == HighlightKind.Label) { tokens.forEach { s ->
val start = s.range.start if (s.kind == HighlightKind.Label) {
val end = s.range.endExclusive val start = s.range.start
if (start in 0..end && end <= text.length && start < end) { val end = s.range.endExclusive
val lexeme = try { text.substring(start, end) } catch (_: Throwable) { null } if (start in 0..end && end <= text.length && start < end) {
if (lexeme != null) { val lexeme = try {
// Heuristic: if it starts with @ and follows a control keyword, it's likely a label text.substring(start, end)
// Otherwise if it starts with @ it's an annotation. } catch (_: Throwable) {
// If it ends with @ it's a loop label. null
when { }
lexeme.endsWith("@") -> putRange(start, end, LyngHighlighterColors.LABEL) if (lexeme != null) {
lexeme.startsWith("@") -> { // Heuristic: if it starts with @ and follows a control keyword, it's likely a label
// Try to see if it's an exit label // Otherwise if it starts with @ it's an annotation.
val prevNonWs = prevNonWs(text, start) // If it ends with @ it's a loop label.
val prevWord = if (prevNonWs >= 0) { when {
var wEnd = prevNonWs + 1 lexeme.endsWith("@") -> putRange(start, end, LyngHighlighterColors.LABEL)
var wStart = prevNonWs lexeme.startsWith("@") -> {
while (wStart > 0 && text[wStart - 1].isLetter()) wStart-- // Try to see if it's an exit label
text.substring(wStart, wEnd) val prevNonWs = prevNonWs(text, start)
} else null val prevWord = if (prevNonWs >= 0) {
var wEnd = prevNonWs + 1
if (prevWord in setOf("return", "break", "continue") || isFollowedByParenOrBlock(end)) { var wStart = prevNonWs
putRange(start, end, LyngHighlighterColors.LABEL) while (wStart > 0 && text[wStart - 1].isLetter()) wStart--
} else { text.substring(wStart, wEnd)
putRange(start, end, LyngHighlighterColors.ANNOTATION) } 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 // Map Enum constants from token highlighter to IDEA enum constant color
run { run {
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() } val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
for (s in tokens) if (s.kind == HighlightKind.EnumConstant) { tokens.forEach { s ->
val start = s.range.start if (s.kind == HighlightKind.EnumConstant) {
val end = s.range.endExclusive val start = s.range.start
if (start in 0..end && end <= text.length && start < end) { val end = s.range.endExclusive
putRange(start, end, LyngHighlighterColors.ENUM_CONSTANT) 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>() val idRanges = mutableSetOf<IntRange>()
try { try {
val binding = Binder.bind(text, mini) val binding = Binder.bind(text, mini)
for (sym in binding.symbols) { binding.symbols.forEach { sym ->
val s = sym.declStart; val e = sym.declEnd val s = sym.declStart
val e = sym.declEnd
if (s in 0..e && e <= text.length && s < e) idRanges += (s until e) if (s in 0..e && e <= text.length && s < e) idRanges += (s until e)
} }
for (ref in binding.references) { binding.references.forEach { ref ->
val s = ref.start; val e = ref.end val s = ref.start
val e = ref.end
if (s in 0..e && e <= text.length && s < e) idRanges += (s until e) if (s in 0..e && e <= text.length && s < e) idRanges += (s until e)
} }
} catch (_: Throwable) { } catch (_: Throwable) {
@ -329,12 +335,13 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
override fun apply(file: PsiFile, annotationResult: Result?, holder: AnnotationHolder) { override fun apply(file: PsiFile, annotationResult: Result?, holder: AnnotationHolder) {
if (annotationResult == null) return if (annotationResult == null) return
// Skip if cache is up-to-date // Skip if cache is up-to-date
val doc = file.viewProvider.document val combinedStamp = LyngAstManager.getCombinedStamp(file)
val currentStamp = doc?.modificationStamp
val cached = file.getUserData(CACHE_KEY) 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) file.putUserData(CACHE_KEY, result)
val doc = file.viewProvider.document
// Store spell index for spell/grammar engines to consume (suspend until ready) // Store spell index for spell/grammar engines to consume (suspend until ready)
val ids = result.spellIdentifiers.map { TextRange(it.first, it.last + 1) } val ids = result.spellIdentifiers.map { TextRange(it.first, it.last + 1) }
val coms = result.spellComments.map { TextRange(it.first, it.last + 1) } val coms = result.spellComments.map { TextRange(it.first, it.last + 1) }

View File

@ -102,7 +102,7 @@ class LyngCompletionContributor : CompletionContributor() {
// Delegate computation to the shared engine to keep behavior in sync with tests // Delegate computation to the shared engine to keep behavior in sync with tests
val engineItems = try { val engineItems = try {
runBlocking { CompletionEngineLight.completeSuspend(text, caret) } runBlocking { CompletionEngineLight.completeSuspend(text, caret, mini) }
} catch (t: Throwable) { } catch (t: Throwable) {
if (DEBUG_COMPLETION) log.warn("[LYNG_DEBUG] Engine completion failed: ${t.message}") if (DEBUG_COMPLETION) log.warn("[LYNG_DEBUG] Engine completion failed: ${t.message}")
emptyList() emptyList()
@ -452,7 +452,7 @@ class LyngCompletionContributor : CompletionContributor() {
for (name in common) { for (name in common) {
if (name in already) continue if (name in already) continue
// Try resolve across classes first to get types/params; if it fails, emit a synthetic safe suggestion. // 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) { if (resolved != null) {
val member = resolved.second val member = resolved.second
when (member) { when (member) {
@ -502,7 +502,7 @@ class LyngCompletionContributor : CompletionContributor() {
for (name in ext) { for (name in ext) {
if (already.contains(name)) continue if (already.contains(name)) continue
// Try to resolve full signature via registry first to get params and return type // 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) { if (resolved != null) {
when (val member = resolved.second) { when (val member = resolved.second) {
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {

View File

@ -24,13 +24,9 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile 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.highlight.offsetOf
import net.sergeych.lyng.idea.LyngLanguage 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.idea.util.TextCtx
import net.sergeych.lyng.miniast.* import net.sergeych.lyng.miniast.*
@ -69,80 +65,90 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
val ident = text.substring(idRange.startOffset, idRange.endOffset) 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}") 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. // 1. Get merged mini-AST from Manager (handles local + .lyng.d merged declarations)
val sink = MiniAstBuilder() val mini = LyngAstManager.getMiniAst(file) ?: return null
val provider = IdeLenientImportProvider.create() val miniSource = mini.range.start.source
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
// 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) Use unified declaration detection // 1) Use unified declaration detection
DocLookupUtils.findDeclarationAt(mini, offset, ident)?.let { (name, kind) -> DocLookupUtils.findDeclarationAt(mini, offset, ident)?.let { (name, kind) ->
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: matched declaration '$name' kind=$kind") if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: matched declaration '$name' kind=$kind")
// Find the actual declaration object to render // Find the actual declaration object to render
for (d in mini.declarations) { mini.declarations.forEach { d ->
if (d.name == name && source.offsetOf(d.nameStart) <= offset && source.offsetOf(d.nameStart) + d.name.length > offset) { if (d.name == name) {
return renderDeclDoc(d) 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 // Handle members if it was a member
if (d is MiniClassDecl) { if (d is MiniClassDecl) {
for (m in d.members) { d.members.forEach { m ->
if (m.name == name && source.offsetOf(m.nameStart) <= offset && source.offsetOf(m.nameStart) + m.name.length > offset) { if (m.name == name) {
return when (m) { val s: Int = miniSource.offsetOf(m.nameStart)
is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m) if (s <= offset && s + m.name.length > offset) {
is MiniMemberValDecl -> renderMemberValDoc(d.name, m) return when (m) {
is MiniInitDecl -> null is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m)
is MiniMemberValDecl -> renderMemberValDoc(d.name, m)
else -> null
}
} }
} }
} }
for (cf in d.ctorFields) { d.ctorFields.forEach { cf ->
if (cf.name == name && source.offsetOf(cf.nameStart) <= offset && source.offsetOf(cf.nameStart) + cf.name.length > offset) { if (cf.name == name) {
// Render as a member val val s: Int = miniSource.offsetOf(cf.nameStart)
val mv = MiniMemberValDecl( if (s <= offset && s + cf.name.length > offset) {
range = MiniRange(cf.nameStart, cf.nameStart), // dummy // Render as a member val
name = cf.name, val mv = MiniMemberValDecl(
mutable = cf.mutable, range = MiniRange(cf.nameStart, cf.nameStart), // dummy
type = cf.type, name = cf.name,
doc = null, mutable = cf.mutable,
nameStart = cf.nameStart type = cf.type,
) doc = null,
return renderMemberValDoc(d.name, mv) nameStart = cf.nameStart
)
return renderMemberValDoc(d.name, mv)
}
} }
} }
for (cf in d.classFields) { d.classFields.forEach { cf ->
if (cf.name == name && source.offsetOf(cf.nameStart) <= offset && source.offsetOf(cf.nameStart) + cf.name.length > offset) { if (cf.name == name) {
// Render as a member val val s: Int = miniSource.offsetOf(cf.nameStart)
val mv = MiniMemberValDecl( if (s <= offset && s + cf.name.length > offset) {
range = MiniRange(cf.nameStart, cf.nameStart), // dummy // Render as a member val
name = cf.name, val mv = MiniMemberValDecl(
mutable = cf.mutable, range = MiniRange(cf.nameStart, cf.nameStart), // dummy
type = cf.type, name = cf.name,
doc = null, mutable = cf.mutable,
nameStart = cf.nameStart type = cf.type,
) doc = null,
return renderMemberValDoc(d.name, mv) nameStart = cf.nameStart
)
return renderMemberValDoc(d.name, mv)
}
} }
} }
} }
if (d is MiniEnumDecl) { if (d is MiniEnumDecl) {
if (d.entries.contains(name) && offset >= source.offsetOf(d.range.start) && offset <= source.offsetOf(d.range.end)) { if (d.entries.contains(name)) {
// For enum constant, we don't have detailed docs in MiniAst yet, but we can render a title val s: Int = miniSource.offsetOf(d.range.start)
return "<div class='doc-title'>enum constant ${d.name}.${name}</div>" 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 // Check parameters
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) { mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
for (p in fn.params) { fn.params.forEach { p ->
if (p.name == name && source.offsetOf(p.nameStart) <= offset && source.offsetOf(p.nameStart) + p.name.length > offset) { if (p.name == name) {
return renderParamDoc(fn, p) 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 } val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
if (sym != null) { if (sym != null) {
// Find local declaration that matches this symbol // Find local declaration that matches this symbol
val ds = mini.declarations.firstOrNull { decl -> var dsFound: MiniDecl? = null
val s = source.offsetOf(decl.nameStart) mini.declarations.forEach { decl ->
decl.name == sym.name && s == sym.declStart 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 // Check parameters
for (fn in mini.declarations.filterIsInstance<MiniFunDecl>()) { mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
for (p in fn.params) { fn.params.forEach { p ->
val s = source.offsetOf(p.nameStart) if (p.name == sym.name) {
if (p.name == sym.name && s == sym.declStart) { val sOffset: Int = miniSource.offsetOf(p.nameStart)
return renderParamDoc(fn, p) if (sOffset == sym.declStart) {
return renderParamDoc(fn, p)
}
} }
} }
} }
// Check class members (fields/functions) // Check class members (fields/functions)
for (cls in mini.declarations.filterIsInstance<MiniClassDecl>()) { mini.declarations.filterIsInstance<MiniClassDecl>().forEach { cls ->
for (m in cls.members) { cls.members.forEach { m ->
val s = source.offsetOf(m.nameStart) if (m.name == sym.name) {
if (m.name == sym.name && s == sym.declStart) { val sOffset: Int = miniSource.offsetOf(m.nameStart)
return when (m) { if (sOffset == sym.declStart) {
is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m) return when (m) {
is MiniMemberValDecl -> renderMemberValDoc(cls.name, m) is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m)
is MiniInitDecl -> null is MiniMemberValDecl -> renderMemberValDoc(cls.name, m)
else -> null
}
} }
} }
} }
for (cf in cls.ctorFields) { cls.ctorFields.forEach { cf ->
val s = source.offsetOf(cf.nameStart) if (cf.name == sym.name) {
if (cf.name == sym.name && s == sym.declStart) { val sOffset: Int = miniSource.offsetOf(cf.nameStart)
// Render as a member val if (sOffset == sym.declStart) {
val mv = MiniMemberValDecl( // Render as a member val
range = MiniRange(cf.nameStart, cf.nameStart), // dummy val mv = MiniMemberValDecl(
name = cf.name, range = MiniRange(cf.nameStart, cf.nameStart), // dummy
mutable = cf.mutable, name = cf.name,
type = cf.type, mutable = cf.mutable,
doc = null, type = cf.type,
nameStart = cf.nameStart doc = null,
) nameStart = cf.nameStart
return renderMemberValDoc(cls.name, mv) )
return renderMemberValDoc(cls.name, mv)
}
} }
} }
for (cf in cls.classFields) { cls.classFields.forEach { cf ->
val s = source.offsetOf(cf.nameStart) if (cf.name == sym.name) {
if (cf.name == sym.name && s == sym.declStart) { val sOffset: Int = miniSource.offsetOf(cf.nameStart)
// Render as a member val if (sOffset == sym.declStart) {
val mv = MiniMemberValDecl( // Render as a member val
range = MiniRange(cf.nameStart, cf.nameStart), // dummy val mv = MiniMemberValDecl(
name = cf.name, range = MiniRange(cf.nameStart, cf.nameStart), // dummy
mutable = cf.mutable, name = cf.name,
type = cf.type, mutable = cf.mutable,
doc = null, type = cf.type,
nameStart = cf.nameStart doc = null,
) nameStart = cf.nameStart
return renderMemberValDoc(cls.name, mv) )
return renderMemberValDoc(cls.name, mv)
}
} }
} }
} }
@ -260,30 +279,30 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
} else null } else null
} }
else -> { else -> {
val guessed = DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules) DocLookupUtils.guessReceiverClassViaMini(mini, text, dotPos, importedModules)
if (guessed != null) guessed ?: DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules, mini)
else { ?: run {
// handle this@Type or as Type // handle this@Type or as Type
val i2 = TextCtx.prevNonWs(text, dotPos - 1) val i2 = TextCtx.prevNonWs(text, dotPos - 1)
if (i2 >= 0) { if (i2 >= 0) {
val identRange = TextCtx.wordRangeAt(text, i2 + 1) val identRange = TextCtx.wordRangeAt(text, i2 + 1)
if (identRange != null) { if (identRange != null) {
val id = text.substring(identRange.startOffset, identRange.endOffset) val id = text.substring(identRange.startOffset, identRange.endOffset)
val k = TextCtx.prevNonWs(text, identRange.startOffset - 1) 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())) { if (k >= 1 && text[k] == 's' && text[k - 1] == 'a' && (k - 1 == 0 || !text[k - 2].isLetterOrDigit())) {
id id
} else if (k >= 0 && text[k] == '@') { } else if (k >= 0 && text[k] == '@') {
val k2 = TextCtx.prevNonWs(text, k - 1) val k2 = TextCtx.prevNonWs(text, k - 1)
if (k2 >= 3 && text.substring(k2 - 3, k2 + 1) == "this") id else null if (k2 >= 3 && text.substring(k2 - 3, k2 + 1) == "this") id else null
} else null
} 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) { 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}") if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] QuickDoc: literal/call '$ident' resolved to $owner.${member.name}")
return when (member) { return when (member) {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
@ -339,7 +358,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
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)
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}") 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)
@ -355,10 +374,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 -> DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules) else -> DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules, mini)
} }
if (guessed != null) { 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}") 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)
@ -371,7 +390,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
run { run {
val candidates = listOf("String", "Iterable", "Iterator", "List", "Collection", "Array", "Dict", "Regex") val candidates = listOf("String", "Iterable", "Iterator", "List", "Collection", "Array", "Dict", "Regex")
for (c in candidates) { 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}") if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Candidate '$c.$ident' resolved via inheritance to $owner.${member.name}")
return when (member) { return when (member) {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, 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) // As a last resort try aggregated String members (extensions from stdlib text)
run { run {
val classes = DocLookupUtils.aggregateClasses(importedModules) val classes = DocLookupUtils.aggregateClasses(importedModules, mini)
val stringCls = classes["String"] val stringCls = classes["String"]
val m = stringCls?.members?.firstOrNull { it.name == ident } val m = stringCls?.members?.firstOrNull { it.name == ident }
if (m != null) { if (m != null) {
@ -396,7 +415,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
} }
} }
// Search across classes; prefer Iterable, then Iterator, then List for common ops // Search across classes; prefer Iterable, then Iterator, then List for common ops
DocLookupUtils.findMemberAcrossClasses(importedModules, ident)?.let { (owner, member) -> DocLookupUtils.findMemberAcrossClasses(importedModules, ident, mini)?.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)
@ -609,10 +628,14 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
} }
private fun previousWordBefore(text: String, offset: Int): TextRange? { 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) var i = (offset - 1).coerceAtLeast(0)
// first, move left past spaces // skip trailing spaces
while (i > 0 && text[i].isWhitespace()) i-- 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 // remember position to check for dot between words
val end = i + 1 val end = i + 1
// now find the start of the identifier // now find the start of the identifier

View File

@ -17,6 +17,7 @@
package net.sergeych.lyng.idea.util package net.sergeych.lyng.idea.util
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager import com.intellij.psi.PsiManager
@ -33,14 +34,15 @@ object LyngAstManager {
private val BINDING_KEY = Key.create<BindingSnapshot>("lyng.binding.cache") private val BINDING_KEY = Key.create<BindingSnapshot>("lyng.binding.cache")
private val STAMP_KEY = Key.create<Long>("lyng.mini.cache.stamp") private val STAMP_KEY = Key.create<Long>("lyng.mini.cache.stamp")
fun getMiniAst(file: PsiFile): MiniScript? { fun getMiniAst(file: PsiFile): MiniScript? = runReadAction {
val doc = file.viewProvider.document ?: return null val vFile = file.virtualFile ?: return@runReadAction null
val stamp = doc.modificationStamp val combinedStamp = getCombinedStamp(file)
val prevStamp = file.getUserData(STAMP_KEY) val prevStamp = file.getUserData(STAMP_KEY)
val cached = file.getUserData(MINI_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 sink = MiniAstBuilder()
val built = try { val built = try {
val provider = IdeLenientImportProvider.create() val provider = IdeLenientImportProvider.create()
@ -48,7 +50,14 @@ object LyngAstManager {
runBlocking { Compiler.compileWithMini(src, provider, sink) } runBlocking { Compiler.compileWithMini(src, provider, sink) }
val script = sink.build() val script = sink.build()
if (script != null && !file.name.endsWith(".lyng.d")) { 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 script
} catch (_: Throwable) { } catch (_: Throwable) {
@ -57,53 +66,68 @@ object LyngAstManager {
if (built != null) { if (built != null) {
file.putUserData(MINI_KEY, built) file.putUserData(MINI_KEY, built)
file.putUserData(STAMP_KEY, stamp) file.putUserData(STAMP_KEY, combinedStamp)
// Invalidate binding too // Invalidate binding too
file.putUserData(BINDING_KEY, null) 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) val psiManager = PsiManager.getInstance(file.project)
var current = file.virtualFile?.parent var current = file.virtualFile?.parent
val seen = mutableSetOf<String>() val seen = mutableSetOf<String>()
val result = mutableListOf<PsiFile>()
while (current != null) { while (current != null) {
for (child in current.children) { for (child in current.children) {
if (child.name.endsWith(".lyng.d") && child != file.virtualFile && seen.add(child.path)) { if (child.name.endsWith(".lyng.d") && child != file.virtualFile && seen.add(child.path)) {
val psiD = psiManager.findFile(child) ?: continue val psiD = psiManager.findFile(child) ?: continue
val scriptD = getMiniAst(psiD) result.add(psiD)
if (scriptD != null) {
mainScript.declarations.addAll(scriptD.declarations)
mainScript.imports.addAll(scriptD.imports)
}
} }
} }
current = current.parent current = current.parent
} }
result
} }
fun getBinding(file: PsiFile): BindingSnapshot? { fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction {
val doc = file.viewProvider.document ?: return null val vFile = file.virtualFile ?: return@runReadAction null
val stamp = doc.modificationStamp 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 prevStamp = file.getUserData(STAMP_KEY)
val cached = file.getUserData(BINDING_KEY) val cached = file.getUserData(BINDING_KEY)
if (cached != null && prevStamp != null && prevStamp == stamp) return cached if (cached != null && prevStamp != null && prevStamp == combinedStamp) return@runReadAction cached
val mini = getMiniAst(file) ?: return null val mini = getMiniAst(file) ?: return@runReadAction null
val text = doc.text val text = file.viewProvider.contents.toString()
val binding = try { val binding = try {
Binder.bind(text, mini) Binder.bind(text, mini)
} catch (_: Throwable) { } catch (_: Throwable) {
null null
} }
if (binding != null) { if (binding != null) {
file.putUserData(BINDING_KEY, binding) 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
} }
} }

View File

@ -19,7 +19,7 @@
<!-- Open-ended compatibility: 2024.3+ (build 243 and newer) --> <!-- Open-ended compatibility: 2024.3+ (build 243 and newer) -->
<idea-version since-build="243"/> <idea-version since-build="243"/>
<id>net.sergeych.lyng.idea</id> <id>net.sergeych.lyng.idea</id>
<name>Lyng Language Support</name> <name>Lyng</name>
<vendor email="real.sergeych@gmail.com">Sergey Chernov</vendor> <vendor email="real.sergeych@gmail.com">Sergey Chernov</vendor>
<description> <description>
@ -42,7 +42,8 @@
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<!-- Language and file type --> <!-- 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 --> <!-- Minimal parser/PSI to fully wire editor services for the language -->
<lang.parserDefinition language="Lyng" implementationClass="net.sergeych.lyng.idea.psi.LyngParserDefinition"/> <lang.parserDefinition language="Lyng" implementationClass="net.sergeych.lyng.idea.psi.LyngParserDefinition"/>

View File

@ -2936,7 +2936,9 @@ class Compiler(
returnType = returnTypeMini, returnType = returnTypeMini,
body = bodyRange?.let { MiniBlock(it) }, body = bodyRange?.let { MiniBlock(it) },
doc = declDocLocal, doc = declDocLocal,
nameStart = nameStartPos nameStart = nameStartPos,
receiver = receiverMini,
isExtern = actualExtern
) )
miniSink?.onFunDecl(node) miniSink?.onFunDecl(node)
} }

View File

@ -58,11 +58,11 @@ object CompletionEngineLight {
return completeSuspend(text, idx) 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 // Ensure stdlib Obj*-defined docs (e.g., String methods) are initialized before registry lookup
StdlibDocsBootstrap.ensure() StdlibDocsBootstrap.ensure()
val prefix = prefixAt(text, caret) 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 imported: List<String> = DocLookupUtils.canonicalImportedModules(mini ?: return emptyList(), text)
val cap = 200 val cap = 200

View File

@ -430,6 +430,7 @@ object DocLookupUtils {
fun scanLocalClassMembersFromText(mini: MiniScript, text: String, cls: MiniClassDecl): Map<String, ScannedSig> { fun scanLocalClassMembersFromText(mini: MiniScript, text: String, cls: MiniClassDecl): Map<String, ScannedSig> {
val src = mini.range.start.source val src = mini.range.start.source
if (cls.nameStart.source != src) return emptyMap()
val start = src.offsetOf(cls.bodyRange?.start ?: cls.range.start) val start = src.offsetOf(cls.bodyRange?.start ?: cls.range.start)
val end = src.offsetOf(cls.bodyRange?.end ?: cls.range.end).coerceAtMost(text.length) val end = src.offsetOf(cls.bodyRange?.end ?: cls.range.end).coerceAtMost(text.length)
if (start !in 0..end) return emptyMap() if (start !in 0..end) return emptyMap()

View File

@ -273,4 +273,42 @@ class MiniAstTest {
assertNotNull(e1) assertNotNull(e1)
assertEquals("Doc6", e1.doc?.summary) 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)
}
} }