language tools and site suport
This commit is contained in:
parent
78d8e546d5
commit
c16c0d7ebd
@ -25,15 +25,13 @@ 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 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.HighlightKind
|
||||||
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.LyngAstManager
|
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
|
* 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 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 Diag(val start: Int, val end: Int, val message: String, val severity: HighlightSeverity)
|
||||||
data class Result(val modStamp: Long, val spans: List<Span>, val error: Error? = null)
|
data class Result(val modStamp: Long, val spans: List<Span>, val diagnostics: List<Diag> = emptyList())
|
||||||
|
|
||||||
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
|
||||||
@ -59,224 +57,46 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
if (collectedInfo == null) return null
|
if (collectedInfo == null) return null
|
||||||
ProgressManager.checkCanceled()
|
ProgressManager.checkCanceled()
|
||||||
val text = collectedInfo.text
|
val text = collectedInfo.text
|
||||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
val analysis = LyngAstManager.getAnalysis(collectedInfo.file)
|
||||||
|
|
||||||
// Use LyngAstManager to get the (potentially merged) Mini-AST
|
|
||||||
val mini = LyngAstManager.getMiniAst(collectedInfo.file)
|
|
||||||
?: return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList())
|
?: return Result(collectedInfo.modStamp, collectedInfo.previousSpans ?: emptyList())
|
||||||
|
val mini = analysis.mini
|
||||||
|
|
||||||
ProgressManager.checkCanceled()
|
ProgressManager.checkCanceled()
|
||||||
val source = Source(collectedInfo.file.name, text)
|
|
||||||
|
|
||||||
val out = ArrayList<Span>(256)
|
val out = ArrayList<Span>(256)
|
||||||
|
val diags = ArrayList<Diag>()
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putRange(start: Int, end: Int, key: com.intellij.openapi.editor.colors.TextAttributesKey) {
|
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)
|
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)
|
fun keyForKind(kind: LyngSemanticKind): com.intellij.openapi.editor.colors.TextAttributesKey? = when (kind) {
|
||||||
putRange(s, (s + name.length).coerceAtMost(text.length), key)
|
LyngSemanticKind.Function -> LyngHighlighterColors.FUNCTION
|
||||||
}
|
LyngSemanticKind.Class, LyngSemanticKind.Enum, LyngSemanticKind.TypeAlias -> LyngHighlighterColors.TYPE
|
||||||
fun putMiniRange(r: MiniRange, key: com.intellij.openapi.editor.colors.TextAttributesKey) {
|
LyngSemanticKind.Value -> LyngHighlighterColors.VALUE
|
||||||
val s = source.offsetOf(r.start)
|
LyngSemanticKind.Variable -> LyngHighlighterColors.VARIABLE
|
||||||
val e = source.offsetOf(r.end)
|
LyngSemanticKind.Parameter -> LyngHighlighterColors.PARAMETER
|
||||||
putRange(s, e, key)
|
LyngSemanticKind.TypeRef -> LyngHighlighterColors.TYPE
|
||||||
|
LyngSemanticKind.EnumConstant -> LyngHighlighterColors.ENUM_CONSTANT
|
||||||
}
|
}
|
||||||
|
|
||||||
// Declarations
|
// Semantic highlights from shared tooling
|
||||||
mini.declarations.forEach { d ->
|
LyngLanguageTools.semanticHighlights(analysis).forEach { span ->
|
||||||
if (d.nameStart.source != source) return@forEach
|
keyForKind(span.kind)?.let { putRange(span.range.start, span.range.endExclusive, it) }
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Imports: each segment as namespace/path
|
// Imports: each segment as namespace/path
|
||||||
mini.imports.forEach { imp ->
|
mini?.imports?.forEach { imp ->
|
||||||
if (imp.range.start.source != source) return@forEach
|
imp.segments.forEach { seg ->
|
||||||
imp.segments.forEach { seg -> putMiniRange(seg.range, LyngHighlighterColors.NAMESPACE) }
|
val start = analysis.source.offsetOf(seg.range.start)
|
||||||
|
val end = analysis.source.offsetOf(seg.range.end)
|
||||||
|
putRange(start, end, LyngHighlighterColors.NAMESPACE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameters
|
|
||||||
fun addParams(params: List<MiniParam>) {
|
|
||||||
params.forEach { p ->
|
|
||||||
if (p.nameStart.source == source)
|
|
||||||
putName(p.nameStart, p.name, LyngHighlighterColors.PARAMETER)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
// Add annotation/label coloring using token highlighter
|
||||||
run {
|
run {
|
||||||
tokens.forEach { s ->
|
analysis.lexicalHighlights.forEach { s ->
|
||||||
if (s.kind == HighlightKind.Label) {
|
if (s.kind == HighlightKind.Label) {
|
||||||
val start = s.range.start
|
val start = s.range.start
|
||||||
val end = s.range.endExclusive
|
val end = s.range.endExclusive
|
||||||
@ -302,7 +122,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
text.substring(wStart, wEnd)
|
text.substring(wStart, wEnd)
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
if (prevWord in setOf("return", "break", "continue") || isFollowedByParenOrBlock(end)) {
|
if (prevWord in setOf("return", "break", "continue")) {
|
||||||
putRange(start, end, LyngHighlighterColors.LABEL)
|
putRange(start, end, LyngHighlighterColors.LABEL)
|
||||||
} else {
|
} else {
|
||||||
putRange(start, end, LyngHighlighterColors.ANNOTATION)
|
putRange(start, end, LyngHighlighterColors.ANNOTATION)
|
||||||
@ -315,17 +135,13 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens.forEach { s ->
|
analysis.diagnostics.forEach { d ->
|
||||||
if (s.kind == HighlightKind.EnumConstant) {
|
val range = d.range ?: return@forEach
|
||||||
val start = s.range.start
|
val severity = if (d.severity == LyngDiagnosticSeverity.Warning) HighlightSeverity.WARNING else HighlightSeverity.ERROR
|
||||||
val end = s.range.endExclusive
|
diags += Diag(range.start, range.endExclusive, d.message, severity)
|
||||||
if (start in 0..end && end <= text.length && start < end) {
|
|
||||||
putRange(start, end, LyngHighlighterColors.ENUM_CONSTANT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result(collectedInfo.modStamp, out, null)
|
return Result(collectedInfo.modStamp, out, diags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -346,13 +162,12 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show syntax error if present
|
// Show errors and warnings
|
||||||
val err = result.error
|
result.diagnostics.forEach { d ->
|
||||||
if (err != null) {
|
val start = d.start.coerceIn(0, (doc?.textLength ?: 0))
|
||||||
val start = err.start.coerceIn(0, (doc?.textLength ?: 0))
|
val end = d.end.coerceIn(start, (doc?.textLength ?: start))
|
||||||
val end = err.end.coerceIn(start, (doc?.textLength ?: start))
|
|
||||||
if (end > start) {
|
if (end > start) {
|
||||||
holder.newAnnotation(HighlightSeverity.ERROR, err.message)
|
holder.newAnnotation(d.severity, d.message)
|
||||||
.range(TextRange(start, end))
|
.range(TextRange(start, end))
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
@ -373,30 +188,5 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
return -1
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,9 +96,10 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
log.info("[LYNG_DEBUG] Completion: caret=$caret prefix='${prefix}' memberDotPos=${memberDotPos} file='${file.name}'")
|
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
|
// Build analysis (cached) for both global and member contexts to enable local class/val inference
|
||||||
val mini = LyngAstManager.getMiniAst(file)
|
val analysis = LyngAstManager.getAnalysis(file)
|
||||||
val binding = LyngAstManager.getBinding(file)
|
val mini = analysis?.mini
|
||||||
|
val binding = analysis?.binding
|
||||||
|
|
||||||
// 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 {
|
||||||
@ -160,6 +161,8 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
.withIcon(AllIcons.Nodes.Class)
|
.withIcon(AllIcons.Nodes.Class)
|
||||||
Kind.Enum -> LookupElementBuilder.create(ci.name)
|
Kind.Enum -> LookupElementBuilder.create(ci.name)
|
||||||
.withIcon(AllIcons.Nodes.Enum)
|
.withIcon(AllIcons.Nodes.Enum)
|
||||||
|
Kind.TypeAlias -> LookupElementBuilder.create(ci.name)
|
||||||
|
.withIcon(AllIcons.Nodes.Class)
|
||||||
Kind.Value -> LookupElementBuilder.create(ci.name)
|
Kind.Value -> LookupElementBuilder.create(ci.name)
|
||||||
.withIcon(AllIcons.Nodes.Variable)
|
.withIcon(AllIcons.Nodes.Variable)
|
||||||
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
|
.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.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.*
|
||||||
|
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
|
* 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}")
|
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)
|
// 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 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
|
// Try resolve to: function param at position, function/class/val declaration at position
|
||||||
// 1) Use unified declaration detection
|
// 1) Use unified declaration detection
|
||||||
@ -91,6 +99,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (m) {
|
return when (m) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m)
|
is MiniMemberFunDecl -> renderMemberFunDoc(d.name, m)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(d.name, m)
|
is MiniMemberValDecl -> renderMemberValDoc(d.name, m)
|
||||||
|
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(d.name, m)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,6 +206,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (m) {
|
return when (m) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m)
|
is MiniMemberFunDecl -> renderMemberFunDoc(cls.name, m)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(cls.name, m)
|
is MiniMemberValDecl -> renderMemberValDoc(cls.name, m)
|
||||||
|
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(cls.name, m)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -312,11 +322,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
|
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
|
||||||
is MiniInitDecl -> null
|
is MiniInitDecl -> null
|
||||||
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniEnumDecl -> 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}")
|
log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}")
|
||||||
@ -354,6 +366,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
// And classes/enums
|
// And classes/enums
|
||||||
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
|
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<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
|
// Defensive fallback: if nothing found and it's a well-known stdlib function, render minimal inline docs
|
||||||
if (ident == "println" || ident == "print") {
|
if (ident == "println" || ident == "print") {
|
||||||
@ -372,11 +385,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
|
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
|
||||||
is MiniInitDecl -> null
|
is MiniInitDecl -> null
|
||||||
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -395,11 +410,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
|
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
|
||||||
is MiniInitDecl -> null
|
is MiniInitDecl -> null
|
||||||
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniTypeAliasDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -412,11 +429,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return when (member) {
|
return when (member) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
|
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, member)
|
||||||
is MiniInitDecl -> null
|
is MiniInitDecl -> null
|
||||||
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
is MiniEnumDecl -> 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) {
|
return when (m) {
|
||||||
is MiniMemberFunDecl -> renderMemberFunDoc("String", m)
|
is MiniMemberFunDecl -> renderMemberFunDoc("String", m)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc("String", m)
|
is MiniMemberValDecl -> renderMemberValDoc("String", m)
|
||||||
|
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc("String", m)
|
||||||
is MiniInitDecl -> null
|
is MiniInitDecl -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -512,6 +532,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
is MiniFunDecl -> "function ${d.name}${signatureOf(d)}"
|
is MiniFunDecl -> "function ${d.name}${signatureOf(d)}"
|
||||||
is MiniClassDecl -> "class ${d.name}"
|
is MiniClassDecl -> "class ${d.name}"
|
||||||
is MiniEnumDecl -> "enum ${d.name} { ${d.entries.joinToString(", ")} }"
|
is MiniEnumDecl -> "enum ${d.name} { ${d.entries.joinToString(", ")} }"
|
||||||
|
is MiniTypeAliasDecl -> "type ${d.name}${typeAliasSuffix(d)}"
|
||||||
is MiniValDecl -> {
|
is MiniValDecl -> {
|
||||||
val t = d.type ?: DocLookupUtils.inferTypeRefForVal(d, text, imported, mini)
|
val t = d.type ?: DocLookupUtils.inferTypeRefForVal(d, text, imported, mini)
|
||||||
val typeStr = if (t == null) ": Object?" else typeOf(t)
|
val typeStr = if (t == null) ": Object?" else typeOf(t)
|
||||||
@ -524,6 +545,24 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return sb.toString()
|
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 {
|
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
|
||||||
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
|
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
@ -565,6 +604,25 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return sb.toString()
|
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 {
|
private fun typeOf(t: MiniTypeRef?): String {
|
||||||
val s = DocLookupUtils.typeOf(t)
|
val s = DocLookupUtils.typeOf(t)
|
||||||
return if (s.isEmpty()) (if (t == null) ": Object?" else "") else ": $s"
|
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 name = element.text ?: ""
|
||||||
val results = mutableListOf<ResolveResult>()
|
val results = mutableListOf<ResolveResult>()
|
||||||
|
|
||||||
val mini = LyngAstManager.getMiniAst(file) ?: return emptyArray()
|
val analysis = LyngAstManager.getAnalysis(file) ?: return emptyArray()
|
||||||
val binding = LyngAstManager.getBinding(file)
|
val mini = analysis.mini ?: return emptyArray()
|
||||||
val imported = DocLookupUtils.canonicalImportedModules(mini, text).toSet()
|
val binding = analysis.binding
|
||||||
|
val imported = analysis.importedModules.toSet()
|
||||||
val currentPackage = getPackageName(file)
|
val currentPackage = getPackageName(file)
|
||||||
val allowedPackages = if (currentPackage != null) imported + currentPackage else imported
|
val allowedPackages = if (currentPackage != null) imported + currentPackage else imported
|
||||||
|
|
||||||
@ -64,11 +65,13 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
val kind = when(member) {
|
val kind = when(member) {
|
||||||
is MiniMemberFunDecl -> "Function"
|
is MiniMemberFunDecl -> "Function"
|
||||||
is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value"
|
is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value"
|
||||||
|
is MiniMemberTypeAliasDecl -> "TypeAlias"
|
||||||
is MiniInitDecl -> "Initializer"
|
is MiniInitDecl -> "Initializer"
|
||||||
is MiniFunDecl -> "Function"
|
is MiniFunDecl -> "Function"
|
||||||
is MiniValDecl -> if (member.mutable) "Variable" else "Value"
|
is MiniValDecl -> if (member.mutable) "Variable" else "Value"
|
||||||
is MiniClassDecl -> "Class"
|
is MiniClassDecl -> "Class"
|
||||||
is MiniEnumDecl -> "Enum"
|
is MiniEnumDecl -> "Enum"
|
||||||
|
is MiniTypeAliasDecl -> "TypeAlias"
|
||||||
}
|
}
|
||||||
results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind)))
|
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.MiniClassDecl -> "Class"
|
||||||
is net.sergeych.lyng.miniast.MiniEnumDecl -> "Enum"
|
is net.sergeych.lyng.miniast.MiniEnumDecl -> "Enum"
|
||||||
is net.sergeych.lyng.miniast.MiniValDecl -> if (d.mutable) "Variable" else "Value"
|
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)
|
addIfMatch(d.name, d.nameStart, dKind)
|
||||||
}
|
}
|
||||||
@ -214,6 +218,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
val mKind = when(m) {
|
val mKind = when(m) {
|
||||||
is net.sergeych.lyng.miniast.MiniMemberFunDecl -> "Function"
|
is net.sergeych.lyng.miniast.MiniMemberFunDecl -> "Function"
|
||||||
is net.sergeych.lyng.miniast.MiniMemberValDecl -> if (m.mutable) "Variable" else "Value"
|
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"
|
is net.sergeych.lyng.miniast.MiniInitDecl -> "Initializer"
|
||||||
}
|
}
|
||||||
addIfMatch(m.name, m.nameStart, mKind)
|
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.PsiFile
|
||||||
import com.intellij.psi.PsiManager
|
import com.intellij.psi.PsiManager
|
||||||
import kotlinx.coroutines.runBlocking
|
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.binding.BindingSnapshot
|
||||||
import net.sergeych.lyng.miniast.MiniAstBuilder
|
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||||
import net.sergeych.lyng.miniast.MiniScript
|
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 {
|
object LyngAstManager {
|
||||||
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
||||||
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")
|
||||||
|
private val ANALYSIS_KEY = Key.create<LyngAnalysisResult>("lyng.analysis.cache")
|
||||||
|
|
||||||
fun getMiniAst(file: PsiFile): MiniScript? = runReadAction {
|
fun getMiniAst(file: PsiFile): MiniScript? = runReadAction {
|
||||||
val vFile = file.virtualFile ?: return@runReadAction null
|
getAnalysis(file)?.mini
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCombinedStamp(file: PsiFile): Long = runReadAction {
|
fun getCombinedStamp(file: PsiFile): Long = runReadAction {
|
||||||
@ -102,32 +68,53 @@ object LyngAstManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction {
|
fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction {
|
||||||
val vFile = file.virtualFile ?: return@runReadAction null
|
getAnalysis(file)?.binding
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAnalysis(file: PsiFile): LyngAnalysisResult? = runReadAction {
|
||||||
|
val vFile = file.virtualFile ?: return@runReadAction null
|
||||||
|
val combinedStamp = getCombinedStamp(file)
|
||||||
val prevStamp = file.getUserData(STAMP_KEY)
|
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
|
if (cached != null && prevStamp != null && prevStamp == combinedStamp) return@runReadAction cached
|
||||||
|
|
||||||
val mini = getMiniAst(file) ?: return@runReadAction null
|
|
||||||
val text = file.viewProvider.contents.toString()
|
val text = file.viewProvider.contents.toString()
|
||||||
val binding = try {
|
val built = try {
|
||||||
Binder.bind(text, mini)
|
val provider = IdeLenientImportProvider.create()
|
||||||
|
runBlocking {
|
||||||
|
LyngLanguageTools.analyze(
|
||||||
|
LyngAnalysisRequest(text = text, fileName = file.name, importProvider = provider)
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (binding != null) {
|
if (built != null) {
|
||||||
file.putUserData(BINDING_KEY, binding)
|
val merged = built.mini
|
||||||
// stamp is already set by getMiniAst or we set it here if getMiniAst was cached
|
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)
|
file.putUserData(STAMP_KEY, combinedStamp)
|
||||||
|
return@runReadAction finalAnalysis
|
||||||
}
|
}
|
||||||
binding
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import net.sergeych.lyng.highlight.SimpleLyngHighlighter
|
|||||||
import net.sergeych.lyng.highlight.offsetOf
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
|
|
||||||
enum class SymbolKind { Class, Enum, Function, Value, Variable, Parameter }
|
enum class SymbolKind { Class, Enum, TypeAlias, Function, Value, Variable, Parameter }
|
||||||
|
|
||||||
data class Symbol(
|
data class Symbol(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
@ -126,7 +126,8 @@ object Binder {
|
|||||||
}
|
}
|
||||||
// Members (including fields and methods)
|
// Members (including fields and methods)
|
||||||
for (m in d.members) {
|
for (m in d.members) {
|
||||||
if (m is MiniMemberValDecl) {
|
when (m) {
|
||||||
|
is MiniMemberValDecl -> {
|
||||||
val fs = source.offsetOf(m.nameStart)
|
val fs = source.offsetOf(m.nameStart)
|
||||||
val fe = fs + m.name.length
|
val fe = fs + m.name.length
|
||||||
val kind = if (m.mutable) SymbolKind.Variable else SymbolKind.Value
|
val kind = if (m.mutable) SymbolKind.Variable else SymbolKind.Value
|
||||||
@ -134,6 +135,14 @@ object Binder {
|
|||||||
symbols += fieldSym
|
symbols += fieldSym
|
||||||
classScope.fields += fieldSym.id
|
classScope.fields += fieldSym.id
|
||||||
}
|
}
|
||||||
|
is MiniMemberTypeAliasDecl -> {
|
||||||
|
val fs = source.offsetOf(m.nameStart)
|
||||||
|
val fe = fs + m.name.length
|
||||||
|
val aliasSym = Symbol(nextId++, m.name, SymbolKind.TypeAlias, fs, fe, containerId = sym.id, type = DocLookupUtils.typeOf(m.target))
|
||||||
|
symbols += aliasSym
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +206,12 @@ object Binder {
|
|||||||
symbols += sym
|
symbols += sym
|
||||||
topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id)
|
topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id)
|
||||||
}
|
}
|
||||||
|
is MiniTypeAliasDecl -> {
|
||||||
|
val (s, e) = nameOffsets(d.nameStart, d.name)
|
||||||
|
val sym = Symbol(nextId++, d.name, SymbolKind.TypeAlias, s, e, containerId = null, type = DocLookupUtils.typeOf(d.target))
|
||||||
|
symbols += sym
|
||||||
|
topLevelByName.getOrPut(d.name) { mutableListOf() }.add(sym.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ data class CompletionItem(
|
|||||||
val priority: Double = 0.0,
|
val priority: Double = 0.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class Kind { Function, Class_, Enum, Value, Method, Field }
|
enum class Kind { Function, Class_, Enum, TypeAlias, Value, Method, Field }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform-free, lenient import provider that never fails on unknown packages.
|
* Platform-free, lenient import provider that never fails on unknown packages.
|
||||||
@ -118,9 +118,11 @@ object CompletionEngineLight {
|
|||||||
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
||||||
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
|
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
|
||||||
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
|
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
|
||||||
|
val aliases = decls.filterIsInstance<MiniTypeAliasDecl>().sortedBy { it.name.lowercase() }
|
||||||
funs.forEach { offerDeclAdd(out, prefix, it) }
|
funs.forEach { offerDeclAdd(out, prefix, it) }
|
||||||
classes.forEach { offerDeclAdd(out, prefix, it) }
|
classes.forEach { offerDeclAdd(out, prefix, it) }
|
||||||
enums.forEach { offerDeclAdd(out, prefix, it) }
|
enums.forEach { offerDeclAdd(out, prefix, it) }
|
||||||
|
aliases.forEach { offerDeclAdd(out, prefix, it) }
|
||||||
vals.forEach { offerDeclAdd(out, prefix, it) }
|
vals.forEach { offerDeclAdd(out, prefix, it) }
|
||||||
|
|
||||||
// Imported and builtin
|
// Imported and builtin
|
||||||
@ -135,9 +137,11 @@ object CompletionEngineLight {
|
|||||||
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
||||||
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
|
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
|
||||||
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
|
val vals = decls.filterIsInstance<MiniValDecl>().sortedBy { it.name.lowercase() }
|
||||||
|
val aliases = decls.filterIsInstance<MiniTypeAliasDecl>().sortedBy { it.name.lowercase() }
|
||||||
funs.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
funs.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
||||||
classes.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
classes.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
||||||
enums.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
enums.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
||||||
|
aliases.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
||||||
vals.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
vals.forEach { if (externalAdded < budget) { offerDeclAdd(out, prefix, it); externalAdded++ } }
|
||||||
if (out.size >= cap || externalAdded >= budget) break
|
if (out.size >= cap || externalAdded >= budget) break
|
||||||
}
|
}
|
||||||
@ -196,6 +200,9 @@ object CompletionEngineLight {
|
|||||||
is MiniMemberValDecl -> {
|
is MiniMemberValDecl -> {
|
||||||
add(CompletionItem(m.name, if (m.mutable) Kind.Value else Kind.Field, typeText = typeOf(m.type), priority = 100.0))
|
add(CompletionItem(m.name, if (m.mutable) Kind.Value else Kind.Field, typeText = typeOf(m.type), priority = 100.0))
|
||||||
}
|
}
|
||||||
|
is MiniMemberTypeAliasDecl -> {
|
||||||
|
add(CompletionItem(m.name, Kind.TypeAlias, typeText = typeOf(m.target), priority = 100.0))
|
||||||
|
}
|
||||||
is MiniInitDecl -> {}
|
is MiniInitDecl -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,6 +232,7 @@ object CompletionEngineLight {
|
|||||||
}
|
}
|
||||||
is MiniClassDecl -> add(CompletionItem(d.name, Kind.Class_))
|
is MiniClassDecl -> add(CompletionItem(d.name, Kind.Class_))
|
||||||
is MiniEnumDecl -> add(CompletionItem(d.name, Kind.Enum))
|
is MiniEnumDecl -> add(CompletionItem(d.name, Kind.Enum))
|
||||||
|
is MiniTypeAliasDecl -> add(CompletionItem(d.name, Kind.TypeAlias, typeText = typeOf(d.target)))
|
||||||
is MiniValDecl -> add(CompletionItem(d.name, Kind.Value, typeText = typeOf(d.type)))
|
is MiniValDecl -> add(CompletionItem(d.name, Kind.Value, typeText = typeOf(d.type)))
|
||||||
// else -> add(CompletionItem(d.name, Kind.Value))
|
// else -> add(CompletionItem(d.name, Kind.Value))
|
||||||
}
|
}
|
||||||
@ -289,6 +297,10 @@ object CompletionEngineLight {
|
|||||||
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(chosen.type), priority = groupPriority)
|
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(chosen.type), priority = groupPriority)
|
||||||
if (ci.name.startsWith(prefix, true)) out += ci
|
if (ci.name.startsWith(prefix, true)) out += ci
|
||||||
}
|
}
|
||||||
|
is MiniMemberTypeAliasDecl -> {
|
||||||
|
val ci = CompletionItem(name, Kind.TypeAlias, typeText = typeOf(rep.target), priority = groupPriority)
|
||||||
|
if (ci.name.startsWith(prefix, true)) out += ci
|
||||||
|
}
|
||||||
is MiniInitDecl -> {}
|
is MiniInitDecl -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -317,6 +329,8 @@ object CompletionEngineLight {
|
|||||||
}
|
}
|
||||||
is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0)
|
is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0)
|
||||||
is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0)
|
is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0)
|
||||||
|
is MiniMemberTypeAliasDecl -> CompletionItem(name, Kind.TypeAlias, typeText = typeOf(m.target), priority = 50.0)
|
||||||
|
is MiniTypeAliasDecl -> CompletionItem(name, Kind.TypeAlias, typeText = typeOf(m.target), priority = 50.0)
|
||||||
else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null, priority = 50.0)
|
else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null, priority = 50.0)
|
||||||
}
|
}
|
||||||
if (ci.name.startsWith(prefix, true)) {
|
if (ci.name.startsWith(prefix, true)) {
|
||||||
|
|||||||
@ -34,6 +34,7 @@ object DocLookupUtils {
|
|||||||
is MiniFunDecl -> "Function"
|
is MiniFunDecl -> "Function"
|
||||||
is MiniClassDecl -> "Class"
|
is MiniClassDecl -> "Class"
|
||||||
is MiniEnumDecl -> "Enum"
|
is MiniEnumDecl -> "Enum"
|
||||||
|
is MiniTypeAliasDecl -> "TypeAlias"
|
||||||
is MiniValDecl -> if (d.mutable) "Variable" else "Value"
|
is MiniValDecl -> if (d.mutable) "Variable" else "Value"
|
||||||
}
|
}
|
||||||
return d.name to kind
|
return d.name to kind
|
||||||
@ -87,6 +88,7 @@ object DocLookupUtils {
|
|||||||
val kind = when (m) {
|
val kind = when (m) {
|
||||||
is MiniMemberFunDecl -> "Function"
|
is MiniMemberFunDecl -> "Function"
|
||||||
is MiniMemberValDecl -> if (m.isStatic) "Value" else (if (m.mutable) "Variable" else "Value")
|
is MiniMemberValDecl -> if (m.isStatic) "Value" else (if (m.mutable) "Variable" else "Value")
|
||||||
|
is MiniMemberTypeAliasDecl -> "TypeAlias"
|
||||||
is MiniInitDecl -> "Initializer"
|
is MiniInitDecl -> "Initializer"
|
||||||
}
|
}
|
||||||
return m.name to kind
|
return m.name to kind
|
||||||
@ -119,6 +121,7 @@ object DocLookupUtils {
|
|||||||
return when (d) {
|
return when (d) {
|
||||||
is MiniValDecl -> d.type ?: if (text != null && imported != null) inferTypeRefForVal(d, text, imported, mini) else null
|
is MiniValDecl -> d.type ?: if (text != null && imported != null) inferTypeRefForVal(d, text, imported, mini) else null
|
||||||
is MiniFunDecl -> d.returnType
|
is MiniFunDecl -> d.returnType
|
||||||
|
is MiniTypeAliasDecl -> d.target
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,6 +145,7 @@ object DocLookupUtils {
|
|||||||
is MiniMemberValDecl -> m.type ?: if (text != null && imported != null) {
|
is MiniMemberValDecl -> m.type ?: if (text != null && imported != null) {
|
||||||
inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
|
inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
|
||||||
} else null
|
} else null
|
||||||
|
is MiniMemberTypeAliasDecl -> m.target
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@ -436,7 +440,10 @@ object DocLookupUtils {
|
|||||||
val type = findTypeByRange(mini, sym.name, sym.declStart, text, imported)
|
val type = findTypeByRange(mini, sym.name, sym.declStart, text, imported)
|
||||||
simpleClassNameOf(type)?.let { return it }
|
simpleClassNameOf(type)?.let { return it }
|
||||||
// if it's a class/enum, return its name directly
|
// if it's a class/enum, return its name directly
|
||||||
if (sym.kind == net.sergeych.lyng.binding.SymbolKind.Class || sym.kind == net.sergeych.lyng.binding.SymbolKind.Enum) return sym.name
|
if (sym.kind == net.sergeych.lyng.binding.SymbolKind.Class ||
|
||||||
|
sym.kind == net.sergeych.lyng.binding.SymbolKind.Enum ||
|
||||||
|
sym.kind == net.sergeych.lyng.binding.SymbolKind.TypeAlias
|
||||||
|
) return sym.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -451,6 +458,7 @@ object DocLookupUtils {
|
|||||||
return when (d) {
|
return when (d) {
|
||||||
is MiniClassDecl -> d.name
|
is MiniClassDecl -> d.name
|
||||||
is MiniEnumDecl -> d.name
|
is MiniEnumDecl -> d.name
|
||||||
|
is MiniTypeAliasDecl -> d.name
|
||||||
is MiniValDecl -> simpleClassNameOf(d.type ?: inferTypeRefForVal(d, text, imported, mini))
|
is MiniValDecl -> simpleClassNameOf(d.type ?: inferTypeRefForVal(d, text, imported, mini))
|
||||||
is MiniFunDecl -> simpleClassNameOf(d.returnType)
|
is MiniFunDecl -> simpleClassNameOf(d.returnType)
|
||||||
}
|
}
|
||||||
@ -565,6 +573,7 @@ object DocLookupUtils {
|
|||||||
val rt = when (mm) {
|
val rt = when (mm) {
|
||||||
is MiniMemberFunDecl -> mm.returnType
|
is MiniMemberFunDecl -> mm.returnType
|
||||||
is MiniMemberValDecl -> mm.type
|
is MiniMemberValDecl -> mm.type
|
||||||
|
is MiniMemberTypeAliasDecl -> mm.target
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
return simpleClassNameOf(rt)
|
return simpleClassNameOf(rt)
|
||||||
@ -580,8 +589,10 @@ object DocLookupUtils {
|
|||||||
val rt = when (m) {
|
val rt = when (m) {
|
||||||
is MiniMemberFunDecl -> m.returnType
|
is MiniMemberFunDecl -> m.returnType
|
||||||
is MiniMemberValDecl -> m.type
|
is MiniMemberValDecl -> m.type
|
||||||
|
is MiniMemberTypeAliasDecl -> m.target
|
||||||
is MiniFunDecl -> m.returnType
|
is MiniFunDecl -> m.returnType
|
||||||
is MiniValDecl -> m.type
|
is MiniValDecl -> m.type
|
||||||
|
is MiniTypeAliasDecl -> m.target
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
simpleClassNameOf(rt)
|
simpleClassNameOf(rt)
|
||||||
@ -921,6 +932,7 @@ object DocLookupUtils {
|
|||||||
return when (val m = resolved.second) {
|
return when (val m = resolved.second) {
|
||||||
is MiniMemberFunDecl -> m.returnType
|
is MiniMemberFunDecl -> m.returnType
|
||||||
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini)
|
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini)
|
||||||
|
is MiniMemberTypeAliasDecl -> m.target
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -943,6 +955,7 @@ object DocLookupUtils {
|
|||||||
is MiniEnumDecl -> syntheticTypeRef(d.name)
|
is MiniEnumDecl -> syntheticTypeRef(d.name)
|
||||||
is MiniValDecl -> d.type ?: inferTypeRefForVal(d, fullText, imported, mini)
|
is MiniValDecl -> d.type ?: inferTypeRefForVal(d, fullText, imported, mini)
|
||||||
is MiniFunDecl -> d.returnType
|
is MiniFunDecl -> d.returnType
|
||||||
|
is MiniTypeAliasDecl -> d.target
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -241,6 +241,17 @@ data class MiniEnumDecl(
|
|||||||
val entryPositions: List<Pos> = emptyList()
|
val entryPositions: List<Pos> = emptyList()
|
||||||
) : MiniDecl
|
) : MiniDecl
|
||||||
|
|
||||||
|
data class MiniTypeAliasDecl(
|
||||||
|
override val range: MiniRange,
|
||||||
|
override val name: String,
|
||||||
|
val typeParams: List<String>,
|
||||||
|
val target: MiniTypeRef?,
|
||||||
|
override val doc: MiniDoc?,
|
||||||
|
override val nameStart: Pos,
|
||||||
|
override val isExtern: Boolean = false,
|
||||||
|
override val isStatic: Boolean = false,
|
||||||
|
) : MiniDecl
|
||||||
|
|
||||||
data class MiniCtorField(
|
data class MiniCtorField(
|
||||||
val name: String,
|
val name: String,
|
||||||
val mutable: Boolean,
|
val mutable: Boolean,
|
||||||
@ -290,6 +301,17 @@ data class MiniMemberValDecl(
|
|||||||
override val isExtern: Boolean = false,
|
override val isExtern: Boolean = false,
|
||||||
) : MiniMemberDecl
|
) : MiniMemberDecl
|
||||||
|
|
||||||
|
data class MiniMemberTypeAliasDecl(
|
||||||
|
override val range: MiniRange,
|
||||||
|
override val name: String,
|
||||||
|
val typeParams: List<String>,
|
||||||
|
val target: MiniTypeRef?,
|
||||||
|
override val doc: MiniDoc?,
|
||||||
|
override val nameStart: Pos,
|
||||||
|
override val isStatic: Boolean = false,
|
||||||
|
override val isExtern: Boolean = false,
|
||||||
|
) : MiniMemberDecl
|
||||||
|
|
||||||
data class MiniInitDecl(
|
data class MiniInitDecl(
|
||||||
override val range: MiniRange,
|
override val range: MiniRange,
|
||||||
override val nameStart: Pos,
|
override val nameStart: Pos,
|
||||||
@ -319,6 +341,7 @@ interface MiniAstSink {
|
|||||||
fun onInitDecl(node: MiniInitDecl) {}
|
fun onInitDecl(node: MiniInitDecl) {}
|
||||||
fun onClassDecl(node: MiniClassDecl) {}
|
fun onClassDecl(node: MiniClassDecl) {}
|
||||||
fun onEnumDecl(node: MiniEnumDecl) {}
|
fun onEnumDecl(node: MiniEnumDecl) {}
|
||||||
|
fun onTypeAliasDecl(node: MiniTypeAliasDecl) {}
|
||||||
|
|
||||||
fun onBlock(node: MiniBlock) {}
|
fun onBlock(node: MiniBlock) {}
|
||||||
fun onIdentifier(node: MiniIdentifier) {}
|
fun onIdentifier(node: MiniIdentifier) {}
|
||||||
@ -489,6 +512,41 @@ class MiniAstBuilder : MiniAstSink {
|
|||||||
lastDoc = null
|
lastDoc = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTypeAliasDecl(node: MiniTypeAliasDecl) {
|
||||||
|
val attach = node.copy(doc = node.doc ?: lastDoc)
|
||||||
|
val currentClass = classStack.lastOrNull()
|
||||||
|
if (currentClass != null && functionDepth == 0) {
|
||||||
|
val member = MiniMemberTypeAliasDecl(
|
||||||
|
range = attach.range,
|
||||||
|
name = attach.name,
|
||||||
|
typeParams = attach.typeParams,
|
||||||
|
target = attach.target,
|
||||||
|
doc = attach.doc,
|
||||||
|
nameStart = attach.nameStart,
|
||||||
|
isStatic = attach.isStatic,
|
||||||
|
isExtern = attach.isExtern
|
||||||
|
)
|
||||||
|
val existing = currentClass.members.filterIsInstance<MiniMemberTypeAliasDecl>()
|
||||||
|
.find { it.name == attach.name && it.nameStart == attach.nameStart }
|
||||||
|
val updatedMembers = if (existing != null) {
|
||||||
|
currentClass.members.map { if (it === existing) member else it }
|
||||||
|
} else {
|
||||||
|
currentClass.members + member
|
||||||
|
}
|
||||||
|
classStack.removeLast()
|
||||||
|
classStack.addLast(currentClass.copy(members = updatedMembers))
|
||||||
|
} else {
|
||||||
|
val existing = currentScript?.declarations?.find { it.name == attach.name && it.nameStart == attach.nameStart }
|
||||||
|
if (existing != null) {
|
||||||
|
val idx = currentScript?.declarations?.indexOf(existing) ?: -1
|
||||||
|
if (idx >= 0) currentScript?.declarations?.set(idx, attach)
|
||||||
|
} else {
|
||||||
|
currentScript?.declarations?.add(attach)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastDoc = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBlock(node: MiniBlock) {
|
override fun onBlock(node: MiniBlock) {
|
||||||
blocks.addLast(node)
|
blocks.addLast(node)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,467 @@
|
|||||||
|
/*
|
||||||
|
* 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.tools
|
||||||
|
|
||||||
|
import net.sergeych.lyng.*
|
||||||
|
import net.sergeych.lyng.binding.Binder
|
||||||
|
import net.sergeych.lyng.binding.BindingSnapshot
|
||||||
|
import net.sergeych.lyng.binding.SymbolKind
|
||||||
|
import net.sergeych.lyng.bytecode.BytecodeStatement
|
||||||
|
import net.sergeych.lyng.bytecode.CmdDisassembler
|
||||||
|
import net.sergeych.lyng.format.LyngFormatConfig
|
||||||
|
import net.sergeych.lyng.format.LyngFormatter
|
||||||
|
import net.sergeych.lyng.highlight.HighlightSpan
|
||||||
|
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
|
||||||
|
import net.sergeych.lyng.highlight.TextRange
|
||||||
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
|
import net.sergeych.lyng.miniast.*
|
||||||
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
|
import net.sergeych.lyng.pacman.ImportProvider
|
||||||
|
import net.sergeych.lyng.resolution.ResolutionCollector
|
||||||
|
import net.sergeych.lyng.resolution.ResolutionReport
|
||||||
|
|
||||||
|
data class LyngAnalysisRequest(
|
||||||
|
val text: String,
|
||||||
|
val fileName: String = "<snippet>",
|
||||||
|
val importProvider: ImportProvider = Script.defaultImportManager,
|
||||||
|
val seedScope: Scope? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class LyngDiagnosticSeverity { Error, Warning }
|
||||||
|
|
||||||
|
data class LyngDiagnostic(
|
||||||
|
val message: String,
|
||||||
|
val severity: LyngDiagnosticSeverity,
|
||||||
|
val range: TextRange? = null,
|
||||||
|
val pos: Pos? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LyngAnalysisResult(
|
||||||
|
val source: Source,
|
||||||
|
val text: String,
|
||||||
|
val mini: MiniScript?,
|
||||||
|
val binding: BindingSnapshot?,
|
||||||
|
val resolution: ResolutionReport?,
|
||||||
|
val importedModules: List<String>,
|
||||||
|
val diagnostics: List<LyngDiagnostic>,
|
||||||
|
val lexicalHighlights: List<HighlightSpan>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LyngSymbolTarget(
|
||||||
|
val name: String,
|
||||||
|
val kind: SymbolKind,
|
||||||
|
val range: TextRange,
|
||||||
|
val containerName: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LyngSymbolInfo(
|
||||||
|
val target: LyngSymbolTarget,
|
||||||
|
val signature: String? = null,
|
||||||
|
val doc: MiniDoc? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class LyngSemanticKind {
|
||||||
|
Function,
|
||||||
|
Class,
|
||||||
|
Enum,
|
||||||
|
TypeAlias,
|
||||||
|
Value,
|
||||||
|
Variable,
|
||||||
|
Parameter,
|
||||||
|
TypeRef,
|
||||||
|
EnumConstant
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LyngSemanticSpan(
|
||||||
|
val range: TextRange,
|
||||||
|
val kind: LyngSemanticKind
|
||||||
|
)
|
||||||
|
|
||||||
|
object LyngLanguageTools {
|
||||||
|
|
||||||
|
suspend fun analyze(request: LyngAnalysisRequest): LyngAnalysisResult {
|
||||||
|
val source = Source(request.fileName, request.text)
|
||||||
|
val miniSink = MiniAstBuilder()
|
||||||
|
val resolutionCollector = ResolutionCollector(source.fileName)
|
||||||
|
val diagnostics = ArrayList<LyngDiagnostic>()
|
||||||
|
|
||||||
|
try {
|
||||||
|
Compiler.compileWithResolution(
|
||||||
|
source,
|
||||||
|
request.importProvider,
|
||||||
|
miniSink = miniSink,
|
||||||
|
resolutionSink = resolutionCollector,
|
||||||
|
useBytecodeStatements = false,
|
||||||
|
allowUnresolvedRefs = true,
|
||||||
|
seedScope = request.seedScope
|
||||||
|
)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
val pos = (t as? net.sergeych.lyng.ScriptError)?.pos
|
||||||
|
diagnostics += LyngDiagnostic(
|
||||||
|
message = t.message ?: t.toString(),
|
||||||
|
severity = LyngDiagnosticSeverity.Error,
|
||||||
|
range = pos?.let { posToRange(source, it) },
|
||||||
|
pos = pos
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mini = miniSink.build()
|
||||||
|
val binding = mini?.let { Binder.bind(request.text, it) }
|
||||||
|
val report = try { resolutionCollector.buildReport() } catch (_: Throwable) { null }
|
||||||
|
|
||||||
|
report?.errors?.forEach { err ->
|
||||||
|
diagnostics += LyngDiagnostic(
|
||||||
|
message = err.message,
|
||||||
|
severity = LyngDiagnosticSeverity.Error,
|
||||||
|
range = posToRange(source, err.pos),
|
||||||
|
pos = err.pos
|
||||||
|
)
|
||||||
|
}
|
||||||
|
report?.warnings?.forEach { warn ->
|
||||||
|
diagnostics += LyngDiagnostic(
|
||||||
|
message = warn.message,
|
||||||
|
severity = LyngDiagnosticSeverity.Warning,
|
||||||
|
range = posToRange(source, warn.pos),
|
||||||
|
pos = warn.pos
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val imports = when {
|
||||||
|
mini != null -> DocLookupUtils.canonicalImportedModules(mini, request.text)
|
||||||
|
else -> DocLookupUtils.extractImportsFromText(request.text).toMutableList().apply { add("lyng.stdlib") }.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
val lexical = try { SimpleLyngHighlighter().highlight(request.text) } catch (_: Throwable) { emptyList() }
|
||||||
|
|
||||||
|
return LyngAnalysisResult(
|
||||||
|
source = source,
|
||||||
|
text = request.text,
|
||||||
|
mini = mini,
|
||||||
|
binding = binding,
|
||||||
|
resolution = report,
|
||||||
|
importedModules = imports,
|
||||||
|
diagnostics = diagnostics,
|
||||||
|
lexicalHighlights = lexical
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun analyze(text: String, fileName: String = "<snippet>"): LyngAnalysisResult =
|
||||||
|
analyze(LyngAnalysisRequest(text = text, fileName = fileName))
|
||||||
|
|
||||||
|
fun format(text: String, config: LyngFormatConfig = LyngFormatConfig()): String =
|
||||||
|
LyngFormatter.format(text, config)
|
||||||
|
|
||||||
|
fun lexicalHighlights(text: String): List<HighlightSpan> =
|
||||||
|
SimpleLyngHighlighter().highlight(text)
|
||||||
|
|
||||||
|
fun semanticHighlights(analysis: LyngAnalysisResult): List<LyngSemanticSpan> {
|
||||||
|
val mini = analysis.mini ?: return emptyList()
|
||||||
|
val source = analysis.source
|
||||||
|
val out = ArrayList<LyngSemanticSpan>(128)
|
||||||
|
val covered = HashSet<Pair<Int, Int>>()
|
||||||
|
|
||||||
|
fun addRange(start: Int, end: Int, kind: LyngSemanticKind) {
|
||||||
|
if (start < 0 || end <= start || end > analysis.text.length) return
|
||||||
|
val key = start to end
|
||||||
|
if (covered.add(key)) out += LyngSemanticSpan(TextRange(start, end), kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addName(pos: Pos, name: String, kind: LyngSemanticKind) {
|
||||||
|
val s = source.offsetOf(pos)
|
||||||
|
addRange(s, s + name.length, kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTypeSegments(t: MiniTypeRef?) {
|
||||||
|
when (t) {
|
||||||
|
is MiniTypeName -> t.segments.forEach { seg ->
|
||||||
|
addName(seg.range.start, seg.name, LyngSemanticKind.TypeRef)
|
||||||
|
}
|
||||||
|
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 -> {
|
||||||
|
addRange(source.offsetOf(t.range.start), source.offsetOf(t.range.end), LyngSemanticKind.TypeRef)
|
||||||
|
}
|
||||||
|
is MiniTypeUnion -> {
|
||||||
|
t.options.forEach { addTypeSegments(it) }
|
||||||
|
}
|
||||||
|
is MiniTypeIntersection -> {
|
||||||
|
t.options.forEach { addTypeSegments(it) }
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addDeclTypeSegments(d: MiniDecl) {
|
||||||
|
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) }
|
||||||
|
d.members.forEach { m ->
|
||||||
|
when (m) {
|
||||||
|
is MiniMemberFunDecl -> {
|
||||||
|
addTypeSegments(m.returnType)
|
||||||
|
m.params.forEach { addTypeSegments(it.type) }
|
||||||
|
}
|
||||||
|
is MiniMemberValDecl -> addTypeSegments(m.type)
|
||||||
|
is MiniMemberTypeAliasDecl -> addTypeSegments(m.target)
|
||||||
|
is MiniInitDecl -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MiniEnumDecl -> {}
|
||||||
|
is MiniTypeAliasDecl -> addTypeSegments(d.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (d in mini.declarations) {
|
||||||
|
when (d) {
|
||||||
|
is MiniFunDecl -> addName(d.nameStart, d.name, LyngSemanticKind.Function)
|
||||||
|
is MiniClassDecl -> addName(d.nameStart, d.name, LyngSemanticKind.Class)
|
||||||
|
is MiniEnumDecl -> addName(d.nameStart, d.name, LyngSemanticKind.Enum)
|
||||||
|
is MiniValDecl -> addName(d.nameStart, d.name, if (d.mutable) LyngSemanticKind.Variable else LyngSemanticKind.Value)
|
||||||
|
is MiniTypeAliasDecl -> addName(d.nameStart, d.name, LyngSemanticKind.TypeAlias)
|
||||||
|
}
|
||||||
|
addDeclTypeSegments(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
mini.imports.forEach { imp ->
|
||||||
|
imp.segments.forEach { seg ->
|
||||||
|
addRange(source.offsetOf(seg.range.start), source.offsetOf(seg.range.end), LyngSemanticKind.TypeRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addParams(params: List<MiniParam>) {
|
||||||
|
params.forEach { p -> addName(p.nameStart, p.name, LyngSemanticKind.Parameter) }
|
||||||
|
}
|
||||||
|
mini.declarations.forEach { d ->
|
||||||
|
when (d) {
|
||||||
|
is MiniFunDecl -> addParams(d.params)
|
||||||
|
is MiniClassDecl -> d.members.filterIsInstance<MiniMemberFunDecl>().forEach { addParams(it.params) }
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mini.declarations.filterIsInstance<MiniEnumDecl>().forEach { en ->
|
||||||
|
en.entries.zip(en.entryPositions).forEach { (name, pos) ->
|
||||||
|
addName(pos, name, LyngSemanticKind.EnumConstant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis.binding?.let { binding ->
|
||||||
|
val declKeys = binding.symbols.map { it.declStart to it.declEnd }.toSet()
|
||||||
|
for (ref in binding.references) {
|
||||||
|
if (declKeys.contains(ref.start to ref.end)) continue
|
||||||
|
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId } ?: continue
|
||||||
|
val kind = when (sym.kind) {
|
||||||
|
SymbolKind.Function -> LyngSemanticKind.Function
|
||||||
|
SymbolKind.Class -> LyngSemanticKind.Class
|
||||||
|
SymbolKind.Enum -> LyngSemanticKind.Enum
|
||||||
|
SymbolKind.TypeAlias -> LyngSemanticKind.TypeAlias
|
||||||
|
SymbolKind.Value -> LyngSemanticKind.Value
|
||||||
|
SymbolKind.Variable -> LyngSemanticKind.Variable
|
||||||
|
SymbolKind.Parameter -> LyngSemanticKind.Parameter
|
||||||
|
}
|
||||||
|
addRange(ref.start, ref.end, kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.sortedBy { it.range.start }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun completions(text: String, offset: Int, analysis: LyngAnalysisResult? = null): List<CompletionItem> {
|
||||||
|
val mini = analysis?.mini
|
||||||
|
val binding = analysis?.binding
|
||||||
|
StdlibDocsBootstrap.ensure()
|
||||||
|
return CompletionEngineLight.completeSuspend(text, offset, mini, binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun definitionAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolTarget? {
|
||||||
|
val binding = analysis.binding ?: return null
|
||||||
|
val sym = binding.symbols.firstOrNull { offset in it.declStart until it.declEnd }
|
||||||
|
?: binding.references.firstOrNull { offset in it.start until it.end }
|
||||||
|
?.let { ref -> binding.symbols.firstOrNull { it.id == ref.symbolId } }
|
||||||
|
?: return null
|
||||||
|
val containerName = sym.containerId?.let { cid -> binding.symbols.firstOrNull { it.id == cid }?.name }
|
||||||
|
return LyngSymbolTarget(
|
||||||
|
name = sym.name,
|
||||||
|
kind = sym.kind,
|
||||||
|
range = TextRange(sym.declStart, sym.declEnd),
|
||||||
|
containerName = containerName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun usagesAt(analysis: LyngAnalysisResult, offset: Int, includeDeclaration: Boolean = false): List<TextRange> {
|
||||||
|
val binding = analysis.binding ?: return emptyList()
|
||||||
|
val sym = binding.symbols.firstOrNull { offset in it.declStart until it.declEnd }
|
||||||
|
?: binding.references.firstOrNull { offset in it.start until it.end }
|
||||||
|
?.let { ref -> binding.symbols.firstOrNull { it.id == ref.symbolId } }
|
||||||
|
?: return emptyList()
|
||||||
|
val ranges = binding.references.filter { it.symbolId == sym.id }.map { TextRange(it.start, it.end) }.toMutableList()
|
||||||
|
if (includeDeclaration) ranges.add(TextRange(sym.declStart, sym.declEnd))
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
|
|
||||||
|
fun docAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolInfo? {
|
||||||
|
StdlibDocsBootstrap.ensure()
|
||||||
|
val target = definitionAt(analysis, offset) ?: return null
|
||||||
|
val mini = analysis.mini
|
||||||
|
val imported = analysis.importedModules
|
||||||
|
val name = target.name
|
||||||
|
|
||||||
|
val local = mini?.let { findLocalDecl(it, analysis.text, name, target.range.start) }
|
||||||
|
if (local != null) {
|
||||||
|
val signature = signatureOf(local.first, local.second)
|
||||||
|
return LyngSymbolInfo(target, signature = signature, doc = local.first.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.containerName != null) {
|
||||||
|
val member = DocLookupUtils.resolveMemberWithInheritance(imported, target.containerName, name, mini)
|
||||||
|
if (member != null) {
|
||||||
|
val signature = signatureOf(member.second, member.first)
|
||||||
|
return LyngSymbolInfo(target, signature = signature, doc = member.second.doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (mod in imported) {
|
||||||
|
val decl = BuiltinDocRegistry.docsForModule(mod).firstOrNull { it.name == name }
|
||||||
|
if (decl != null) {
|
||||||
|
val signature = signatureOf(decl, null)
|
||||||
|
return LyngSymbolInfo(target, signature = signature, doc = decl.doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LyngSymbolInfo(target, signature = null, doc = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun disassembleSymbol(
|
||||||
|
code: String,
|
||||||
|
symbol: String,
|
||||||
|
importProvider: ImportProvider = Script.defaultImportManager
|
||||||
|
): String {
|
||||||
|
val script = Compiler.compile(code.toSource(), importProvider)
|
||||||
|
val scope = importProvider.newStdScope(Pos.builtIn)
|
||||||
|
script.execute(scope)
|
||||||
|
val (container, member) = splitMember(symbol)
|
||||||
|
if (member == null) return disassembleFromScope(scope, container)
|
||||||
|
val rec = scope.get(container) ?: return "$symbol is not found"
|
||||||
|
val cls = rec.value as? ObjClass ?: return "$container is not a class"
|
||||||
|
val classScope = cls.classScope ?: return "$container has no class scope"
|
||||||
|
return disassembleFromScope(classScope, member)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun posToRange(source: Source, pos: Pos): TextRange {
|
||||||
|
val s = source.offsetOf(pos)
|
||||||
|
return TextRange(s, (s + 1).coerceAtMost(source.text.length))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findLocalDecl(mini: MiniScript, text: String, name: String, declStart: Int): Pair<MiniNamedDecl, String?>? {
|
||||||
|
val src = mini.range.start.source
|
||||||
|
fun matches(p: Pos, len: Int) = src.offsetOf(p).let { s -> s == declStart && len > 0 }
|
||||||
|
|
||||||
|
for (d in mini.declarations) {
|
||||||
|
if (d.name == name && matches(d.nameStart, d.name.length)) return d to null
|
||||||
|
if (d is MiniClassDecl) {
|
||||||
|
d.members.forEach { m ->
|
||||||
|
if (m.name == name && matches(m.nameStart, m.name.length)) return m to d.name
|
||||||
|
}
|
||||||
|
d.ctorFields.firstOrNull { it.name == name && matches(it.nameStart, it.name.length) }?.let {
|
||||||
|
return DocLookupUtils.toMemberVal(it) to d.name
|
||||||
|
}
|
||||||
|
d.classFields.firstOrNull { it.name == name && matches(it.nameStart, it.name.length) }?.let {
|
||||||
|
return DocLookupUtils.toMemberVal(it) to d.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun signatureOf(decl: MiniNamedDecl, ownerClass: String?): String? {
|
||||||
|
val owner = ownerClass?.let { "$it." } ?: ""
|
||||||
|
return when (decl) {
|
||||||
|
is MiniFunDecl -> {
|
||||||
|
val params = decl.params.joinToString(", ") { it.name + typeSuffix(it.type) }
|
||||||
|
val ret = typeSuffix(decl.returnType)
|
||||||
|
"fun $owner${decl.name}($params)$ret"
|
||||||
|
}
|
||||||
|
is MiniMemberFunDecl -> {
|
||||||
|
val params = decl.params.joinToString(", ") { it.name + typeSuffix(it.type) }
|
||||||
|
val ret = typeSuffix(decl.returnType)
|
||||||
|
"fun $owner${decl.name}($params)$ret"
|
||||||
|
}
|
||||||
|
is MiniValDecl -> {
|
||||||
|
val kw = if (decl.mutable) "var" else "val"
|
||||||
|
"$kw $owner${decl.name}${typeSuffix(decl.type)}"
|
||||||
|
}
|
||||||
|
is MiniMemberValDecl -> {
|
||||||
|
val kw = if (decl.mutable) "var" else "val"
|
||||||
|
"$kw $owner${decl.name}${typeSuffix(decl.type)}"
|
||||||
|
}
|
||||||
|
is MiniClassDecl -> {
|
||||||
|
val bases = if (decl.bases.isEmpty()) "" else ": " + decl.bases.joinToString(", ")
|
||||||
|
"class ${decl.name}$bases"
|
||||||
|
}
|
||||||
|
is MiniEnumDecl -> "enum ${decl.name}"
|
||||||
|
is MiniTypeAliasDecl -> {
|
||||||
|
val tp = if (decl.typeParams.isEmpty()) "" else "<" + decl.typeParams.joinToString(", ") + ">"
|
||||||
|
"type ${decl.name}$tp = ${DocLookupUtils.typeOf(decl.target)}"
|
||||||
|
}
|
||||||
|
is MiniMemberTypeAliasDecl -> {
|
||||||
|
val tp = if (decl.typeParams.isEmpty()) "" else "<" + decl.typeParams.joinToString(", ") + ">"
|
||||||
|
"type $owner${decl.name}$tp = ${DocLookupUtils.typeOf(decl.target)}"
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun typeSuffix(type: MiniTypeRef?): String =
|
||||||
|
type?.let { ": ${DocLookupUtils.typeOf(it)}" } ?: ""
|
||||||
|
|
||||||
|
private fun splitMember(symbol: String): Pair<String, String?> {
|
||||||
|
val idx = symbol.lastIndexOf('.')
|
||||||
|
return if (idx >= 0 && idx + 1 < symbol.length) {
|
||||||
|
symbol.substring(0, idx) to symbol.substring(idx + 1)
|
||||||
|
} else {
|
||||||
|
symbol to null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disassembleFromScope(scope: Scope, name: String): String {
|
||||||
|
val record = scope.get(name) ?: return "$name is not found"
|
||||||
|
val stmt = record.value as? net.sergeych.lyng.Statement ?: return "$name is not a compiled body"
|
||||||
|
val bytecode = (stmt as? BytecodeStatement)?.bytecodeFunction()
|
||||||
|
?: (stmt as? BytecodeBodyProvider)?.bytecodeBody()?.bytecodeFunction()
|
||||||
|
?: return "$name is not a compiled body"
|
||||||
|
return CmdDisassembler.disassemble(bytecode)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -131,4 +131,16 @@ class StdlibTest {
|
|||||||
assertEquals(31, p.age)
|
assertEquals(31, p.age)
|
||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// fun testFunFromSample() = runTest {
|
||||||
|
// range should be iterable if it is intrange
|
||||||
|
// eval("""
|
||||||
|
// val data = 1..5 // or [1, 2, 3, 4, 5]
|
||||||
|
// fun test() {
|
||||||
|
// data.filter { it % 2 == 0 }.map { it * it }
|
||||||
|
// }
|
||||||
|
// test()
|
||||||
|
// """.trimIndent())
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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.tools
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.miniast.MiniClassDecl
|
||||||
|
import net.sergeych.lyng.miniast.MiniMemberTypeAliasDecl
|
||||||
|
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class LyngLanguageToolsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun languageTools_dryRun_rootLyng_hasNoErrors() = runTest {
|
||||||
|
val res = LyngLanguageTools.analyze(rootLyng, "root.lyng")
|
||||||
|
assertNotNull(res.mini, "root.lyng should build Mini-AST")
|
||||||
|
assertTrue(res.lexicalHighlights.isNotEmpty(), "root.lyng should produce lexical highlights")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun languageTools_tracks_inner_and_type_aliases() = runTest {
|
||||||
|
val code = """
|
||||||
|
/** Box docs */
|
||||||
|
type Box<T> = List<T?>
|
||||||
|
|
||||||
|
class Outer {
|
||||||
|
type Alias = Box<Int>
|
||||||
|
class Inner {
|
||||||
|
val value: Int = 1
|
||||||
|
}
|
||||||
|
enum Kind { A, B }
|
||||||
|
object Obj { val flag = true }
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
val res = LyngLanguageTools.analyze(code, "inner.lyng")
|
||||||
|
val mini = res.mini
|
||||||
|
assertNotNull(mini, "Mini-AST must be built")
|
||||||
|
|
||||||
|
val outer = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "Outer" }
|
||||||
|
assertNotNull(outer, "Outer class should be captured")
|
||||||
|
val aliasMember = outer.members.filterIsInstance<MiniMemberTypeAliasDecl>().firstOrNull { it.name == "Alias" }
|
||||||
|
assertNotNull(aliasMember, "Inner type alias should be captured as a class member")
|
||||||
|
|
||||||
|
val sem = LyngLanguageTools.semanticHighlights(res)
|
||||||
|
assertTrue(sem.any { it.kind == LyngSemanticKind.TypeAlias }, "Type aliases should be part of semantic highlights")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun languageTools_completion_and_docs_for_type_alias() = runTest {
|
||||||
|
val code = """
|
||||||
|
/** Box docs */
|
||||||
|
type Box<T> = List<T>
|
||||||
|
val x: Box<Int> = [1]
|
||||||
|
<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val caret = code.indexOf("<caret>")
|
||||||
|
val text = code.replace("<caret>", "")
|
||||||
|
val res = LyngLanguageTools.analyze(text, "alias.lyng")
|
||||||
|
val items = LyngLanguageTools.completions(text, caret, res)
|
||||||
|
assertTrue(items.any { it.name == "Box" }, "Completion should include Box type alias")
|
||||||
|
|
||||||
|
val aliasOffset = text.indexOf("Box<Int>")
|
||||||
|
val doc = LyngLanguageTools.docAt(res, aliasOffset)
|
||||||
|
assertNotNull(doc, "Docs should resolve for Box")
|
||||||
|
assertEquals("Box", doc.target.name)
|
||||||
|
assertEquals("Box docs", doc.doc?.summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun languageTools_definition_and_usages() = runTest {
|
||||||
|
val code = """
|
||||||
|
val answer = 42
|
||||||
|
println(answer)
|
||||||
|
answer
|
||||||
|
""".trimIndent()
|
||||||
|
val res = LyngLanguageTools.analyze(code, "usage.lyng")
|
||||||
|
val usageOffset = code.lastIndexOf("answer")
|
||||||
|
val def = LyngLanguageTools.definitionAt(res, usageOffset)
|
||||||
|
assertNotNull(def, "Definition should resolve")
|
||||||
|
assertEquals("answer", def.name)
|
||||||
|
val usages = LyngLanguageTools.usagesAt(res, usageOffset)
|
||||||
|
assertTrue(usages.size >= 2, "Expected at least two usages, got ${usages.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun languageTools_disassemble_symbol() = runTest {
|
||||||
|
val code = """
|
||||||
|
fun add(a: Int, b: Int): Int {
|
||||||
|
a + b
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
val dis = LyngLanguageTools.disassembleSymbol(code, "add")
|
||||||
|
assertTrue(!dis.contains("not a compiled body"), "Disassembly should be produced, got: $dis")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,9 +19,17 @@ package net.sergeych.lyngweb
|
|||||||
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import kotlinx.browser.window
|
import kotlinx.browser.window
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.sergeych.lyng.highlight.TextRange
|
||||||
|
import net.sergeych.lyng.miniast.CompletionItem
|
||||||
|
import net.sergeych.lyng.tools.LyngAnalysisResult
|
||||||
|
import net.sergeych.lyng.tools.LyngSymbolInfo
|
||||||
|
import net.sergeych.lyng.tools.LyngSymbolTarget
|
||||||
import org.jetbrains.compose.web.attributes.placeholder
|
import org.jetbrains.compose.web.attributes.placeholder
|
||||||
import org.jetbrains.compose.web.dom.Div
|
import org.jetbrains.compose.web.dom.Div
|
||||||
import org.jetbrains.compose.web.events.SyntheticKeyboardEvent
|
import org.jetbrains.compose.web.events.SyntheticKeyboardEvent
|
||||||
|
import org.w3c.dom.CanvasRenderingContext2D
|
||||||
|
import org.w3c.dom.HTMLCanvasElement
|
||||||
import org.w3c.dom.HTMLElement
|
import org.w3c.dom.HTMLElement
|
||||||
import org.w3c.dom.HTMLTextAreaElement
|
import org.w3c.dom.HTMLTextAreaElement
|
||||||
|
|
||||||
@ -47,12 +55,20 @@ fun EditorWithOverlay(
|
|||||||
setCode: (String) -> Unit,
|
setCode: (String) -> Unit,
|
||||||
tabSize: Int = 4,
|
tabSize: Int = 4,
|
||||||
onKeyDown: ((SyntheticKeyboardEvent) -> Unit)? = null,
|
onKeyDown: ((SyntheticKeyboardEvent) -> Unit)? = null,
|
||||||
|
onAnalysisReady: ((LyngAnalysisResult) -> Unit)? = null,
|
||||||
|
onCompletionRequested: ((Int, List<CompletionItem>) -> Unit)? = null,
|
||||||
|
onDefinitionResolved: ((Int, LyngSymbolTarget?) -> Unit)? = null,
|
||||||
|
onUsagesResolved: ((Int, List<TextRange>) -> Unit)? = null,
|
||||||
|
onDocRequested: ((Int, LyngSymbolInfo?) -> Unit)? = null,
|
||||||
// New sizing controls
|
// New sizing controls
|
||||||
minRows: Int = 6,
|
minRows: Int = 6,
|
||||||
maxRows: Int? = null,
|
maxRows: Int? = null,
|
||||||
autoGrow: Boolean = false,
|
autoGrow: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var containerEl by remember { mutableStateOf<HTMLElement?>(null) }
|
||||||
var overlayEl by remember { mutableStateOf<HTMLElement?>(null) }
|
var overlayEl by remember { mutableStateOf<HTMLElement?>(null) }
|
||||||
|
var diagOverlayEl by remember { mutableStateOf<HTMLElement?>(null) }
|
||||||
var taEl by remember { mutableStateOf<HTMLTextAreaElement?>(null) }
|
var taEl by remember { mutableStateOf<HTMLTextAreaElement?>(null) }
|
||||||
var lastGoodHtml by remember { mutableStateOf<String?>(null) }
|
var lastGoodHtml by remember { mutableStateOf<String?>(null) }
|
||||||
var lastGoodText by remember { mutableStateOf<String?>(null) }
|
var lastGoodText by remember { mutableStateOf<String?>(null) }
|
||||||
@ -62,9 +78,16 @@ fun EditorWithOverlay(
|
|||||||
var pendingScrollLeft by remember { mutableStateOf<Double?>(null) }
|
var pendingScrollLeft by remember { mutableStateOf<Double?>(null) }
|
||||||
var cachedLineHeight by remember { mutableStateOf<Double?>(null) }
|
var cachedLineHeight by remember { mutableStateOf<Double?>(null) }
|
||||||
var cachedVInsets by remember { mutableStateOf<Double?>(null) }
|
var cachedVInsets by remember { mutableStateOf<Double?>(null) }
|
||||||
|
var cachedCharWidth by remember { mutableStateOf<Double?>(null) }
|
||||||
|
var lastAnalysis by remember { mutableStateOf<LyngAnalysisResult?>(null) }
|
||||||
|
var lastAnalysisText by remember { mutableStateOf<String?>(null) }
|
||||||
|
var lineStarts by remember { mutableStateOf(IntArray(0)) }
|
||||||
|
var tooltipText by remember { mutableStateOf<String?>(null) }
|
||||||
|
var tooltipX by remember { mutableStateOf<Double?>(null) }
|
||||||
|
var tooltipY by remember { mutableStateOf<Double?>(null) }
|
||||||
|
|
||||||
fun ensureMetrics(ta: HTMLTextAreaElement) {
|
fun ensureMetrics(ta: HTMLTextAreaElement) {
|
||||||
if (cachedLineHeight == null || cachedVInsets == null) {
|
if (cachedLineHeight == null || cachedVInsets == null || cachedCharWidth == null) {
|
||||||
val cs = window.getComputedStyle(ta)
|
val cs = window.getComputedStyle(ta)
|
||||||
val lhStr = cs.getPropertyValue("line-height").trim()
|
val lhStr = cs.getPropertyValue("line-height").trim()
|
||||||
val lh = lhStr.removeSuffix("px").toDoubleOrNull() ?: 20.0
|
val lh = lhStr.removeSuffix("px").toDoubleOrNull() ?: 20.0
|
||||||
@ -78,6 +101,21 @@ fun EditorWithOverlay(
|
|||||||
val bb = parsePx("border-bottom-width")
|
val bb = parsePx("border-bottom-width")
|
||||||
cachedLineHeight = lh
|
cachedLineHeight = lh
|
||||||
cachedVInsets = pt + pb + bt + bb
|
cachedVInsets = pt + pb + bt + bb
|
||||||
|
|
||||||
|
val canvas = window.document.createElement("canvas") as HTMLCanvasElement
|
||||||
|
val ctx = canvas.getContext("2d") as? CanvasRenderingContext2D
|
||||||
|
if (ctx != null) {
|
||||||
|
val fontSize = cs.fontSize
|
||||||
|
val fontFamily = cs.fontFamily
|
||||||
|
val fontWeight = cs.fontWeight
|
||||||
|
val fontStyle = cs.fontStyle
|
||||||
|
ctx.font = "$fontStyle $fontWeight $fontSize $fontFamily"
|
||||||
|
val m = ctx.measureText("M")
|
||||||
|
val w = if (m.width > 0.0) m.width else 8.0
|
||||||
|
cachedCharWidth = w
|
||||||
|
} else {
|
||||||
|
cachedCharWidth = 8.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,14 +140,17 @@ fun EditorWithOverlay(
|
|||||||
ta.style.height = "${target}px"
|
ta.style.height = "${target}px"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update overlay HTML whenever code changes
|
suspend fun ensureAnalysis(text: String): LyngAnalysisResult {
|
||||||
LaunchedEffect(code) {
|
val cached = lastAnalysis
|
||||||
fun clamp(i: Int, lo: Int, hi: Int): Int = if (i < lo) lo else if (i > hi) hi else i
|
val cachedText = lastAnalysisText
|
||||||
fun safeSubstring(text: String, start: Int, end: Int): String {
|
if (cached != null && cachedText == text) return cached
|
||||||
val s = clamp(start, 0, text.length)
|
val analysis = LyngWebTools.analyze(text)
|
||||||
val e = clamp(end, 0, text.length)
|
lastAnalysis = analysis
|
||||||
return if (e <= s) "" else text.substring(s, e)
|
lastAnalysisText = text
|
||||||
|
onAnalysisReady?.invoke(analysis)
|
||||||
|
return analysis
|
||||||
}
|
}
|
||||||
|
|
||||||
fun htmlEscapeLocal(s: String): String = buildString(s.length) {
|
fun htmlEscapeLocal(s: String): String = buildString(s.length) {
|
||||||
for (ch in s) when (ch) {
|
for (ch in s) when (ch) {
|
||||||
'<' -> append("<")
|
'<' -> append("<")
|
||||||
@ -121,6 +162,50 @@ fun EditorWithOverlay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildLineStarts(text: String): IntArray {
|
||||||
|
val starts = ArrayList<Int>(maxOf(1, text.length / 16))
|
||||||
|
starts.add(0)
|
||||||
|
for (i in text.indices) {
|
||||||
|
if (text[i] == '\n') starts.add(i + 1)
|
||||||
|
}
|
||||||
|
return starts.toIntArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun offsetFromMouse(ta: HTMLTextAreaElement, clientX: Double, clientY: Double): Int? {
|
||||||
|
ensureMetrics(ta)
|
||||||
|
val rect = ta.getBoundingClientRect()
|
||||||
|
val lineHeight = cachedLineHeight ?: return null
|
||||||
|
val charWidth = cachedCharWidth ?: return null
|
||||||
|
val cs = window.getComputedStyle(ta)
|
||||||
|
fun parsePx(name: String): Double {
|
||||||
|
val v = cs.getPropertyValue(name).trim().removeSuffix("px").toDoubleOrNull()
|
||||||
|
return v ?: 0.0
|
||||||
|
}
|
||||||
|
val padLeft = parsePx("padding-left") + parsePx("border-left-width")
|
||||||
|
val padTop = parsePx("padding-top") + parsePx("border-top-width")
|
||||||
|
val x = clientX - rect.left + ta.scrollLeft - padLeft
|
||||||
|
val y = clientY - rect.top + ta.scrollTop - padTop
|
||||||
|
if (y < 0) return 0
|
||||||
|
val lineIdx = (y / lineHeight).toInt().coerceAtLeast(0)
|
||||||
|
if (lineStarts.isEmpty()) return 0
|
||||||
|
val actualLineIdx = lineIdx.coerceAtMost(lineStarts.size - 1)
|
||||||
|
val lineStart = lineStarts[actualLineIdx]
|
||||||
|
val lineEnd = if (actualLineIdx + 1 < lineStarts.size) lineStarts[actualLineIdx + 1] - 1 else code.length
|
||||||
|
val lineLen = (lineEnd - lineStart).coerceAtLeast(0)
|
||||||
|
val col = (x / charWidth).toInt().coerceAtLeast(0)
|
||||||
|
val clampedCol = col.coerceAtMost(lineLen)
|
||||||
|
return (lineStart + clampedCol).coerceIn(0, code.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update overlay HTML whenever code changes
|
||||||
|
LaunchedEffect(code) {
|
||||||
|
fun clamp(i: Int, lo: Int, hi: Int): Int = if (i < lo) lo else if (i > hi) hi else i
|
||||||
|
fun safeSubstring(text: String, start: Int, end: Int): String {
|
||||||
|
val s = clamp(start, 0, text.length)
|
||||||
|
val e = clamp(end, 0, text.length)
|
||||||
|
return if (e <= s) "" else text.substring(s, e)
|
||||||
|
}
|
||||||
|
|
||||||
fun trimHtmlToTextPrefix(html: String, prefixChars: Int): String {
|
fun trimHtmlToTextPrefix(html: String, prefixChars: Int): String {
|
||||||
if (prefixChars <= 0) return ""
|
if (prefixChars <= 0) return ""
|
||||||
var i = 0
|
var i = 0
|
||||||
@ -188,10 +273,103 @@ fun EditorWithOverlay(
|
|||||||
val sl = pendingScrollLeft ?: (taEl?.scrollLeft ?: 0.0)
|
val sl = pendingScrollLeft ?: (taEl?.scrollLeft ?: 0.0)
|
||||||
overlayEl?.scrollTop = st
|
overlayEl?.scrollTop = st
|
||||||
overlayEl?.scrollLeft = sl
|
overlayEl?.scrollLeft = sl
|
||||||
|
diagOverlayEl?.scrollTop = st
|
||||||
|
diagOverlayEl?.scrollLeft = sl
|
||||||
pendingScrollTop = null
|
pendingScrollTop = null
|
||||||
pendingScrollLeft = null
|
pendingScrollLeft = null
|
||||||
// If text changed and autoGrow enabled, adjust height
|
// If text changed and autoGrow enabled, adjust height
|
||||||
adjustTextareaHeight()
|
adjustTextareaHeight()
|
||||||
|
lineStarts = buildLineStarts(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildDiagnosticsHtml(
|
||||||
|
text: String,
|
||||||
|
diagnostics: List<net.sergeych.lyng.tools.LyngDiagnostic>
|
||||||
|
): String {
|
||||||
|
if (diagnostics.isEmpty()) return ""
|
||||||
|
val ranges = diagnostics.mapNotNull { d ->
|
||||||
|
val r = d.range ?: return@mapNotNull null
|
||||||
|
if (r.start < 0 || r.endExclusive <= r.start || r.endExclusive > text.length) return@mapNotNull null
|
||||||
|
Triple(r, d.severity, d.message)
|
||||||
|
}.sortedBy { it.first.start }
|
||||||
|
if (ranges.isEmpty()) return ""
|
||||||
|
val out = StringBuilder(text.length + 64)
|
||||||
|
var cursor = 0
|
||||||
|
for ((range, severity, message) in ranges) {
|
||||||
|
if (range.start < cursor) continue
|
||||||
|
if (cursor < range.start) {
|
||||||
|
out.append(htmlEscapeLocal(text.substring(cursor, range.start)))
|
||||||
|
}
|
||||||
|
val color = when (severity) {
|
||||||
|
net.sergeych.lyng.tools.LyngDiagnosticSeverity.Error -> "#dc3545"
|
||||||
|
net.sergeych.lyng.tools.LyngDiagnosticSeverity.Warning -> "#ffc107"
|
||||||
|
}
|
||||||
|
val seg = htmlEscapeLocal(text.substring(range.start, range.endExclusive))
|
||||||
|
val tip = htmlEscapeLocal(message).replace("\"", """)
|
||||||
|
out.append("<span title=\"").append(tip).append("\" style=\"text-decoration-line:underline;text-decoration-style:wavy;")
|
||||||
|
out.append("text-decoration-color:").append(color).append(";\">")
|
||||||
|
out.append(seg)
|
||||||
|
out.append("</span>")
|
||||||
|
cursor = range.endExclusive
|
||||||
|
}
|
||||||
|
if (cursor < text.length) out.append(htmlEscapeLocal(text.substring(cursor)))
|
||||||
|
return out.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun diagnosticMessageAt(offset: Int, analysis: LyngAnalysisResult?): String? {
|
||||||
|
val list = analysis?.diagnostics ?: return null
|
||||||
|
for (d in list) {
|
||||||
|
val r = d.range ?: continue
|
||||||
|
if (offset in r.start until r.endExclusive) return d.message
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCaretTooltip() {
|
||||||
|
val ta = taEl ?: return
|
||||||
|
val offset = ta.selectionStart ?: return
|
||||||
|
val msg = diagnosticMessageAt(offset, lastAnalysis)
|
||||||
|
if (msg.isNullOrBlank()) {
|
||||||
|
ta.removeAttribute("title")
|
||||||
|
} else {
|
||||||
|
ta.setAttribute("title", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHoverTooltip(clientX: Double, clientY: Double) {
|
||||||
|
val ta = taEl ?: return
|
||||||
|
val offset = offsetFromMouse(ta, clientX, clientY) ?: return
|
||||||
|
val msg = diagnosticMessageAt(offset, lastAnalysis)
|
||||||
|
if (msg.isNullOrBlank()) {
|
||||||
|
tooltipText = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val container = containerEl ?: return
|
||||||
|
val rect = container.getBoundingClientRect()
|
||||||
|
tooltipText = msg
|
||||||
|
tooltipX = (clientX - rect.left + 12.0).coerceAtLeast(0.0)
|
||||||
|
tooltipY = (clientY - rect.top + 12.0).coerceAtLeast(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(code, lastAnalysis) {
|
||||||
|
val analysis = lastAnalysis ?: return@LaunchedEffect
|
||||||
|
if (lastAnalysisText != code) {
|
||||||
|
diagOverlayEl?.innerHTML = htmlEscapeLocal(code)
|
||||||
|
updateCaretTooltip()
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
val html = buildDiagnosticsHtml(code, analysis.diagnostics)
|
||||||
|
val content = if (html.isEmpty()) htmlEscapeLocal(code) else html
|
||||||
|
diagOverlayEl?.innerHTML = content
|
||||||
|
updateCaretTooltip()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(code, onAnalysisReady) {
|
||||||
|
if (onAnalysisReady == null) return@LaunchedEffect
|
||||||
|
try {
|
||||||
|
ensureAnalysis(code)
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSelection(start: Int, end: Int = start) {
|
fun setSelection(start: Int, end: Int = start) {
|
||||||
@ -206,6 +384,10 @@ fun EditorWithOverlay(
|
|||||||
// avoid external CSS dependency: ensure base positioning inline
|
// avoid external CSS dependency: ensure base positioning inline
|
||||||
classes("position-relative")
|
classes("position-relative")
|
||||||
attr("style", "position:relative;")
|
attr("style", "position:relative;")
|
||||||
|
ref { it ->
|
||||||
|
containerEl = it
|
||||||
|
onDispose { if (containerEl === it) containerEl = null }
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
// Overlay: highlighted code
|
// Overlay: highlighted code
|
||||||
org.jetbrains.compose.web.dom.Div({
|
org.jetbrains.compose.web.dom.Div({
|
||||||
@ -226,6 +408,23 @@ fun EditorWithOverlay(
|
|||||||
}
|
}
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
|
// Diagnostics overlay: transparent text with wavy underlines
|
||||||
|
org.jetbrains.compose.web.dom.Div({
|
||||||
|
attr(
|
||||||
|
"style",
|
||||||
|
buildString {
|
||||||
|
append("position:absolute; left:0; top:0; right:0; bottom:0;")
|
||||||
|
append("overflow:auto; box-sizing:border-box; white-space:pre-wrap; word-break:break-word; tab-size:")
|
||||||
|
append(tabSize)
|
||||||
|
append("; margin:0; pointer-events:none; color:transparent;")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ref { it ->
|
||||||
|
diagOverlayEl = it
|
||||||
|
onDispose { if (diagOverlayEl === it) diagOverlayEl = null }
|
||||||
|
}
|
||||||
|
}) {}
|
||||||
|
|
||||||
// Textarea: user input with transparent text
|
// Textarea: user input with transparent text
|
||||||
org.jetbrains.compose.web.dom.TextArea(value = code, attrs = {
|
org.jetbrains.compose.web.dom.TextArea(value = code, attrs = {
|
||||||
ref { ta ->
|
ref { ta ->
|
||||||
@ -269,13 +468,16 @@ fun EditorWithOverlay(
|
|||||||
val v = (ev.target as HTMLTextAreaElement).value
|
val v = (ev.target as HTMLTextAreaElement).value
|
||||||
setCode(v)
|
setCode(v)
|
||||||
adjustTextareaHeight()
|
adjustTextareaHeight()
|
||||||
|
updateCaretTooltip()
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown { ev ->
|
onKeyDown { ev ->
|
||||||
// bubble to caller first so they may intercept shortcuts
|
// bubble to caller first so they may intercept shortcuts
|
||||||
onKeyDown?.invoke(ev)
|
onKeyDown?.invoke(ev)
|
||||||
|
if (ev.defaultPrevented) return@onKeyDown
|
||||||
val ta = taEl ?: return@onKeyDown
|
val ta = taEl ?: return@onKeyDown
|
||||||
val key = ev.key
|
val key = ev.key
|
||||||
|
val keyLower = key.lowercase()
|
||||||
// If user pressed Ctrl/Cmd + Enter, treat it as a shortcut (e.g., Run)
|
// If user pressed Ctrl/Cmd + Enter, treat it as a shortcut (e.g., Run)
|
||||||
// and DO NOT insert a newline here. Let the host handler act.
|
// and DO NOT insert a newline here. Let the host handler act.
|
||||||
// Also prevent default so the textarea won't add a line.
|
// Also prevent default so the textarea won't add a line.
|
||||||
@ -283,6 +485,48 @@ fun EditorWithOverlay(
|
|||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
return@onKeyDown
|
return@onKeyDown
|
||||||
}
|
}
|
||||||
|
if (ev.ctrlKey || ev.metaKey) {
|
||||||
|
val offset = ta.selectionStart ?: 0
|
||||||
|
val text = ta.value
|
||||||
|
when {
|
||||||
|
(key == " " || keyLower == "space" || keyLower == "spacebar") && onCompletionRequested != null -> {
|
||||||
|
ev.preventDefault()
|
||||||
|
scope.launch {
|
||||||
|
val analysis = ensureAnalysis(text)
|
||||||
|
val items = LyngWebTools.completions(text, offset, analysis)
|
||||||
|
onCompletionRequested(offset, items)
|
||||||
|
}
|
||||||
|
return@onKeyDown
|
||||||
|
}
|
||||||
|
keyLower == "b" && onDefinitionResolved != null -> {
|
||||||
|
ev.preventDefault()
|
||||||
|
scope.launch {
|
||||||
|
val analysis = ensureAnalysis(text)
|
||||||
|
val target = LyngWebTools.definitionAt(analysis, offset)
|
||||||
|
onDefinitionResolved(offset, target)
|
||||||
|
}
|
||||||
|
return@onKeyDown
|
||||||
|
}
|
||||||
|
ev.shiftKey && keyLower == "u" && onUsagesResolved != null -> {
|
||||||
|
ev.preventDefault()
|
||||||
|
scope.launch {
|
||||||
|
val analysis = ensureAnalysis(text)
|
||||||
|
val ranges = LyngWebTools.usagesAt(analysis, offset, includeDeclaration = false)
|
||||||
|
onUsagesResolved(offset, ranges)
|
||||||
|
}
|
||||||
|
return@onKeyDown
|
||||||
|
}
|
||||||
|
keyLower == "q" && onDocRequested != null -> {
|
||||||
|
ev.preventDefault()
|
||||||
|
scope.launch {
|
||||||
|
val analysis = ensureAnalysis(text)
|
||||||
|
val info = LyngWebTools.docAt(analysis, offset)
|
||||||
|
onDocRequested(offset, info)
|
||||||
|
}
|
||||||
|
return@onKeyDown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (key == "Tab" && ev.shiftKey) {
|
if (key == "Tab" && ev.shiftKey) {
|
||||||
// Shift+Tab: outdent current line(s)
|
// Shift+Tab: outdent current line(s)
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
@ -336,21 +580,57 @@ fun EditorWithOverlay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyUp { _ ->
|
||||||
|
updateCaretTooltip()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseUp { _ ->
|
||||||
|
updateCaretTooltip()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove { ev ->
|
||||||
|
updateHoverTooltip(ev.clientX.toDouble(), ev.clientY.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave { _ ->
|
||||||
|
tooltipText = null
|
||||||
|
}
|
||||||
|
|
||||||
onScroll { ev ->
|
onScroll { ev ->
|
||||||
val src = ev.target as? HTMLTextAreaElement ?: return@onScroll
|
val src = ev.target as? HTMLTextAreaElement ?: return@onScroll
|
||||||
overlayEl?.scrollTop = src.scrollTop
|
overlayEl?.scrollTop = src.scrollTop
|
||||||
overlayEl?.scrollLeft = src.scrollLeft
|
overlayEl?.scrollLeft = src.scrollLeft
|
||||||
|
diagOverlayEl?.scrollTop = src.scrollTop
|
||||||
|
diagOverlayEl?.scrollLeft = src.scrollLeft
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (tooltipText != null && tooltipX != null && tooltipY != null) {
|
||||||
|
org.jetbrains.compose.web.dom.Div({
|
||||||
|
attr(
|
||||||
|
"style",
|
||||||
|
buildString {
|
||||||
|
append("position:absolute; z-index:3; pointer-events:none;")
|
||||||
|
append("left:").append(tooltipX).append("px; top:").append(tooltipY).append("px;")
|
||||||
|
append("background:#212529; color:#f8f9fa; padding:4px 6px; border-radius:4px;")
|
||||||
|
append("font-size:12px; line-height:1.3; max-width:360px; white-space:pre-wrap;")
|
||||||
|
append("box-shadow:0 4px 10px rgba(0,0,0,.15);")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
org.jetbrains.compose.web.dom.Text(tooltipText!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// No built-in action buttons: EditorWithOverlay is a pure editor now
|
// No built-in action buttons: EditorWithOverlay is a pure editor now
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure overlay typography and paddings mirror the textarea so characters line up 1:1
|
// Ensure overlay typography and paddings mirror the textarea so characters line up 1:1
|
||||||
LaunchedEffect(taEl, overlayEl) {
|
LaunchedEffect(taEl, overlayEl, diagOverlayEl) {
|
||||||
try {
|
try {
|
||||||
val ta = taEl ?: return@LaunchedEffect
|
val ta = taEl ?: return@LaunchedEffect
|
||||||
val ov = overlayEl ?: return@LaunchedEffect
|
val ov = overlayEl ?: return@LaunchedEffect
|
||||||
|
val diag = diagOverlayEl
|
||||||
val cs = window.getComputedStyle(ta)
|
val cs = window.getComputedStyle(ta)
|
||||||
|
|
||||||
// Best-effort concrete line-height
|
// Best-effort concrete line-height
|
||||||
@ -376,6 +656,7 @@ fun EditorWithOverlay(
|
|||||||
append("color: var(--bs-body-color);")
|
append("color: var(--bs-body-color);")
|
||||||
}
|
}
|
||||||
ov.setAttribute("style", style)
|
ov.setAttribute("style", style)
|
||||||
|
diag?.setAttribute("style", style + "color:transparent;")
|
||||||
// also enforce concrete line-height on textarea to stabilize caret metrics
|
// also enforce concrete line-height on textarea to stabilize caret metrics
|
||||||
val existing = ta.getAttribute("style") ?: ""
|
val existing = ta.getAttribute("style") ?: ""
|
||||||
if (!existing.contains("line-height") && !lineHeight.isNullOrBlank()) {
|
if (!existing.contains("line-height") && !lineHeight.isNullOrBlank()) {
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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.lyngweb
|
||||||
|
|
||||||
|
import net.sergeych.lyng.format.LyngFormatConfig
|
||||||
|
import net.sergeych.lyng.highlight.TextRange
|
||||||
|
import net.sergeych.lyng.miniast.CompletionItem
|
||||||
|
import net.sergeych.lyng.tools.LyngAnalysisResult
|
||||||
|
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||||
|
import net.sergeych.lyng.tools.LyngSymbolInfo
|
||||||
|
import net.sergeych.lyng.tools.LyngSymbolTarget
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin JS-friendly facade for shared Lyng language tooling.
|
||||||
|
* Keeps web editor/site integrations consistent with IDE tooling behavior.
|
||||||
|
*/
|
||||||
|
object LyngWebTools {
|
||||||
|
suspend fun analyze(text: String, fileName: String = "<web>"): LyngAnalysisResult =
|
||||||
|
LyngLanguageTools.analyze(text, fileName)
|
||||||
|
|
||||||
|
suspend fun completions(text: String, offset: Int, analysis: LyngAnalysisResult? = null): List<CompletionItem> {
|
||||||
|
val a = analysis ?: analyze(text)
|
||||||
|
return LyngLanguageTools.completions(text, offset, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun definitionAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolTarget? =
|
||||||
|
LyngLanguageTools.definitionAt(analysis, offset)
|
||||||
|
|
||||||
|
fun usagesAt(analysis: LyngAnalysisResult, offset: Int, includeDeclaration: Boolean = false): List<TextRange> =
|
||||||
|
LyngLanguageTools.usagesAt(analysis, offset, includeDeclaration)
|
||||||
|
|
||||||
|
fun docAt(analysis: LyngAnalysisResult, offset: Int): LyngSymbolInfo? =
|
||||||
|
LyngLanguageTools.docAt(analysis, offset)
|
||||||
|
|
||||||
|
fun format(text: String, config: LyngFormatConfig = LyngFormatConfig()): String =
|
||||||
|
LyngLanguageTools.format(text, config)
|
||||||
|
|
||||||
|
suspend fun disassembleSymbol(text: String, symbol: String): String =
|
||||||
|
LyngLanguageTools.disassembleSymbol(text, symbol)
|
||||||
|
}
|
||||||
@ -120,6 +120,12 @@ fun ReferencePage() {
|
|||||||
Div { Text("$kind ${d.name}$t") }
|
Div { Text("$kind ${d.name}$t") }
|
||||||
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
|
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
|
||||||
}
|
}
|
||||||
|
is MiniTypeAliasDecl -> {
|
||||||
|
val params = if (d.typeParams.isEmpty()) "" else d.typeParams.joinToString(", ", "<", ">")
|
||||||
|
val target = DocLookupUtils.typeOf(d.target).ifEmpty { "Any" }
|
||||||
|
Div { Text("type ${d.name}$params = $target") }
|
||||||
|
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
|
||||||
|
}
|
||||||
is MiniClassDecl -> {
|
is MiniClassDecl -> {
|
||||||
Div { Text("class ${d.name}") }
|
Div { Text("class ${d.name}") }
|
||||||
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
|
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
|
||||||
@ -145,6 +151,12 @@ fun ReferencePage() {
|
|||||||
val staticStr = if (m.isStatic) "static " else ""
|
val staticStr = if (m.isStatic) "static " else ""
|
||||||
Li { Text("${staticStr}${kindM} ${d.name}.${m.name}${ts}") }
|
Li { Text("${staticStr}${kindM} ${d.name}.${m.name}${ts}") }
|
||||||
}
|
}
|
||||||
|
is MiniMemberTypeAliasDecl -> {
|
||||||
|
val params = if (m.typeParams.isEmpty()) "" else m.typeParams.joinToString(", ", "<", ">")
|
||||||
|
val target = DocLookupUtils.typeOf(m.target).ifEmpty { "Any" }
|
||||||
|
val staticStr = if (m.isStatic) "static " else ""
|
||||||
|
Li { Text("${staticStr}type ${d.name}.${m.name}$params = $target") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,15 @@ import kotlinx.coroutines.launch
|
|||||||
import net.sergeych.lyng.LyngVersion
|
import net.sergeych.lyng.LyngVersion
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.Script
|
||||||
import net.sergeych.lyng.ScriptError
|
import net.sergeych.lyng.ScriptError
|
||||||
|
import net.sergeych.lyng.highlight.TextRange
|
||||||
|
import net.sergeych.lyng.miniast.CompletionItem
|
||||||
|
import net.sergeych.lyng.tools.LyngDiagnostic
|
||||||
|
import net.sergeych.lyng.tools.LyngDiagnosticSeverity
|
||||||
|
import net.sergeych.lyng.tools.LyngSymbolInfo
|
||||||
|
import net.sergeych.lyng.tools.LyngSymbolTarget
|
||||||
import net.sergeych.lyngweb.EditorWithOverlay
|
import net.sergeych.lyngweb.EditorWithOverlay
|
||||||
|
import net.sergeych.lyngweb.LyngWebTools
|
||||||
|
import org.jetbrains.compose.web.attributes.InputType
|
||||||
import org.jetbrains.compose.web.dom.*
|
import org.jetbrains.compose.web.dom.*
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -52,6 +60,15 @@ fun TryLyngPage(route: String) {
|
|||||||
var output by remember { mutableStateOf<String?>(null) }
|
var output by remember { mutableStateOf<String?>(null) }
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
var extendedError by remember { mutableStateOf<String?>(null) }
|
var extendedError by remember { mutableStateOf<String?>(null) }
|
||||||
|
var diagnostics by remember { mutableStateOf<List<LyngDiagnostic>>(emptyList()) }
|
||||||
|
var completionItems by remember { mutableStateOf<List<CompletionItem>>(emptyList()) }
|
||||||
|
var completionOffset by remember { mutableStateOf<Int?>(null) }
|
||||||
|
var docInfo by remember { mutableStateOf<LyngSymbolInfo?>(null) }
|
||||||
|
var definitionTarget by remember { mutableStateOf<LyngSymbolTarget?>(null) }
|
||||||
|
var usageRanges by remember { mutableStateOf<List<TextRange>>(emptyList()) }
|
||||||
|
var disasmSymbol by remember { mutableStateOf<String>("") }
|
||||||
|
var disasmOutput by remember { mutableStateOf<String?>(null) }
|
||||||
|
var disasmError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
fun runCode() {
|
fun runCode() {
|
||||||
if (running) return
|
if (running) return
|
||||||
@ -59,6 +76,14 @@ fun TryLyngPage(route: String) {
|
|||||||
output = null
|
output = null
|
||||||
error = null
|
error = null
|
||||||
extendedError = null
|
extendedError = null
|
||||||
|
completionItems = emptyList()
|
||||||
|
completionOffset = null
|
||||||
|
docInfo = null
|
||||||
|
definitionTarget = null
|
||||||
|
usageRanges = emptyList()
|
||||||
|
diagnostics = emptyList()
|
||||||
|
disasmOutput = null
|
||||||
|
disasmError = null
|
||||||
scope.launch {
|
scope.launch {
|
||||||
// keep this outside try so we can show partial prints if evaluation fails
|
// keep this outside try so we can show partial prints if evaluation fails
|
||||||
val printed = StringBuilder()
|
val printed = StringBuilder()
|
||||||
@ -156,6 +181,22 @@ fun TryLyngPage(route: String) {
|
|||||||
runCode()
|
runCode()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onAnalysisReady = { analysis ->
|
||||||
|
diagnostics = analysis.diagnostics
|
||||||
|
},
|
||||||
|
onCompletionRequested = { offset, items ->
|
||||||
|
completionOffset = offset
|
||||||
|
completionItems = items
|
||||||
|
},
|
||||||
|
onDefinitionResolved = { _, target ->
|
||||||
|
definitionTarget = target
|
||||||
|
},
|
||||||
|
onUsagesResolved = { _, ranges ->
|
||||||
|
usageRanges = ranges
|
||||||
|
},
|
||||||
|
onDocRequested = { _, info ->
|
||||||
|
docInfo = info
|
||||||
|
},
|
||||||
// Keep current initial size but allow the editor to grow with content
|
// Keep current initial size but allow the editor to grow with content
|
||||||
autoGrow = true
|
autoGrow = true
|
||||||
)
|
)
|
||||||
@ -218,10 +259,150 @@ fun TryLyngPage(route: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Language tools quick view
|
||||||
|
Div({ classes("card", "mb-3") }) {
|
||||||
|
Div({ classes("card-header", "d-flex", "align-items-center", "gap-2") }) {
|
||||||
|
I({ classes("bi", "bi-diagram-3") })
|
||||||
|
Span({ classes("fw-semibold") }) { Text("Language tools") }
|
||||||
|
}
|
||||||
|
Div({ classes("card-body") }) {
|
||||||
|
Div({ classes("mb-3") }) {
|
||||||
|
Span({ classes("fw-semibold", "me-2") }) { Text("Diagnostics") }
|
||||||
|
if (diagnostics.isEmpty()) {
|
||||||
|
Span({ classes("text-muted") }) { Text("No errors or warnings.") }
|
||||||
|
} else {
|
||||||
|
Ul({ classes("mb-0") }) {
|
||||||
|
diagnostics.forEach { d ->
|
||||||
|
Li {
|
||||||
|
val sev = when (d.severity) {
|
||||||
|
LyngDiagnosticSeverity.Error -> "Error"
|
||||||
|
LyngDiagnosticSeverity.Warning -> "Warning"
|
||||||
|
}
|
||||||
|
val range = d.range?.let { " @${it.start}-${it.endExclusive}" } ?: ""
|
||||||
|
Text("$sev: ${d.message}$range")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Div({ classes("mb-3") }) {
|
||||||
|
Span({ classes("fw-semibold", "me-2") }) { Text("Quick docs") }
|
||||||
|
if (docInfo == null) {
|
||||||
|
Span({ classes("text-muted") }) { Text("Press Ctrl+Q (or ⌘+Q) on a symbol.") }
|
||||||
|
} else {
|
||||||
|
val info = docInfo!!
|
||||||
|
Div({ classes("small") }) {
|
||||||
|
Text("${info.target.kind} ${info.target.name}")
|
||||||
|
info.signature?.let { sig ->
|
||||||
|
Br()
|
||||||
|
Code { Text(sig) }
|
||||||
|
}
|
||||||
|
info.doc?.summary?.let { doc ->
|
||||||
|
Br()
|
||||||
|
Text(doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Div({ classes("mb-3") }) {
|
||||||
|
Span({ classes("fw-semibold", "me-2") }) { Text("Definition") }
|
||||||
|
if (definitionTarget == null) {
|
||||||
|
Span({ classes("text-muted") }) { Text("Press Ctrl+B (or ⌘+B) on a symbol.") }
|
||||||
|
} else {
|
||||||
|
val def = definitionTarget!!
|
||||||
|
Span({ classes("small") }) {
|
||||||
|
Text("${def.kind} ${def.name} @${def.range.start}-${def.range.endExclusive}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Div({ classes("mb-3") }) {
|
||||||
|
Span({ classes("fw-semibold", "me-2") }) { Text("Usages") }
|
||||||
|
if (usageRanges.isEmpty()) {
|
||||||
|
Span({ classes("text-muted") }) { Text("Press Ctrl+Shift+U (or ⌘+Shift+U) on a symbol.") }
|
||||||
|
} else {
|
||||||
|
Span({ classes("small") }) { Text("${usageRanges.size} usage(s) found.") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Div({ classes("mb-0") }) {
|
||||||
|
Span({ classes("fw-semibold", "me-2") }) { Text("Completions") }
|
||||||
|
if (completionItems.isEmpty()) {
|
||||||
|
Span({ classes("text-muted") }) { Text("Press Ctrl+Space (or ⌘+Space).") }
|
||||||
|
} else {
|
||||||
|
val shown = completionItems.take(8)
|
||||||
|
Span({ classes("text-muted", "small", "ms-1") }) {
|
||||||
|
completionOffset?.let { Text("@$it") }
|
||||||
|
}
|
||||||
|
Ul({ classes("mb-0") }) {
|
||||||
|
shown.forEach { item ->
|
||||||
|
Li { Text("${item.name} (${item.kind})") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (completionItems.size > shown.size) {
|
||||||
|
Span({ classes("text-muted", "small") }) {
|
||||||
|
Text("…and ${completionItems.size - shown.size} more")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disassembly
|
||||||
|
Div({ classes("card", "mb-3") }) {
|
||||||
|
Div({ classes("card-header", "d-flex", "align-items-center", "gap-2") }) {
|
||||||
|
I({ classes("bi", "bi-braces") })
|
||||||
|
Span({ classes("fw-semibold") }) { Text("Disassembly") }
|
||||||
|
}
|
||||||
|
Div({ classes("card-body") }) {
|
||||||
|
Div({ classes("d-flex", "gap-2", "align-items-center", "mb-2") }) {
|
||||||
|
Input(type = InputType.Text, attrs = {
|
||||||
|
classes("form-control")
|
||||||
|
attr("placeholder", "Symbol (e.g., MyClass.method or topLevelFun)")
|
||||||
|
value(disasmSymbol)
|
||||||
|
onInput { ev ->
|
||||||
|
disasmSymbol = ev.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Button(attrs = {
|
||||||
|
classes("btn", "btn-outline-primary")
|
||||||
|
if (disasmSymbol.isBlank()) attr("disabled", "disabled")
|
||||||
|
onClick {
|
||||||
|
it.preventDefault()
|
||||||
|
val symbol = disasmSymbol.trim()
|
||||||
|
if (symbol.isEmpty()) return@onClick
|
||||||
|
disasmOutput = null
|
||||||
|
disasmError = null
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
disasmOutput = LyngWebTools.disassembleSymbol(code, symbol)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
disasmError = t.message ?: t.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) { Text("Disassemble") }
|
||||||
|
}
|
||||||
|
if (disasmError != null) {
|
||||||
|
Div({ classes("alert", "alert-danger", "py-2", "mb-2") }) { Text(disasmError!!) }
|
||||||
|
}
|
||||||
|
if (disasmOutput != null) {
|
||||||
|
Pre({ classes("mb-0") }) { Code { Text(disasmOutput!!) } }
|
||||||
|
} else if (disasmError == null) {
|
||||||
|
Span({ classes("text-muted", "small") }) {
|
||||||
|
Text("Uses the bytecode compiler; not a dry run.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tips
|
// Tips
|
||||||
P({ classes("text-muted", "small") }) {
|
P({ classes("text-muted", "small") }) {
|
||||||
I({ classes("bi", "bi-info-circle", "me-1") })
|
I({ classes("bi", "bi-info-circle", "me-1") })
|
||||||
Text("Tip: press Ctrl+Enter (or ⌘+Enter on Mac) to run.")
|
Text("Tip: Ctrl+Enter runs, Ctrl+Space completes, Ctrl+B jumps to definition, Ctrl+Shift+U finds usages, Ctrl+Q shows docs.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user