language tools and site suport
This commit is contained in:
1 parent
78d8e546d5
commit
c16c0d7ebd
16 files changed
+1402
-338
No files matched your search
@@ -27,7 +27,7 @@ import net.sergeych.lyng.highlight.SimpleLyngHighlighter
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
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(
|
||||
val id: Int,
|
||||
@@ -126,13 +126,22 @@ object Binder {
|
||||
}
|
||||
// Members (including fields and methods)
|
||||
for (m in d.members) {
|
||||
if (m is MiniMemberValDecl) {
|
||||
val fs = source.offsetOf(m.nameStart)
|
||||
val fe = fs + m.name.length
|
||||
val kind = if (m.mutable) SymbolKind.Variable else SymbolKind.Value
|
||||
val fieldSym = Symbol(nextId++, m.name, kind, fs, fe, containerId = sym.id, type = DocLookupUtils.typeOf(m.type))
|
||||
symbols += fieldSym
|
||||
classScope.fields += fieldSym.id
|
||||
when (m) {
|
||||
is MiniMemberValDecl -> {
|
||||
val fs = source.offsetOf(m.nameStart)
|
||||
val fe = fs + m.name.length
|
||||
val kind = if (m.mutable) SymbolKind.Variable else SymbolKind.Value
|
||||
val fieldSym = Symbol(nextId++, m.name, kind, fs, fe, containerId = sym.id, type = DocLookupUtils.typeOf(m.type))
|
||||
symbols += fieldSym
|
||||
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
|
||||
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,
|
||||
)
|
||||
|
||||
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.
|
||||
@@ -118,9 +118,11 @@ object CompletionEngineLight {
|
||||
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
||||
val enums = decls.filterIsInstance<MiniEnumDecl>().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) }
|
||||
classes.forEach { offerDeclAdd(out, prefix, it) }
|
||||
enums.forEach { offerDeclAdd(out, prefix, it) }
|
||||
aliases.forEach { offerDeclAdd(out, prefix, it) }
|
||||
vals.forEach { offerDeclAdd(out, prefix, it) }
|
||||
|
||||
// Imported and builtin
|
||||
@@ -135,9 +137,11 @@ object CompletionEngineLight {
|
||||
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
||||
val enums = decls.filterIsInstance<MiniEnumDecl>().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++ } }
|
||||
classes.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++ } }
|
||||
if (out.size >= cap || externalAdded >= budget) break
|
||||
}
|
||||
@@ -196,6 +200,9 @@ object CompletionEngineLight {
|
||||
is MiniMemberValDecl -> {
|
||||
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 -> {}
|
||||
}
|
||||
}
|
||||
@@ -225,6 +232,7 @@ object CompletionEngineLight {
|
||||
}
|
||||
is MiniClassDecl -> add(CompletionItem(d.name, Kind.Class_))
|
||||
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)))
|
||||
// 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)
|
||||
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 -> {}
|
||||
}
|
||||
}
|
||||
@@ -317,6 +329,8 @@ object CompletionEngineLight {
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
if (ci.name.startsWith(prefix, true)) {
|
||||
|
||||
@@ -34,6 +34,7 @@ object DocLookupUtils {
|
||||
is MiniFunDecl -> "Function"
|
||||
is MiniClassDecl -> "Class"
|
||||
is MiniEnumDecl -> "Enum"
|
||||
is MiniTypeAliasDecl -> "TypeAlias"
|
||||
is MiniValDecl -> if (d.mutable) "Variable" else "Value"
|
||||
}
|
||||
return d.name to kind
|
||||
@@ -87,6 +88,7 @@ object DocLookupUtils {
|
||||
val kind = when (m) {
|
||||
is MiniMemberFunDecl -> "Function"
|
||||
is MiniMemberValDecl -> if (m.isStatic) "Value" else (if (m.mutable) "Variable" else "Value")
|
||||
is MiniMemberTypeAliasDecl -> "TypeAlias"
|
||||
is MiniInitDecl -> "Initializer"
|
||||
}
|
||||
return m.name to kind
|
||||
@@ -119,6 +121,7 @@ object DocLookupUtils {
|
||||
return when (d) {
|
||||
is MiniValDecl -> d.type ?: if (text != null && imported != null) inferTypeRefForVal(d, text, imported, mini) else null
|
||||
is MiniFunDecl -> d.returnType
|
||||
is MiniTypeAliasDecl -> d.target
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -142,6 +145,7 @@ object DocLookupUtils {
|
||||
is MiniMemberValDecl -> m.type ?: if (text != null && imported != null) {
|
||||
inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
|
||||
} else null
|
||||
is MiniMemberTypeAliasDecl -> m.target
|
||||
|
||||
else -> null
|
||||
}
|
||||
@@ -436,7 +440,10 @@ object DocLookupUtils {
|
||||
val type = findTypeByRange(mini, sym.name, sym.declStart, text, imported)
|
||||
simpleClassNameOf(type)?.let { return it }
|
||||
// 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) {
|
||||
is MiniClassDecl -> d.name
|
||||
is MiniEnumDecl -> d.name
|
||||
is MiniTypeAliasDecl -> d.name
|
||||
is MiniValDecl -> simpleClassNameOf(d.type ?: inferTypeRefForVal(d, text, imported, mini))
|
||||
is MiniFunDecl -> simpleClassNameOf(d.returnType)
|
||||
}
|
||||
@@ -565,6 +573,7 @@ object DocLookupUtils {
|
||||
val rt = when (mm) {
|
||||
is MiniMemberFunDecl -> mm.returnType
|
||||
is MiniMemberValDecl -> mm.type
|
||||
is MiniMemberTypeAliasDecl -> mm.target
|
||||
else -> null
|
||||
}
|
||||
return simpleClassNameOf(rt)
|
||||
@@ -580,8 +589,10 @@ object DocLookupUtils {
|
||||
val rt = when (m) {
|
||||
is MiniMemberFunDecl -> m.returnType
|
||||
is MiniMemberValDecl -> m.type
|
||||
is MiniMemberTypeAliasDecl -> m.target
|
||||
is MiniFunDecl -> m.returnType
|
||||
is MiniValDecl -> m.type
|
||||
is MiniTypeAliasDecl -> m.target
|
||||
else -> null
|
||||
}
|
||||
simpleClassNameOf(rt)
|
||||
@@ -921,6 +932,7 @@ object DocLookupUtils {
|
||||
return when (val m = resolved.second) {
|
||||
is MiniMemberFunDecl -> m.returnType
|
||||
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini)
|
||||
is MiniMemberTypeAliasDecl -> m.target
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -943,6 +955,7 @@ object DocLookupUtils {
|
||||
is MiniEnumDecl -> syntheticTypeRef(d.name)
|
||||
is MiniValDecl -> d.type ?: inferTypeRefForVal(d, fullText, imported, mini)
|
||||
is MiniFunDecl -> d.returnType
|
||||
is MiniTypeAliasDecl -> d.target
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -241,6 +241,17 @@ data class MiniEnumDecl(
|
||||
val entryPositions: List<Pos> = emptyList()
|
||||
) : 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(
|
||||
val name: String,
|
||||
val mutable: Boolean,
|
||||
@@ -290,6 +301,17 @@ data class MiniMemberValDecl(
|
||||
override val isExtern: Boolean = false,
|
||||
) : 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(
|
||||
override val range: MiniRange,
|
||||
override val nameStart: Pos,
|
||||
@@ -319,6 +341,7 @@ interface MiniAstSink {
|
||||
fun onInitDecl(node: MiniInitDecl) {}
|
||||
fun onClassDecl(node: MiniClassDecl) {}
|
||||
fun onEnumDecl(node: MiniEnumDecl) {}
|
||||
fun onTypeAliasDecl(node: MiniTypeAliasDecl) {}
|
||||
|
||||
fun onBlock(node: MiniBlock) {}
|
||||
fun onIdentifier(node: MiniIdentifier) {}
|
||||
@@ -489,6 +512,41 @@ class MiniAstBuilder : MiniAstSink {
|
||||
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) {
|
||||
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)
|
||||
""".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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user