language tools and site suport
This commit is contained in:
1 parent
78d8e546d5
commit
c16c0d7ebd
16 files changed
+1402
-338
No files matched your search
+41
-251
@@ -25,15 +25,13 @@ import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiFile
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.binding.Binder
|
||||
import net.sergeych.lyng.binding.SymbolKind
|
||||
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.LyngAstManager
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.tools.LyngDiagnosticSeverity
|
||||
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||
import net.sergeych.lyng.tools.LyngSemanticKind
|
||||
|
||||
/**
|
||||
* ExternalAnnotator that runs Lyng MiniAst on the document text in background
|
||||
@@ -43,8 +41,8 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
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)
|
||||
data class Result(val modStamp: Long, val spans: List<Span>, val error: Error? = null)
|
||||
data class Diag(val start: Int, val end: Int, val message: String, val severity: HighlightSeverity)
|
||||
data class Result(val modStamp: Long, val spans: List<Span>, val diagnostics: List<Diag> = emptyList())
|
||||
|
||||
override fun collectInformation(file: PsiFile): Input? {
|
||||
val doc: Document = file.viewProvider.document ?: return null
|
||||
@@ -59,224 +57,46 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
if (collectedInfo == null) return null
|
||||
ProgressManager.checkCanceled()
|
||||
val text = collectedInfo.text
|
||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||
|
||||
// Use LyngAstManager to get the (potentially merged) Mini-AST
|
||||
val mini = LyngAstManager.getMiniAst(collectedInfo.file)
|
||||
val analysis = LyngAstManager.getAnalysis(collectedInfo.file)
|
||||
?: return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList())
|
||||
|
||||
ProgressManager.checkCanceled()
|
||||
val source = Source(collectedInfo.file.name, text)
|
||||
|
||||
val out = ArrayList<Span>(256)
|
||||
val mini = analysis.mini
|
||||
|
||||
fun isFollowedByParenOrBlock(rangeEnd: Int): Boolean {
|
||||
var i = rangeEnd
|
||||
while (i < text.length) {
|
||||
val ch = text[i]
|
||||
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') { i++; continue }
|
||||
return ch == '(' || ch == '{'
|
||||
}
|
||||
return false
|
||||
}
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
val out = ArrayList<Span>(256)
|
||||
val diags = ArrayList<Diag>()
|
||||
|
||||
fun putRange(start: Int, end: Int, key: com.intellij.openapi.editor.colors.TextAttributesKey) {
|
||||
if (start in 0..end && end <= text.length && start < end) out += Span(start, end, key)
|
||||
}
|
||||
fun putName(startPos: net.sergeych.lyng.Pos, name: String, key: com.intellij.openapi.editor.colors.TextAttributesKey) {
|
||||
val s = source.offsetOf(startPos)
|
||||
putRange(s, (s + name.length).coerceAtMost(text.length), key)
|
||||
}
|
||||
fun putMiniRange(r: MiniRange, key: com.intellij.openapi.editor.colors.TextAttributesKey) {
|
||||
val s = source.offsetOf(r.start)
|
||||
val e = source.offsetOf(r.end)
|
||||
putRange(s, e, key)
|
||||
|
||||
fun keyForKind(kind: LyngSemanticKind): com.intellij.openapi.editor.colors.TextAttributesKey? = when (kind) {
|
||||
LyngSemanticKind.Function -> LyngHighlighterColors.FUNCTION
|
||||
LyngSemanticKind.Class, LyngSemanticKind.Enum, LyngSemanticKind.TypeAlias -> LyngHighlighterColors.TYPE
|
||||
LyngSemanticKind.Value -> LyngHighlighterColors.VALUE
|
||||
LyngSemanticKind.Variable -> LyngHighlighterColors.VARIABLE
|
||||
LyngSemanticKind.Parameter -> LyngHighlighterColors.PARAMETER
|
||||
LyngSemanticKind.TypeRef -> LyngHighlighterColors.TYPE
|
||||
LyngSemanticKind.EnumConstant -> LyngHighlighterColors.ENUM_CONSTANT
|
||||
}
|
||||
|
||||
// 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)
|
||||
is MiniValDecl -> putName(
|
||||
d.nameStart,
|
||||
d.name,
|
||||
if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE
|
||||
)
|
||||
is MiniEnumDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE)
|
||||
}
|
||||
// Semantic highlights from shared tooling
|
||||
LyngLanguageTools.semanticHighlights(analysis).forEach { span ->
|
||||
keyForKind(span.kind)?.let { putRange(span.range.start, span.range.endExclusive, it) }
|
||||
}
|
||||
|
||||
// Imports: each segment as namespace/path
|
||||
mini.imports.forEach { imp ->
|
||||
if (imp.range.start.source != source) return@forEach
|
||||
imp.segments.forEach { seg -> putMiniRange(seg.range, LyngHighlighterColors.NAMESPACE) }
|
||||
}
|
||||
|
||||
// Parameters
|
||||
fun addParams(params: List<MiniParam>) {
|
||||
params.forEach { p ->
|
||||
if (p.nameStart.source == source)
|
||||
putName(p.nameStart, p.name, LyngHighlighterColors.PARAMETER)
|
||||
mini?.imports?.forEach { imp ->
|
||||
imp.segments.forEach { seg ->
|
||||
val start = analysis.source.offsetOf(seg.range.start)
|
||||
val end = analysis.source.offsetOf(seg.range.end)
|
||||
putRange(start, end, LyngHighlighterColors.NAMESPACE)
|
||||
}
|
||||
}
|
||||
mini.declarations.forEach { d ->
|
||||
when (d) {
|
||||
is MiniFunDecl -> addParams(d.params)
|
||||
is MiniClassDecl -> d.members.filterIsInstance<MiniMemberFunDecl>().forEach { addParams(it.params) }
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
is MiniGenericType -> {
|
||||
addTypeSegments(t.base)
|
||||
t.args.forEach { addTypeSegments(it) }
|
||||
}
|
||||
is MiniFunctionType -> {
|
||||
t.receiver?.let { addTypeSegments(it) }
|
||||
t.params.forEach { addTypeSegments(it) }
|
||||
addTypeSegments(t.returnType)
|
||||
}
|
||||
is MiniTypeVar -> { /* name is in range; could be highlighted as TYPE as well */
|
||||
if (t.range.start.source == source)
|
||||
putMiniRange(t.range, LyngHighlighterColors.TYPE)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
fun addDeclTypeSegments(d: MiniDecl) {
|
||||
if (d.nameStart.source != source) return
|
||||
when (d) {
|
||||
is MiniFunDecl -> {
|
||||
addTypeSegments(d.returnType)
|
||||
d.params.forEach { addTypeSegments(it.type) }
|
||||
addTypeSegments(d.receiver)
|
||||
}
|
||||
is MiniValDecl -> {
|
||||
addTypeSegments(d.type)
|
||||
addTypeSegments(d.receiver)
|
||||
}
|
||||
is MiniClassDecl -> {
|
||||
d.ctorFields.forEach { addTypeSegments(it.type) }
|
||||
d.classFields.forEach { addTypeSegments(it.type) }
|
||||
for (m in d.members) {
|
||||
when (m) {
|
||||
is MiniMemberFunDecl -> {
|
||||
addTypeSegments(m.returnType)
|
||||
m.params.forEach { addTypeSegments(it.type) }
|
||||
}
|
||||
is MiniMemberValDecl -> {
|
||||
addTypeSegments(m.type)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
is MiniEnumDecl -> {}
|
||||
}
|
||||
}
|
||||
mini.declarations.forEach { d -> addDeclTypeSegments(d) }
|
||||
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
// Semantic usages via Binder (best-effort)
|
||||
try {
|
||||
val binding = Binder.bind(text, mini)
|
||||
|
||||
// Map declaration ranges to avoid duplicating them as usages
|
||||
val declKeys = HashSet<Pair<Int, Int>>(binding.symbols.size * 2)
|
||||
binding.symbols.forEach { sym -> declKeys += (sym.declStart to sym.declEnd) }
|
||||
|
||||
fun keyForKind(k: SymbolKind) = when (k) {
|
||||
SymbolKind.Function -> LyngHighlighterColors.FUNCTION
|
||||
SymbolKind.Class, SymbolKind.Enum -> LyngHighlighterColors.TYPE
|
||||
SymbolKind.Parameter -> LyngHighlighterColors.PARAMETER
|
||||
SymbolKind.Value -> LyngHighlighterColors.VALUE
|
||||
SymbolKind.Variable -> LyngHighlighterColors.VARIABLE
|
||||
}
|
||||
|
||||
// Track covered ranges to not override later heuristics
|
||||
val covered = HashSet<Pair<Int, Int>>()
|
||||
|
||||
binding.references.forEach { ref ->
|
||||
val key = ref.start to ref.end
|
||||
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
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
// Build simple name -> role map for top-level vals/vars and parameters
|
||||
val nameRole = HashMap<String, com.intellij.openapi.editor.colors.TextAttributesKey>(8)
|
||||
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 }
|
||||
is MiniClassDecl -> {
|
||||
d.members.forEach { m ->
|
||||
if (m is MiniMemberFunDecl) {
|
||||
m.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER }
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// Must rethrow cancellation; otherwise ignore binder failures (best-effort)
|
||||
if (e is com.intellij.openapi.progress.ProcessCanceledException) throw e
|
||||
}
|
||||
|
||||
// Add annotation/label coloring using token highlighter
|
||||
run {
|
||||
tokens.forEach { s ->
|
||||
analysis.lexicalHighlights.forEach { s ->
|
||||
if (s.kind == HighlightKind.Label) {
|
||||
val start = s.range.start
|
||||
val end = s.range.endExclusive
|
||||
@@ -302,7 +122,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
text.substring(wStart, wEnd)
|
||||
} else null
|
||||
|
||||
if (prevWord in setOf("return", "break", "continue") || isFollowedByParenOrBlock(end)) {
|
||||
if (prevWord in setOf("return", "break", "continue")) {
|
||||
putRange(start, end, LyngHighlighterColors.LABEL)
|
||||
} else {
|
||||
putRange(start, end, LyngHighlighterColors.ANNOTATION)
|
||||
@@ -315,17 +135,13 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
analysis.diagnostics.forEach { d ->
|
||||
val range = d.range ?: return@forEach
|
||||
val severity = if (d.severity == LyngDiagnosticSeverity.Warning) HighlightSeverity.WARNING else HighlightSeverity.ERROR
|
||||
diags += Diag(range.start, range.endExclusive, d.message, severity)
|
||||
}
|
||||
|
||||
return Result(collectedInfo.modStamp, out, null)
|
||||
return Result(collectedInfo.modStamp, out, diags)
|
||||
}
|
||||
|
||||
|
||||
@@ -346,13 +162,12 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
.create()
|
||||
}
|
||||
|
||||
// Show syntax error if present
|
||||
val err = result.error
|
||||
if (err != null) {
|
||||
val start = err.start.coerceIn(0, (doc?.textLength ?: 0))
|
||||
val end = err.end.coerceIn(start, (doc?.textLength ?: start))
|
||||
// Show errors and warnings
|
||||
result.diagnostics.forEach { d ->
|
||||
val start = d.start.coerceIn(0, (doc?.textLength ?: 0))
|
||||
val end = d.end.coerceIn(start, (doc?.textLength ?: start))
|
||||
if (end > start) {
|
||||
holder.newAnnotation(HighlightSeverity.ERROR, err.message)
|
||||
holder.newAnnotation(d.severity, d.message)
|
||||
.range(TextRange(start, end))
|
||||
.create()
|
||||
}
|
||||
@@ -373,30 +188,5 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the error highlight a bit wider than a single character so it is easier to see and click.
|
||||
* Strategy:
|
||||
* - If the offset points inside an identifier-like token (letters/digits/underscore), expand to the full token.
|
||||
* - Otherwise select a small range starting at the offset with a minimum width, but not crossing the line end.
|
||||
*/
|
||||
private fun expandErrorRange(text: String, rawStart: Int): Pair<Int, Int> {
|
||||
if (text.isEmpty()) return 0 to 0
|
||||
val len = text.length
|
||||
val start = rawStart.coerceIn(0, len)
|
||||
fun isWord(ch: Char) = ch == '_' || ch.isLetterOrDigit()
|
||||
|
||||
if (start < len && isWord(text[start])) {
|
||||
var s = start
|
||||
var e = start
|
||||
while (s > 0 && isWord(text[s - 1])) s--
|
||||
while (e < len && isWord(text[e])) e++
|
||||
return s to e
|
||||
}
|
||||
|
||||
// Not inside a word: select a short, visible range up to EOL
|
||||
val lineEnd = text.indexOf('\n', start).let { if (it == -1) len else it }
|
||||
val minWidth = 4
|
||||
val end = (start + minWidth).coerceAtMost(lineEnd).coerceAtLeast((start + 1).coerceAtMost(lineEnd))
|
||||
return start to end
|
||||
}
|
||||
|
||||
}
|
||||
+6
-3
@@ -96,9 +96,10 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
log.info("[LYNG_DEBUG] Completion: caret=$caret prefix='${prefix}' memberDotPos=${memberDotPos} file='${file.name}'")
|
||||
}
|
||||
|
||||
// Build MiniAst (cached) for both global and member contexts to enable local class/val inference
|
||||
val mini = LyngAstManager.getMiniAst(file)
|
||||
val binding = LyngAstManager.getBinding(file)
|
||||
// Build analysis (cached) for both global and member contexts to enable local class/val inference
|
||||
val analysis = LyngAstManager.getAnalysis(file)
|
||||
val mini = analysis?.mini
|
||||
val binding = analysis?.binding
|
||||
|
||||
// Delegate computation to the shared engine to keep behavior in sync with tests
|
||||
val engineItems = try {
|
||||
@@ -160,6 +161,8 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
.withIcon(AllIcons.Nodes.Class)
|
||||
Kind.Enum -> LookupElementBuilder.create(ci.name)
|
||||
.withIcon(AllIcons.Nodes.Enum)
|
||||
Kind.TypeAlias -> LookupElementBuilder.create(ci.name)
|
||||
.withIcon(AllIcons.Nodes.Class)
|
||||
Kind.Value -> LookupElementBuilder.create(ci.name)
|
||||
.withIcon(AllIcons.Nodes.Variable)
|
||||
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
|
||||
|
||||
@@ -29,6 +29,8 @@ import net.sergeych.lyng.idea.LyngLanguage
|
||||
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||
import net.sergeych.lyng.idea.util.TextCtx
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||
import net.sergeych.lyng.tools.LyngSymbolInfo
|
||||
|
||||
/**
|
||||
* Quick Docs backed by MiniAst: when caret is on an identifier that corresponds
|
||||
@@ -66,9 +68,15 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: ident='$ident' at ${idRange.startOffset}..${idRange.endOffset} in ${file.name}")
|
||||
|
||||
// 1. Get merged mini-AST from Manager (handles local + .lyng.d merged declarations)
|
||||
val mini = LyngAstManager.getMiniAst(file) ?: return null
|
||||
val analysis = LyngAstManager.getAnalysis(file) ?: return null
|
||||
val mini = analysis.mini ?: return null
|
||||
val miniSource = mini.range.start.source
|
||||
val imported = DocLookupUtils.canonicalImportedModules(mini, text)
|
||||
val imported = analysis.importedModules.ifEmpty { DocLookupUtils.canonicalImportedModules(mini, text) }
|
||||
|
||||
// Single-source quick doc lookup
|
||||
LyngLanguageTools.docAt(analysis, offset)?.let { info ->
|
||||
renderDocFromInfo(info)?.let { return it }
|
||||
}
|
||||
|
||||
// Try resolve to: function param at position, function/class/val declaration at position
|
||||
// 1) Use unified declaration detection
|
||||
@@ -91,6 +99,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (m) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(d.name, m)
|
||||
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(d.name, m)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -197,6 +206,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (m) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(cls.name, m)
|
||||
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(cls.name, m)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -312,11 +322,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
|
||||
is MiniInitDecl -> null
|
||||
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
}
|
||||
}
|
||||
log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}")
|
||||
@@ -354,6 +366,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
// And classes/enums
|
||||
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
|
||||
docs.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
|
||||
docs.filterIsInstance<MiniTypeAliasDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
|
||||
}
|
||||
// Defensive fallback: if nothing found and it's a well-known stdlib function, render minimal inline docs
|
||||
if (ident == "println" || ident == "print") {
|
||||
@@ -372,11 +385,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
|
||||
is MiniInitDecl -> null
|
||||
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -395,11 +410,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
|
||||
is MiniInitDecl -> null
|
||||
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -412,11 +429,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (member) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
|
||||
is MiniInitDecl -> null
|
||||
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,6 +450,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return when (m) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc("String", m)
|
||||
is MiniMemberValDecl -> renderMemberValDoc("String", m)
|
||||
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc("String", m)
|
||||
is MiniInitDecl -> null
|
||||
}
|
||||
}
|
||||
@@ -512,6 +532,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
is MiniFunDecl -> "function ${d.name}${signatureOf(d)}"
|
||||
is MiniClassDecl -> "class ${d.name}"
|
||||
is MiniEnumDecl -> "enum ${d.name} { ${d.entries.joinToString(", ")} }"
|
||||
is MiniTypeAliasDecl -> "type ${d.name}${typeAliasSuffix(d)}"
|
||||
is MiniValDecl -> {
|
||||
val t = d.type ?: DocLookupUtils.inferTypeRefForVal(d, text, imported, mini)
|
||||
val typeStr = if (t == null) ": Object?" else typeOf(t)
|
||||
@@ -524,6 +545,24 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun renderDocFromInfo(info: LyngSymbolInfo): String? {
|
||||
val kind = when (info.target.kind) {
|
||||
net.sergeych.lyng.binding.SymbolKind.Function -> "function"
|
||||
net.sergeych.lyng.binding.SymbolKind.Class -> "class"
|
||||
net.sergeych.lyng.binding.SymbolKind.Enum -> "enum"
|
||||
net.sergeych.lyng.binding.SymbolKind.TypeAlias -> "type"
|
||||
net.sergeych.lyng.binding.SymbolKind.Value -> "val"
|
||||
net.sergeych.lyng.binding.SymbolKind.Variable -> "var"
|
||||
net.sergeych.lyng.binding.SymbolKind.Parameter -> "parameter"
|
||||
}
|
||||
val title = info.signature ?: "$kind ${info.target.name}"
|
||||
if (title.isBlank() && info.doc == null) return null
|
||||
val sb = StringBuilder()
|
||||
sb.append(renderTitle(title))
|
||||
sb.append(renderDocBody(info.doc))
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
|
||||
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
|
||||
val sb = StringBuilder()
|
||||
@@ -565,6 +604,25 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun renderMemberTypeAliasDoc(className: String, m: MiniMemberTypeAliasDecl): String {
|
||||
val tp = if (m.typeParams.isEmpty()) "" else "<" + m.typeParams.joinToString(", ") + ">"
|
||||
val body = typeOf(m.target)
|
||||
val rhs = if (body.isBlank()) "" else " = ${body.removePrefix(": ")}"
|
||||
val staticStr = if (m.isStatic) "static " else ""
|
||||
val title = "${staticStr}type $className.${m.name}$tp$rhs"
|
||||
val sb = StringBuilder()
|
||||
sb.append(renderTitle(title))
|
||||
sb.append(renderDocBody(m.doc))
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun typeAliasSuffix(d: MiniTypeAliasDecl): String {
|
||||
val tp = if (d.typeParams.isEmpty()) "" else "<" + d.typeParams.joinToString(", ") + ">"
|
||||
val body = typeOf(d.target)
|
||||
val rhs = if (body.isBlank()) "" else " = ${body.removePrefix(": ")}"
|
||||
return "$tp$rhs"
|
||||
}
|
||||
|
||||
private fun typeOf(t: MiniTypeRef?): String {
|
||||
val s = DocLookupUtils.typeOf(t)
|
||||
return if (s.isEmpty()) (if (t == null) ": Object?" else "") else ": $s"
|
||||
|
||||
@@ -36,9 +36,10 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
val name = element.text ?: ""
|
||||
val results = mutableListOf<ResolveResult>()
|
||||
|
||||
val mini = LyngAstManager.getMiniAst(file) ?: return emptyArray()
|
||||
val binding = LyngAstManager.getBinding(file)
|
||||
val imported = DocLookupUtils.canonicalImportedModules(mini, text).toSet()
|
||||
val analysis = LyngAstManager.getAnalysis(file) ?: return emptyArray()
|
||||
val mini = analysis.mini ?: return emptyArray()
|
||||
val binding = analysis.binding
|
||||
val imported = analysis.importedModules.toSet()
|
||||
val currentPackage = getPackageName(file)
|
||||
val allowedPackages = if (currentPackage != null) imported + currentPackage else imported
|
||||
|
||||
@@ -64,11 +65,13 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
val kind = when(member) {
|
||||
is MiniMemberFunDecl -> "Function"
|
||||
is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value"
|
||||
is MiniMemberTypeAliasDecl -> "TypeAlias"
|
||||
is MiniInitDecl -> "Initializer"
|
||||
is MiniFunDecl -> "Function"
|
||||
is MiniValDecl -> if (member.mutable) "Variable" else "Value"
|
||||
is MiniClassDecl -> "Class"
|
||||
is MiniEnumDecl -> "Enum"
|
||||
is MiniTypeAliasDecl -> "TypeAlias"
|
||||
}
|
||||
results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind)))
|
||||
}
|
||||
@@ -199,6 +202,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
is net.sergeych.lyng.miniast.MiniClassDecl -> "Class"
|
||||
is net.sergeych.lyng.miniast.MiniEnumDecl -> "Enum"
|
||||
is net.sergeych.lyng.miniast.MiniValDecl -> if (d.mutable) "Variable" else "Value"
|
||||
is net.sergeych.lyng.miniast.MiniTypeAliasDecl -> "TypeAlias"
|
||||
}
|
||||
addIfMatch(d.name, d.nameStart, dKind)
|
||||
}
|
||||
@@ -214,6 +218,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
val mKind = when(m) {
|
||||
is net.sergeych.lyng.miniast.MiniMemberFunDecl -> "Function"
|
||||
is net.sergeych.lyng.miniast.MiniMemberValDecl -> if (m.mutable) "Variable" else "Value"
|
||||
is net.sergeych.lyng.miniast.MiniMemberTypeAliasDecl -> "TypeAlias"
|
||||
is net.sergeych.lyng.miniast.MiniInitDecl -> "Initializer"
|
||||
}
|
||||
addIfMatch(m.name, m.nameStart, mKind)
|
||||
|
||||
@@ -22,55 +22,21 @@ import com.intellij.openapi.util.Key
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.Compiler
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.binding.Binder
|
||||
import net.sergeych.lyng.binding.BindingSnapshot
|
||||
import net.sergeych.lyng.miniast.MiniAstBuilder
|
||||
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||
import net.sergeych.lyng.miniast.MiniScript
|
||||
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
||||
import net.sergeych.lyng.tools.LyngAnalysisResult
|
||||
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||
|
||||
object LyngAstManager {
|
||||
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
||||
private val BINDING_KEY = Key.create<BindingSnapshot>("lyng.binding.cache")
|
||||
private val STAMP_KEY = Key.create<Long>("lyng.mini.cache.stamp")
|
||||
private val ANALYSIS_KEY = Key.create<LyngAnalysisResult>("lyng.analysis.cache")
|
||||
|
||||
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 == combinedStamp) return@runReadAction cached
|
||||
|
||||
val text = file.viewProvider.contents.toString()
|
||||
val sink = MiniAstBuilder()
|
||||
val built = try {
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
val src = Source(file.name, text)
|
||||
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
||||
val script = sink.build()
|
||||
if (script != null && !file.name.endsWith(".lyng.d")) {
|
||||
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) {
|
||||
sink.build()
|
||||
}
|
||||
|
||||
if (built != null) {
|
||||
file.putUserData(MINI_KEY, built)
|
||||
file.putUserData(STAMP_KEY, combinedStamp)
|
||||
// Invalidate binding too
|
||||
file.putUserData(BINDING_KEY, null)
|
||||
}
|
||||
built
|
||||
getAnalysis(file)?.mini
|
||||
}
|
||||
|
||||
fun getCombinedStamp(file: PsiFile): Long = runReadAction {
|
||||
@@ -102,32 +68,53 @@ object LyngAstManager {
|
||||
}
|
||||
|
||||
fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction {
|
||||
getAnalysis(file)?.binding
|
||||
}
|
||||
|
||||
fun getAnalysis(file: PsiFile): LyngAnalysisResult? = 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 combinedStamp = getCombinedStamp(file)
|
||||
val prevStamp = file.getUserData(STAMP_KEY)
|
||||
val cached = file.getUserData(BINDING_KEY)
|
||||
|
||||
val cached = file.getUserData(ANALYSIS_KEY)
|
||||
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)
|
||||
val built = try {
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
runBlocking {
|
||||
LyngLanguageTools.analyze(
|
||||
LyngAnalysisRequest(text = text, fileName = file.name, importProvider = provider)
|
||||
)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
if (binding != null) {
|
||||
file.putUserData(BINDING_KEY, binding)
|
||||
// stamp is already set by getMiniAst or we set it here if getMiniAst was cached
|
||||
if (built != null) {
|
||||
val merged = built.mini
|
||||
if (merged != null && !file.name.endsWith(".lyng.d")) {
|
||||
val dFiles = collectDeclarationFiles(file)
|
||||
for (df in dFiles) {
|
||||
val dAnalysis = getAnalysis(df)
|
||||
val dMini = dAnalysis?.mini ?: continue
|
||||
merged.declarations.addAll(dMini.declarations)
|
||||
merged.imports.addAll(dMini.imports)
|
||||
}
|
||||
}
|
||||
val finalAnalysis = if (merged != null) {
|
||||
built.copy(
|
||||
mini = merged,
|
||||
importedModules = DocLookupUtils.canonicalImportedModules(merged, text)
|
||||
)
|
||||
} else {
|
||||
built
|
||||
}
|
||||
file.putUserData(ANALYSIS_KEY, finalAnalysis)
|
||||
file.putUserData(MINI_KEY, finalAnalysis.mini)
|
||||
file.putUserData(BINDING_KEY, finalAnalysis.binding)
|
||||
file.putUserData(STAMP_KEY, combinedStamp)
|
||||
return@runReadAction finalAnalysis
|
||||
}
|
||||
binding
|
||||
null
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user