plugin with type declarations, collection types and much better type tracking for autocomplete
This commit is contained in:
parent
aba0048a83
commit
fe5dded7af
@ -45,6 +45,8 @@ dependencies {
|
|||||||
// Tests for IntelliJ Platform fixtures rely on JUnit 3/4 API (junit.framework.TestCase)
|
// Tests for IntelliJ Platform fixtures rely on JUnit 3/4 API (junit.framework.TestCase)
|
||||||
// Add JUnit 4 which contains the JUnit 3 compatibility classes used by BasePlatformTestCase/UsefulTestCase
|
// Add JUnit 4 which contains the JUnit 3 compatibility classes used by BasePlatformTestCase/UsefulTestCase
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2")
|
||||||
|
testImplementation("org.opentest4j:opentest4j:1.3.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
intellij {
|
intellij {
|
||||||
|
|||||||
@ -102,7 +102,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
|
|
||||||
// Delegate computation to the shared engine to keep behavior in sync with tests
|
// Delegate computation to the shared engine to keep behavior in sync with tests
|
||||||
val engineItems = try {
|
val engineItems = try {
|
||||||
runBlocking { CompletionEngineLight.completeSuspend(text, caret, mini) }
|
runBlocking { CompletionEngineLight.completeSuspend(text, caret, mini, binding) }
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
if (DEBUG_COMPLETION) log.warn("[LYNG_DEBUG] Engine completion failed: ${t.message}")
|
if (DEBUG_COMPLETION) log.warn("[LYNG_DEBUG] Engine completion failed: ${t.message}")
|
||||||
emptyList()
|
emptyList()
|
||||||
@ -185,33 +185,51 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
?: DocLookupUtils.guessReturnClassAcrossKnownCallees(text, memberDotPos, imported, mini)
|
||||||
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
|
?: DocLookupUtils.guessReceiverClass(text, memberDotPos, imported, mini)
|
||||||
if (!inferredClass.isNullOrBlank()) {
|
if (!inferredClass.isNullOrBlank()) {
|
||||||
val ext = BuiltinDocRegistry.extensionMemberNamesFor(inferredClass)
|
val ext = DocLookupUtils.collectExtensionMemberNames(imported, inferredClass, mini)
|
||||||
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${'$'}{ext}")
|
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${ext}")
|
||||||
for (name in ext) {
|
for (name in ext) {
|
||||||
if (existing.contains(name)) continue
|
if (existing.contains(name)) continue
|
||||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name, mini)
|
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, inferredClass, name, mini)
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
when (val member = resolved.second) {
|
val m = resolved.second
|
||||||
|
val builder = when (m) {
|
||||||
is MiniMemberFunDecl -> {
|
is MiniMemberFunDecl -> {
|
||||||
val params = member.params.joinToString(", ") { it.name }
|
val params = m.params.joinToString(", ") { it.name }
|
||||||
val ret = typeOf(member.returnType)
|
val ret = typeOf(m.returnType)
|
||||||
val builder = LookupElementBuilder.create(name)
|
LookupElementBuilder.create(name)
|
||||||
.withIcon(AllIcons.Nodes.Method)
|
.withIcon(AllIcons.Nodes.Method)
|
||||||
.withTailText("(${ '$' }params)", true)
|
.withTailText("($params)", true)
|
||||||
|
.withTypeText(ret, true)
|
||||||
|
.withInsertHandler(ParenInsertHandler)
|
||||||
|
}
|
||||||
|
is MiniFunDecl -> {
|
||||||
|
val params = m.params.joinToString(", ") { it.name }
|
||||||
|
val ret = typeOf(m.returnType)
|
||||||
|
LookupElementBuilder.create(name)
|
||||||
|
.withIcon(AllIcons.Nodes.Method)
|
||||||
|
.withTailText("($params)", true)
|
||||||
.withTypeText(ret, true)
|
.withTypeText(ret, true)
|
||||||
.withInsertHandler(ParenInsertHandler)
|
.withInsertHandler(ParenInsertHandler)
|
||||||
emit(builder)
|
|
||||||
existing.add(name)
|
|
||||||
}
|
}
|
||||||
is MiniMemberValDecl -> {
|
is MiniMemberValDecl -> {
|
||||||
val builder = LookupElementBuilder.create(name)
|
LookupElementBuilder.create(name)
|
||||||
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||||
.withTypeText(typeOf(member.type), true)
|
.withTypeText(typeOf(m.type), true)
|
||||||
emit(builder)
|
}
|
||||||
existing.add(name)
|
is MiniValDecl -> {
|
||||||
|
LookupElementBuilder.create(name)
|
||||||
|
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||||
|
.withTypeText(typeOf(m.type), true)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
LookupElementBuilder.create(name)
|
||||||
|
.withIcon(AllIcons.Nodes.Method)
|
||||||
|
.withTailText("()", true)
|
||||||
|
.withInsertHandler(ParenInsertHandler)
|
||||||
}
|
}
|
||||||
is MiniInitDecl -> {}
|
|
||||||
}
|
}
|
||||||
|
emit(builder)
|
||||||
|
existing.add(name)
|
||||||
} else {
|
} else {
|
||||||
// Fallback: emit simple method name without detailed types
|
// Fallback: emit simple method name without detailed types
|
||||||
val builder = LookupElementBuilder.create(name)
|
val builder = LookupElementBuilder.create(name)
|
||||||
@ -455,27 +473,44 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, name, mini)
|
val resolved = DocLookupUtils.findMemberAcrossClasses(imported, name, mini)
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
val member = resolved.second
|
val member = resolved.second
|
||||||
when (member) {
|
val builder = when (member) {
|
||||||
is MiniMemberFunDecl -> {
|
is MiniMemberFunDecl -> {
|
||||||
val params = member.params.joinToString(", ") { it.name }
|
val params = member.params.joinToString(", ") { it.name }
|
||||||
val ret = typeOf(member.returnType)
|
val ret = typeOf(member.returnType)
|
||||||
val builder = LookupElementBuilder.create(name)
|
LookupElementBuilder.create(name)
|
||||||
.withIcon(AllIcons.Nodes.Method)
|
.withIcon(AllIcons.Nodes.Method)
|
||||||
.withTailText("(${params})", true)
|
.withTailText("($params)", true)
|
||||||
|
.withTypeText(ret, true)
|
||||||
|
.withInsertHandler(ParenInsertHandler)
|
||||||
|
}
|
||||||
|
is MiniFunDecl -> {
|
||||||
|
val params = member.params.joinToString(", ") { it.name }
|
||||||
|
val ret = typeOf(member.returnType)
|
||||||
|
LookupElementBuilder.create(name)
|
||||||
|
.withIcon(AllIcons.Nodes.Method)
|
||||||
|
.withTailText("($params)", true)
|
||||||
.withTypeText(ret, true)
|
.withTypeText(ret, true)
|
||||||
.withInsertHandler(ParenInsertHandler)
|
.withInsertHandler(ParenInsertHandler)
|
||||||
emit(builder)
|
|
||||||
already.add(name)
|
|
||||||
}
|
}
|
||||||
is MiniMemberValDecl -> {
|
is MiniMemberValDecl -> {
|
||||||
val builder = LookupElementBuilder.create(name)
|
LookupElementBuilder.create(name)
|
||||||
.withIcon(AllIcons.Nodes.Field)
|
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||||
.withTypeText(typeOf(member.type), true)
|
.withTypeText(typeOf(member.type), true)
|
||||||
emit(builder)
|
|
||||||
already.add(name)
|
|
||||||
}
|
}
|
||||||
is MiniInitDecl -> {}
|
is MiniValDecl -> {
|
||||||
|
LookupElementBuilder.create(name)
|
||||||
|
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||||
|
.withTypeText(typeOf(member.type), true)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
LookupElementBuilder.create(name)
|
||||||
|
.withIcon(AllIcons.Nodes.Method)
|
||||||
|
.withTailText("()", true)
|
||||||
|
.withInsertHandler(ParenInsertHandler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
emit(builder)
|
||||||
|
already.add(name)
|
||||||
} else {
|
} else {
|
||||||
// Synthetic fallback: method without detailed params/types to improve UX in absence of docs
|
// Synthetic fallback: method without detailed params/types to improve UX in absence of docs
|
||||||
val isProperty = name in setOf("size", "length")
|
val isProperty = name in setOf("size", "length")
|
||||||
@ -504,29 +539,46 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
// Try to resolve full signature via registry first to get params and return type
|
// Try to resolve full signature via registry first to get params and return type
|
||||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini)
|
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini)
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
when (val member = resolved.second) {
|
val m = resolved.second
|
||||||
|
val builder = when (m) {
|
||||||
is MiniMemberFunDecl -> {
|
is MiniMemberFunDecl -> {
|
||||||
val params = member.params.joinToString(", ") { it.name }
|
val params = m.params.joinToString(", ") { it.name }
|
||||||
val ret = typeOf(member.returnType)
|
val ret = typeOf(m.returnType)
|
||||||
val builder = LookupElementBuilder.create(name)
|
LookupElementBuilder.create(name)
|
||||||
.withIcon(AllIcons.Nodes.Method)
|
.withIcon(AllIcons.Nodes.Method)
|
||||||
.withTailText("(${params})", true)
|
.withTailText("($params)", true)
|
||||||
|
.withTypeText(ret, true)
|
||||||
|
.withInsertHandler(ParenInsertHandler)
|
||||||
|
}
|
||||||
|
is MiniFunDecl -> {
|
||||||
|
val params = m.params.joinToString(", ") { it.name }
|
||||||
|
val ret = typeOf(m.returnType)
|
||||||
|
LookupElementBuilder.create(name)
|
||||||
|
.withIcon(AllIcons.Nodes.Method)
|
||||||
|
.withTailText("($params)", true)
|
||||||
.withTypeText(ret, true)
|
.withTypeText(ret, true)
|
||||||
.withInsertHandler(ParenInsertHandler)
|
.withInsertHandler(ParenInsertHandler)
|
||||||
emit(builder)
|
|
||||||
already.add(name)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
is MiniMemberValDecl -> {
|
is MiniMemberValDecl -> {
|
||||||
val builder = LookupElementBuilder.create(name)
|
LookupElementBuilder.create(name)
|
||||||
.withIcon(if (member.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||||
.withTypeText(typeOf(member.type), true)
|
.withTypeText(typeOf(m.type), true)
|
||||||
emit(builder)
|
}
|
||||||
already.add(name)
|
is MiniValDecl -> {
|
||||||
continue
|
LookupElementBuilder.create(name)
|
||||||
|
.withIcon(if (m.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field)
|
||||||
|
.withTypeText(typeOf(m.type), true)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
LookupElementBuilder.create(name)
|
||||||
|
.withIcon(AllIcons.Nodes.Method)
|
||||||
|
.withTailText("()", true)
|
||||||
|
.withInsertHandler(ParenInsertHandler)
|
||||||
}
|
}
|
||||||
is MiniInitDecl -> {}
|
|
||||||
}
|
}
|
||||||
|
emit(builder)
|
||||||
|
already.add(name)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
// Fallback: emit without detailed types if we couldn't resolve
|
// Fallback: emit without detailed types if we couldn't resolve
|
||||||
val builder = LookupElementBuilder.create(name)
|
val builder = LookupElementBuilder.create(name)
|
||||||
|
|||||||
@ -68,6 +68,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
// 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 mini = LyngAstManager.getMiniAst(file) ?: return null
|
||||||
val miniSource = mini.range.start.source
|
val miniSource = mini.range.start.source
|
||||||
|
val imported = DocLookupUtils.canonicalImportedModules(mini, text)
|
||||||
|
|
||||||
// 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
|
||||||
@ -78,7 +79,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
if (d.name == name) {
|
if (d.name == name) {
|
||||||
val s: Int = miniSource.offsetOf(d.nameStart)
|
val s: Int = miniSource.offsetOf(d.nameStart)
|
||||||
if (s <= offset && s + d.name.length > offset) {
|
if (s <= offset && s + d.name.length > offset) {
|
||||||
return renderDeclDoc(d)
|
return renderDeclDoc(d, text, mini, imported)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle members if it was a member
|
// Handle members if it was a member
|
||||||
@ -105,6 +106,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
name = cf.name,
|
name = cf.name,
|
||||||
mutable = cf.mutable,
|
mutable = cf.mutable,
|
||||||
type = cf.type,
|
type = cf.type,
|
||||||
|
initRange = null,
|
||||||
doc = null,
|
doc = null,
|
||||||
nameStart = cf.nameStart
|
nameStart = cf.nameStart
|
||||||
)
|
)
|
||||||
@ -122,6 +124,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
name = cf.name,
|
name = cf.name,
|
||||||
mutable = cf.mutable,
|
mutable = cf.mutable,
|
||||||
type = cf.type,
|
type = cf.type,
|
||||||
|
initRange = null,
|
||||||
doc = null,
|
doc = null,
|
||||||
nameStart = cf.nameStart
|
nameStart = cf.nameStart
|
||||||
)
|
)
|
||||||
@ -171,7 +174,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dsFound != null) return renderDeclDoc(dsFound)
|
if (dsFound != null) return renderDeclDoc(dsFound, text, mini, imported)
|
||||||
|
|
||||||
// Check parameters
|
// Check parameters
|
||||||
mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
|
mini.declarations.filterIsInstance<MiniFunDecl>().forEach { fn ->
|
||||||
@ -209,6 +212,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
name = cf.name,
|
name = cf.name,
|
||||||
mutable = cf.mutable,
|
mutable = cf.mutable,
|
||||||
type = cf.type,
|
type = cf.type,
|
||||||
|
initRange = null,
|
||||||
doc = null,
|
doc = null,
|
||||||
nameStart = cf.nameStart
|
nameStart = cf.nameStart
|
||||||
)
|
)
|
||||||
@ -226,6 +230,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
name = cf.name,
|
name = cf.name,
|
||||||
mutable = cf.mutable,
|
mutable = cf.mutable,
|
||||||
type = cf.type,
|
type = cf.type,
|
||||||
|
initRange = null,
|
||||||
doc = null,
|
doc = null,
|
||||||
nameStart = cf.nameStart
|
nameStart = cf.nameStart
|
||||||
)
|
)
|
||||||
@ -308,6 +313,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
is MiniInitDecl -> null
|
is MiniInitDecl -> null
|
||||||
|
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}")
|
log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}")
|
||||||
@ -318,7 +327,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
// 4) As a fallback, if the caret is on an identifier text that matches any declaration name, show that
|
// 4) As a fallback, if the caret is on an identifier text that matches any declaration name, show that
|
||||||
mini.declarations.firstOrNull { it.name == ident }?.let {
|
mini.declarations.firstOrNull { it.name == ident }?.let {
|
||||||
log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}")
|
log.info("[LYNG_DEBUG] QuickDoc: fallback by name '${it.name}' kind=${it::class.simpleName}")
|
||||||
return renderDeclDoc(it)
|
return renderDeclDoc(it, text, mini, imported)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Consult BuiltinDocRegistry for imported modules (top-level and class members)
|
// 4) Consult BuiltinDocRegistry for imported modules (top-level and class members)
|
||||||
@ -338,13 +347,13 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
if (arity != null && chosen.params.size != arity && matches.size > 1) {
|
if (arity != null && chosen.params.size != arity && matches.size > 1) {
|
||||||
return renderOverloads(ident, matches)
|
return renderOverloads(ident, matches)
|
||||||
}
|
}
|
||||||
return renderDeclDoc(chosen)
|
return renderDeclDoc(chosen, text, mini, imported)
|
||||||
}
|
}
|
||||||
// Also allow values/consts
|
// Also allow values/consts
|
||||||
docs.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
docs.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it, text, mini, imported) }
|
||||||
// And classes/enums
|
// And classes/enums
|
||||||
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
|
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) }
|
docs.filterIsInstance<MiniEnumDecl>().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") {
|
||||||
@ -364,6 +373,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
is MiniInitDecl -> null
|
is MiniInitDecl -> null
|
||||||
|
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -383,6 +396,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
is MiniInitDecl -> null
|
is MiniInitDecl -> null
|
||||||
|
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -396,6 +413,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
is MiniInitDecl -> null
|
is MiniInitDecl -> null
|
||||||
|
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -421,6 +442,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
|
||||||
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
|
||||||
is MiniInitDecl -> null
|
is MiniInitDecl -> null
|
||||||
|
is MiniFunDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniValDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniClassDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
|
is MiniEnumDecl -> renderDeclDoc(member, text, mini, importedModules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -468,12 +493,16 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return contextElement ?: file.findElementAt(targetOffset)
|
return contextElement ?: file.findElementAt(targetOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderDeclDoc(d: MiniDecl): String {
|
private fun renderDeclDoc(d: MiniDecl, text: String, mini: MiniScript, imported: List<String>): String {
|
||||||
val title = when (d) {
|
val title = when (d) {
|
||||||
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 MiniValDecl -> if (d.mutable) "var ${d.name}${typeOf(d.type)}" else "val ${d.name}${typeOf(d.type)}"
|
is MiniValDecl -> {
|
||||||
|
val t = d.type ?: DocLookupUtils.inferTypeRefForVal(d, text, imported, mini)
|
||||||
|
val typeStr = if (t == null) ": Object?" else typeOf(t)
|
||||||
|
if (d.mutable) "var ${d.name}${typeStr}" else "val ${d.name}${typeStr}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Show full detailed documentation, not just the summary
|
// Show full detailed documentation, not just the summary
|
||||||
val raw = d.doc?.raw
|
val raw = d.doc?.raw
|
||||||
@ -506,7 +535,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun renderMemberValDoc(className: String, m: MiniMemberValDecl): String {
|
private fun renderMemberValDoc(className: String, m: MiniMemberValDecl): String {
|
||||||
val ts = typeOf(m.type)
|
val ts = if (m.type == null) ": Object?" else typeOf(m.type)
|
||||||
val kind = if (m.mutable) "var" else "val"
|
val kind = if (m.mutable) "var" else "val"
|
||||||
val staticStr = if (m.isStatic) "static " else ""
|
val staticStr = if (m.isStatic) "static " else ""
|
||||||
val title = "${staticStr}${kind} $className.${m.name}${ts}"
|
val title = "${staticStr}${kind} $className.${m.name}${ts}"
|
||||||
@ -527,7 +556,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
is MiniFunctionType -> ": (..) -> ..${if (t.nullable) "?" else ""}"
|
is MiniFunctionType -> ": (..) -> ..${if (t.nullable) "?" else ""}"
|
||||||
is MiniTypeVar -> ": ${t.name}${if (t.nullable) "?" else ""}"
|
is MiniTypeVar -> ": ${t.name}${if (t.nullable) "?" else ""}"
|
||||||
null -> ""
|
null -> ": Object?"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun signatureOf(fn: MiniFunDecl): String {
|
private fun signatureOf(fn: MiniFunDecl): String {
|
||||||
|
|||||||
@ -63,6 +63,10 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
is MiniMemberFunDecl -> "Function"
|
is MiniMemberFunDecl -> "Function"
|
||||||
is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value"
|
is MiniMemberValDecl -> if (member.mutable) "Variable" else "Value"
|
||||||
is MiniInitDecl -> "Initializer"
|
is MiniInitDecl -> "Initializer"
|
||||||
|
is MiniFunDecl -> "Function"
|
||||||
|
is MiniValDecl -> if (member.mutable) "Variable" else "Value"
|
||||||
|
is MiniClassDecl -> "Class"
|
||||||
|
is MiniEnumDecl -> "Enum"
|
||||||
}
|
}
|
||||||
results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind)))
|
results.add(PsiElementResolveResult(LyngDeclarationElement(it, member.name, kind)))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ actual object ArgBuilderProvider {
|
|||||||
private val tl = object : ThreadLocal<AndroidArgsBuilder>() {
|
private val tl = object : ThreadLocal<AndroidArgsBuilder>() {
|
||||||
override fun initialValue(): AndroidArgsBuilder = AndroidArgsBuilder()
|
override fun initialValue(): AndroidArgsBuilder = AndroidArgsBuilder()
|
||||||
}
|
}
|
||||||
actual fun acquire(): ArgsBuilder = tl.get()
|
actual fun acquire(): ArgsBuilder = tl.get()!!
|
||||||
}
|
}
|
||||||
|
|
||||||
private class AndroidArgsBuilder : ArgsBuilder {
|
private class AndroidArgsBuilder : ArgsBuilder {
|
||||||
|
|||||||
@ -973,7 +973,10 @@ class Compiler(
|
|||||||
private fun parseTypeDeclarationWithMini(): Pair<TypeDecl, MiniTypeRef?> {
|
private fun parseTypeDeclarationWithMini(): Pair<TypeDecl, MiniTypeRef?> {
|
||||||
// Only parse a type if a ':' follows; otherwise keep current behavior
|
// Only parse a type if a ':' follows; otherwise keep current behavior
|
||||||
if (!cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) return Pair(TypeDecl.TypeAny, null)
|
if (!cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) return Pair(TypeDecl.TypeAny, null)
|
||||||
|
return parseTypeExpressionWithMini()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseTypeExpressionWithMini(): Pair<TypeDecl, MiniTypeRef> {
|
||||||
// Parse a qualified base name: ID ('.' ID)*
|
// Parse a qualified base name: ID ('.' ID)*
|
||||||
val segments = mutableListOf<MiniTypeName.Segment>()
|
val segments = mutableListOf<MiniTypeName.Segment>()
|
||||||
var first = true
|
var first = true
|
||||||
@ -1009,41 +1012,28 @@ class Compiler(
|
|||||||
else MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable)
|
else MiniGenericType(MiniRange(typeStart, rangeEnd), base, args, nullable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional generic arguments: '<' Type (',' Type)* '>' — single-level only (no nested generics for now)
|
// Optional generic arguments: '<' Type (',' Type)* '>'
|
||||||
var args: MutableList<MiniTypeRef>? = null
|
var miniArgs: MutableList<MiniTypeRef>? = null
|
||||||
|
var semArgs: MutableList<TypeDecl>? = null
|
||||||
val afterBasePos = cc.savePos()
|
val afterBasePos = cc.savePos()
|
||||||
if (cc.skipTokenOfType(Token.Type.LT, isOptional = true)) {
|
if (cc.skipTokenOfType(Token.Type.LT, isOptional = true)) {
|
||||||
args = mutableListOf()
|
miniArgs = mutableListOf()
|
||||||
|
semArgs = mutableListOf()
|
||||||
do {
|
do {
|
||||||
// Parse argument as simple or qualified type (single level), with optional nullable '?'
|
val (argSem, argMini) = parseTypeExpressionWithMini()
|
||||||
val argSegs = mutableListOf<MiniTypeName.Segment>()
|
miniArgs += argMini
|
||||||
var argFirst = true
|
semArgs += argSem
|
||||||
val argStart = cc.currentPos()
|
|
||||||
while (true) {
|
|
||||||
val idTok = if (argFirst) cc.requireToken(
|
|
||||||
Token.Type.ID,
|
|
||||||
"type argument name expected"
|
|
||||||
) else cc.requireToken(Token.Type.ID, "identifier expected after '.' in type argument")
|
|
||||||
argFirst = false
|
|
||||||
argSegs += MiniTypeName.Segment(idTok.value, MiniRange(idTok.pos, idTok.pos))
|
|
||||||
val p = cc.savePos()
|
|
||||||
val tt = cc.next()
|
|
||||||
if (tt.type == Token.Type.DOT) continue else {
|
|
||||||
cc.restorePos(p); break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val argNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
|
|
||||||
val argEnd = cc.currentPos()
|
|
||||||
val argRef = MiniTypeName(MiniRange(argStart, argEnd), argSegs.toList(), nullable = argNullable)
|
|
||||||
args += argRef
|
|
||||||
|
|
||||||
val sep = cc.next()
|
val sep = cc.next()
|
||||||
when (sep.type) {
|
if (sep.type == Token.Type.COMMA) {
|
||||||
Token.Type.COMMA -> { /* continue */
|
// continue
|
||||||
}
|
} else if (sep.type == Token.Type.GT) {
|
||||||
|
break
|
||||||
Token.Type.GT -> break
|
} else if (sep.type == Token.Type.SHR) {
|
||||||
else -> sep.raiseSyntax("expected ',' or '>' in generic arguments")
|
cc.pushPendingGT()
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
sep.raiseSyntax("expected ',' or '>' in generic arguments")
|
||||||
}
|
}
|
||||||
} while (true)
|
} while (true)
|
||||||
lastEnd = cc.currentPos()
|
lastEnd = cc.currentPos()
|
||||||
@ -1055,10 +1045,11 @@ class Compiler(
|
|||||||
val isNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
|
val isNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
|
||||||
val endPos = cc.currentPos()
|
val endPos = cc.currentPos()
|
||||||
|
|
||||||
val miniRef = buildBaseRef(if (args != null) endPos else lastEnd, args, isNullable)
|
val miniRef = buildBaseRef(if (miniArgs != null) endPos else lastEnd, miniArgs, isNullable)
|
||||||
// Semantic: keep simple for now, just use qualified base name with nullable flag
|
// Semantic: keep simple for now, just use qualified base name with nullable flag
|
||||||
val qualified = segments.joinToString(".") { it.name }
|
val qualified = segments.joinToString(".") { it.name }
|
||||||
val sem = TypeDecl.Simple(qualified, isNullable)
|
val sem = if (semArgs != null) TypeDecl.Generic(qualified, semArgs, isNullable)
|
||||||
|
else TypeDecl.Simple(qualified, isNullable)
|
||||||
return Pair(sem, miniRef)
|
return Pair(sem, miniRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1474,7 +1465,9 @@ class Compiler(
|
|||||||
|
|
||||||
"init" -> {
|
"init" -> {
|
||||||
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
if (codeContexts.lastOrNull() is CodeContext.ClassBody && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
|
miniSink?.onEnterFunction(null)
|
||||||
val block = parseBlock()
|
val block = parseBlock()
|
||||||
|
miniSink?.onExitFunction(cc.currentPos())
|
||||||
lastParsedBlockRange?.let { range ->
|
lastParsedBlockRange?.let { range ->
|
||||||
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
|
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
|
||||||
}
|
}
|
||||||
@ -2714,8 +2707,7 @@ class Compiler(
|
|||||||
val declDocLocal = pendingDeclDoc
|
val declDocLocal = pendingDeclDoc
|
||||||
val outerLabel = lastLabel
|
val outerLabel = lastLabel
|
||||||
|
|
||||||
// Emit MiniFunDecl before body parsing (body range unknown yet)
|
val node = run {
|
||||||
run {
|
|
||||||
val params = argsDeclaration.params.map { p ->
|
val params = argsDeclaration.params.map { p ->
|
||||||
MiniParam(
|
MiniParam(
|
||||||
name = p.name,
|
name = p.name,
|
||||||
@ -2737,8 +2729,10 @@ class Compiler(
|
|||||||
)
|
)
|
||||||
miniSink?.onFunDecl(node)
|
miniSink?.onFunDecl(node)
|
||||||
pendingDeclDoc = null
|
pendingDeclDoc = null
|
||||||
|
node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
miniSink?.onEnterFunction(node)
|
||||||
return inCodeContext(CodeContext.Function(name)) {
|
return inCodeContext(CodeContext.Function(name)) {
|
||||||
cc.labels.add(name)
|
cc.labels.add(name)
|
||||||
outerLabel?.let { cc.labels.add(it) }
|
outerLabel?.let { cc.labels.add(it) }
|
||||||
@ -2941,6 +2935,7 @@ class Compiler(
|
|||||||
isExtern = actualExtern
|
isExtern = actualExtern
|
||||||
)
|
)
|
||||||
miniSink?.onFunDecl(node)
|
miniSink?.onFunDecl(node)
|
||||||
|
miniSink?.onExitFunction(cc.currentPos())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3186,9 +3181,11 @@ class Compiler(
|
|||||||
while (true) {
|
while (true) {
|
||||||
val t = cc.skipWsTokens()
|
val t = cc.skipWsTokens()
|
||||||
if (t.isId("get")) {
|
if (t.isId("get")) {
|
||||||
|
val getStart = cc.currentPos()
|
||||||
cc.next() // consume 'get'
|
cc.next() // consume 'get'
|
||||||
cc.requireToken(Token.Type.LPAREN)
|
cc.requireToken(Token.Type.LPAREN)
|
||||||
cc.requireToken(Token.Type.RPAREN)
|
cc.requireToken(Token.Type.RPAREN)
|
||||||
|
miniSink?.onEnterFunction(null)
|
||||||
getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
parseBlock()
|
parseBlock()
|
||||||
@ -3200,11 +3197,14 @@ class Compiler(
|
|||||||
} else {
|
} else {
|
||||||
throw ScriptError(cc.current().pos, "Expected { or = after get()")
|
throw ScriptError(cc.current().pos, "Expected { or = after get()")
|
||||||
}
|
}
|
||||||
|
miniSink?.onExitFunction(cc.currentPos())
|
||||||
} else if (t.isId("set")) {
|
} else if (t.isId("set")) {
|
||||||
|
val setStart = cc.currentPos()
|
||||||
cc.next() // consume 'set'
|
cc.next() // consume 'set'
|
||||||
cc.requireToken(Token.Type.LPAREN)
|
cc.requireToken(Token.Type.LPAREN)
|
||||||
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
||||||
cc.requireToken(Token.Type.RPAREN)
|
cc.requireToken(Token.Type.RPAREN)
|
||||||
|
miniSink?.onEnterFunction(null)
|
||||||
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
val body = parseBlock()
|
val body = parseBlock()
|
||||||
@ -3226,6 +3226,7 @@ class Compiler(
|
|||||||
} else {
|
} else {
|
||||||
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||||
}
|
}
|
||||||
|
miniSink?.onExitFunction(cc.currentPos())
|
||||||
} else if (t.isId("private") || t.isId("protected")) {
|
} else if (t.isId("private") || t.isId("protected")) {
|
||||||
val vis = if (t.isId("private")) Visibility.Private else Visibility.Protected
|
val vis = if (t.isId("private")) Visibility.Private else Visibility.Protected
|
||||||
val mark = cc.savePos()
|
val mark = cc.savePos()
|
||||||
@ -3237,6 +3238,7 @@ class Compiler(
|
|||||||
cc.next() // consume '('
|
cc.next() // consume '('
|
||||||
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name")
|
||||||
cc.requireToken(Token.Type.RPAREN)
|
cc.requireToken(Token.Type.RPAREN)
|
||||||
|
miniSink?.onEnterFunction(null)
|
||||||
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
cc.skipWsTokens()
|
cc.skipWsTokens()
|
||||||
val body = parseBlock()
|
val body = parseBlock()
|
||||||
@ -3261,6 +3263,7 @@ class Compiler(
|
|||||||
} else {
|
} else {
|
||||||
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||||
}
|
}
|
||||||
|
miniSink?.onExitFunction(cc.currentPos())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cc.restorePos(mark)
|
cc.restorePos(mark)
|
||||||
|
|||||||
@ -34,18 +34,34 @@ class CompilerContext(val tokens: List<Token>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentIndex = 0
|
var currentIndex = 0
|
||||||
|
private var pendingGT = 0
|
||||||
|
|
||||||
fun hasNext() = currentIndex < tokens.size
|
fun hasNext() = currentIndex < tokens.size || pendingGT > 0
|
||||||
fun hasPrevious() = currentIndex > 0
|
fun hasPrevious() = currentIndex > 0
|
||||||
fun next() =
|
fun next(): Token {
|
||||||
if (currentIndex < tokens.size) tokens[currentIndex++]
|
if (pendingGT > 0) {
|
||||||
|
pendingGT--
|
||||||
|
val last = tokens[currentIndex - 1]
|
||||||
|
return Token(">", last.pos.copy(column = last.pos.column + 1), Token.Type.GT)
|
||||||
|
}
|
||||||
|
return if (currentIndex < tokens.size) tokens[currentIndex++]
|
||||||
else Token("", tokens.last().pos, Token.Type.EOF)
|
else Token("", tokens.last().pos, Token.Type.EOF)
|
||||||
|
}
|
||||||
|
|
||||||
fun previous() = if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
|
fun pushPendingGT() {
|
||||||
|
pendingGT++
|
||||||
|
}
|
||||||
|
|
||||||
fun savePos() = currentIndex
|
fun previous() = if (pendingGT > 0) {
|
||||||
|
pendingGT-- // This is wrong, previous should go back.
|
||||||
|
// But we don't really use previous() in generics parser after splitting.
|
||||||
|
throw IllegalStateException("previous() not supported after pushPendingGT")
|
||||||
|
} else if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
|
||||||
|
|
||||||
|
fun savePos() = (currentIndex shl 2) or (pendingGT and 3)
|
||||||
fun restorePos(pos: Int) {
|
fun restorePos(pos: Int) {
|
||||||
currentIndex = pos
|
currentIndex = pos shr 2
|
||||||
|
pendingGT = pos and 3
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ensureLabelIsValid(pos: Pos, label: String) {
|
fun ensureLabelIsValid(pos: Pos, label: String) {
|
||||||
@ -106,12 +122,13 @@ class CompilerContext(val tokens: List<Token>) {
|
|||||||
errorMessage: String = "expected ${tokenType.name}",
|
errorMessage: String = "expected ${tokenType.name}",
|
||||||
isOptional: Boolean = false
|
isOptional: Boolean = false
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
val pos = savePos()
|
||||||
val t = next()
|
val t = next()
|
||||||
return if (t.type != tokenType) {
|
return if (t.type != tokenType) {
|
||||||
if (!isOptional) {
|
if (!isOptional) {
|
||||||
throw ScriptError(t.pos, errorMessage)
|
throw ScriptError(t.pos, errorMessage)
|
||||||
} else {
|
} else {
|
||||||
previous()
|
restorePos(pos)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
} else true
|
} else true
|
||||||
@ -122,20 +139,25 @@ class CompilerContext(val tokens: List<Token>) {
|
|||||||
* @return true if token was found and skipped
|
* @return true if token was found and skipped
|
||||||
*/
|
*/
|
||||||
fun skipNextIf(vararg types: Token.Type): Boolean {
|
fun skipNextIf(vararg types: Token.Type): Boolean {
|
||||||
|
val pos = savePos()
|
||||||
val t = next()
|
val t = next()
|
||||||
return if (t.type in types)
|
return if (t.type in types)
|
||||||
true
|
true
|
||||||
else {
|
else {
|
||||||
previous()
|
restorePos(pos)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun skipTokens(vararg tokenTypes: Token.Type) {
|
fun skipTokens(vararg tokenTypes: Token.Type) {
|
||||||
while (next().type in tokenTypes) { /**/
|
while (hasNext()) {
|
||||||
|
val pos = savePos()
|
||||||
|
if (next().type !in tokenTypes) {
|
||||||
|
restorePos(pos)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
previous()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nextNonWhitespace(): Token {
|
fun nextNonWhitespace(): Token {
|
||||||
@ -163,12 +185,13 @@ class CompilerContext(val tokens: List<Token>) {
|
|||||||
|
|
||||||
|
|
||||||
inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
|
inline fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): Boolean {
|
||||||
|
val pos = savePos()
|
||||||
val t = next()
|
val t = next()
|
||||||
return if (t.type == typeId) {
|
return if (t.type == typeId) {
|
||||||
f(t)
|
f(t)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
previous()
|
restorePos(pos)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,5 +27,6 @@ sealed class TypeDecl(val isNullable:Boolean = false) {
|
|||||||
object TypeAny : TypeDecl()
|
object TypeAny : TypeDecl()
|
||||||
object TypeNullableAny : TypeDecl(true)
|
object TypeNullableAny : TypeDecl(true)
|
||||||
|
|
||||||
class Simple(val name: String,isNullable: Boolean) : TypeDecl(isNullable)
|
class Simple(val name: String, isNullable: Boolean) : TypeDecl(isNullable)
|
||||||
|
class Generic(val name: String, val args: List<TypeDecl>, isNullable: Boolean) : TypeDecl(isNullable)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -236,6 +236,7 @@ class ClassDocsBuilder internal constructor(private val className: String) {
|
|||||||
name = name,
|
name = name,
|
||||||
mutable = mutable,
|
mutable = mutable,
|
||||||
type = type?.toMiniTypeRef(),
|
type = type?.toMiniTypeRef(),
|
||||||
|
initRange = null,
|
||||||
doc = md,
|
doc = md,
|
||||||
nameStart = Pos.builtIn,
|
nameStart = Pos.builtIn,
|
||||||
isStatic = isStatic,
|
isStatic = isStatic,
|
||||||
@ -534,6 +535,9 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Concurrency helpers
|
// Concurrency helpers
|
||||||
|
mod.classDoc(name = "Deferred", doc = "Represents a value that will be available in the future.", bases = listOf(type("Obj"))) {
|
||||||
|
method(name = "await", doc = "Suspend until the value is available and return it.")
|
||||||
|
}
|
||||||
mod.funDoc(
|
mod.funDoc(
|
||||||
name = "launch",
|
name = "launch",
|
||||||
doc = StdlibInlineDocIndex.topFunDoc("launch") ?: "Launch an asynchronous task and return a `Deferred`.",
|
doc = StdlibInlineDocIndex.topFunDoc("launch") ?: "Launch an asynchronous task and return a `Deferred`.",
|
||||||
@ -551,8 +555,17 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
|||||||
returns = type("lyng.Iterable")
|
returns = type("lyng.Iterable")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Common types
|
||||||
|
mod.classDoc(name = "Int", doc = "64-bit signed integer.", bases = listOf(type("Obj")))
|
||||||
|
mod.classDoc(name = "Real", doc = "64-bit floating point number.", bases = listOf(type("Obj")))
|
||||||
|
mod.classDoc(name = "Bool", doc = "Boolean value (true or false).", bases = listOf(type("Obj")))
|
||||||
|
mod.classDoc(name = "Char", doc = "Single character (UTF-16 code unit).", bases = listOf(type("Obj")))
|
||||||
|
mod.classDoc(name = "Buffer", doc = "Mutable byte array.", bases = listOf(type("Obj")))
|
||||||
|
mod.classDoc(name = "Regex", doc = "Regular expression.", bases = listOf(type("Obj")))
|
||||||
|
mod.classDoc(name = "Range", doc = "Arithmetic progression.", bases = listOf(type("Obj")))
|
||||||
|
|
||||||
// Common Iterable helpers (document top-level extension-like APIs as class members)
|
// Common Iterable helpers (document top-level extension-like APIs as class members)
|
||||||
mod.classDoc(name = "Iterable", doc = StdlibInlineDocIndex.classDoc("Iterable") ?: "Helper operations for iterable collections.") {
|
mod.classDoc(name = "Iterable", doc = StdlibInlineDocIndex.classDoc("Iterable") ?: "Helper operations for iterable collections.", bases = listOf(type("Obj"))) {
|
||||||
fun md(name: String, fallback: String) = StdlibInlineDocIndex.methodDoc("Iterable", name) ?: fallback
|
fun md(name: String, fallback: String) = StdlibInlineDocIndex.methodDoc("Iterable", name) ?: fallback
|
||||||
method(name = "filter", doc = md("filter", "Filter elements by predicate."), params = listOf(ParamDoc("predicate")), returns = type("lyng.Iterable"))
|
method(name = "filter", doc = md("filter", "Filter elements by predicate."), params = listOf(ParamDoc("predicate")), returns = type("lyng.Iterable"))
|
||||||
method(name = "drop", doc = md("drop", "Skip the first N elements."), params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.Iterable"))
|
method(name = "drop", doc = md("drop", "Skip the first N elements."), params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.Iterable"))
|
||||||
@ -591,7 +604,7 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Iterator helpers
|
// Iterator helpers
|
||||||
mod.classDoc(name = "Iterator", doc = StdlibInlineDocIndex.classDoc("Iterator") ?: "Iterator protocol for sequential access.") {
|
mod.classDoc(name = "Iterator", doc = StdlibInlineDocIndex.classDoc("Iterator") ?: "Iterator protocol for sequential access.", bases = listOf(type("Obj"))) {
|
||||||
fun md(name: String, fallback: String) = StdlibInlineDocIndex.methodDoc("Iterator", name) ?: fallback
|
fun md(name: String, fallback: String) = StdlibInlineDocIndex.methodDoc("Iterator", name) ?: fallback
|
||||||
method(name = "hasNext", doc = md("hasNext", "Whether another element is available."), returns = type("lyng.Bool"))
|
method(name = "hasNext", doc = md("hasNext", "Whether another element is available."), returns = type("lyng.Bool"))
|
||||||
method(name = "next", doc = md("next", "Return the next element."))
|
method(name = "next", doc = md("next", "Return the next element."))
|
||||||
@ -600,22 +613,22 @@ private fun buildStdlibDocs(): List<MiniDecl> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exceptions and utilities
|
// Exceptions and utilities
|
||||||
mod.classDoc(name = "Exception", doc = StdlibInlineDocIndex.classDoc("Exception") ?: "Exception helpers.") {
|
mod.classDoc(name = "Exception", doc = StdlibInlineDocIndex.classDoc("Exception") ?: "Exception helpers.", bases = listOf(type("Obj"))) {
|
||||||
method(name = "printStackTrace", doc = StdlibInlineDocIndex.methodDoc("Exception", "printStackTrace") ?: "Print this exception and its stack trace to standard output.")
|
method(name = "printStackTrace", doc = StdlibInlineDocIndex.methodDoc("Exception", "printStackTrace") ?: "Print this exception and its stack trace to standard output.")
|
||||||
}
|
}
|
||||||
|
|
||||||
mod.classDoc(name = "Enum", doc = StdlibInlineDocIndex.classDoc("Enum") ?: "Base class for all enums.") {
|
mod.classDoc(name = "Enum", doc = StdlibInlineDocIndex.classDoc("Enum") ?: "Base class for all enums.", bases = listOf(type("Obj"))) {
|
||||||
method(name = "name", doc = "Returns the name of this enum constant.", returns = type("lyng.String"))
|
method(name = "name", doc = "Returns the name of this enum constant.", returns = type("lyng.String"))
|
||||||
method(name = "ordinal", doc = "Returns the ordinal of this enum constant.", returns = type("lyng.Int"))
|
method(name = "ordinal", doc = "Returns the ordinal of this enum constant.", returns = type("lyng.Int"))
|
||||||
}
|
}
|
||||||
|
|
||||||
mod.classDoc(name = "String", doc = StdlibInlineDocIndex.classDoc("String") ?: "String helpers.") {
|
mod.classDoc(name = "String", doc = StdlibInlineDocIndex.classDoc("String") ?: "String helpers.", bases = listOf(type("Obj"))) {
|
||||||
// Only include inline-source method here; Kotlin-embedded methods are now documented via DocHelpers near definitions.
|
// Only include inline-source method here; Kotlin-embedded methods are now documented via DocHelpers near definitions.
|
||||||
method(name = "re", doc = StdlibInlineDocIndex.methodDoc("String", "re") ?: "Compile this string into a regular expression.", returns = type("lyng.Regex"))
|
method(name = "re", doc = StdlibInlineDocIndex.methodDoc("String", "re") ?: "Compile this string into a regular expression.", returns = type("lyng.Regex"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackTraceEntry structure
|
// StackTraceEntry structure
|
||||||
mod.classDoc(name = "StackTraceEntry", doc = StdlibInlineDocIndex.classDoc("StackTraceEntry") ?: "Represents a single stack trace element.") {
|
mod.classDoc(name = "StackTraceEntry", doc = StdlibInlineDocIndex.classDoc("StackTraceEntry") ?: "Represents a single stack trace element.", bases = listOf(type("Obj"))) {
|
||||||
// Fields are not present as declarations in root.lyng's class header docs. Keep seeded defaults.
|
// Fields are not present as declarations in root.lyng's class header docs. Keep seeded defaults.
|
||||||
field(name = "sourceName", doc = "Source (file) name.", type = type("lyng.String"))
|
field(name = "sourceName", doc = "Source (file) name.", type = type("lyng.String"))
|
||||||
field(name = "line", doc = "Line number (1-based).", type = type("lyng.Int"))
|
field(name = "line", doc = "Line number (1-based).", type = type("lyng.Int"))
|
||||||
|
|||||||
@ -23,6 +23,7 @@ package net.sergeych.lyng.miniast
|
|||||||
import net.sergeych.lyng.Compiler
|
import net.sergeych.lyng.Compiler
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.Script
|
||||||
import net.sergeych.lyng.Source
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.binding.BindingSnapshot
|
||||||
import net.sergeych.lyng.highlight.offsetOf
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
import net.sergeych.lyng.pacman.ImportProvider
|
import net.sergeych.lyng.pacman.ImportProvider
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ object CompletionEngineLight {
|
|||||||
return completeSuspend(text, idx)
|
return completeSuspend(text, idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun completeSuspend(text: String, caret: Int, providedMini: MiniScript? = null): List<CompletionItem> {
|
suspend fun completeSuspend(text: String, caret: Int, providedMini: MiniScript? = null, binding: BindingSnapshot? = null): List<CompletionItem> {
|
||||||
// Ensure stdlib Obj*-defined docs (e.g., String methods) are initialized before registry lookup
|
// Ensure stdlib Obj*-defined docs (e.g., String methods) are initialized before registry lookup
|
||||||
StdlibDocsBootstrap.ensure()
|
StdlibDocsBootstrap.ensure()
|
||||||
val prefix = prefixAt(text, caret)
|
val prefix = prefixAt(text, caret)
|
||||||
@ -73,6 +74,10 @@ object CompletionEngineLight {
|
|||||||
val memberDot = DocLookupUtils.findDotLeft(text, word?.first ?: caret)
|
val memberDot = DocLookupUtils.findDotLeft(text, word?.first ?: caret)
|
||||||
if (memberDot != null) {
|
if (memberDot != null) {
|
||||||
// 0) Try chained member call return type inference
|
// 0) Try chained member call return type inference
|
||||||
|
DocLookupUtils.guessReturnClassFromMemberCallBeforeMini(mini, text, memberDot, imported, binding)?.let { cls ->
|
||||||
|
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||||
|
return out
|
||||||
|
}
|
||||||
DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDot, imported, mini)?.let { cls ->
|
DocLookupUtils.guessReturnClassFromMemberCallBefore(text, memberDot, imported, mini)?.let { cls ->
|
||||||
offerMembersAdd(out, prefix, imported, cls, mini)
|
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||||
return out
|
return out
|
||||||
@ -88,7 +93,7 @@ object CompletionEngineLight {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
// 1) Receiver inference fallback
|
// 1) Receiver inference fallback
|
||||||
(DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDot, imported) ?: DocLookupUtils.guessReceiverClass(text, memberDot, imported, mini))?.let { cls ->
|
(DocLookupUtils.guessReceiverClassViaMini(mini, text, memberDot, imported, binding) ?: DocLookupUtils.guessReceiverClass(text, memberDot, imported, mini))?.let { cls ->
|
||||||
offerMembersAdd(out, prefix, imported, cls, mini)
|
offerMembersAdd(out, prefix, imported, cls, mini)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
@ -97,11 +102,16 @@ object CompletionEngineLight {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Global identifiers: params > local decls > imported > stdlib; Functions > Classes > Values; alphabetical
|
// Global identifiers: params > local decls > imported > stdlib; Functions > Classes > Values; alphabetical
|
||||||
if (mini != null) {
|
offerParamsInScope(out, prefix, mini, text, caret)
|
||||||
offerParamsInScope(out, prefix, mini, text, caret)
|
|
||||||
|
val locals = DocLookupUtils.extractLocalsAt(text, caret)
|
||||||
|
for (name in locals) {
|
||||||
|
if (name.startsWith(prefix, true)) {
|
||||||
|
out.add(CompletionItem(name, Kind.Value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val decls = mini?.declarations ?: emptyList()
|
val decls = mini.declarations
|
||||||
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
|
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
|
||||||
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() }
|
||||||
@ -274,33 +284,38 @@ object CompletionEngineLight {
|
|||||||
emitGroup(directMap)
|
emitGroup(directMap)
|
||||||
emitGroup(inheritedMap)
|
emitGroup(inheritedMap)
|
||||||
|
|
||||||
// Supplement with stdlib extension members defined in root.lyng (e.g., fun String.re(...))
|
// Supplement with extension members (both stdlib and local)
|
||||||
run {
|
run {
|
||||||
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
||||||
val ext = BuiltinDocRegistry.extensionMemberNamesFor(className)
|
val extensions = DocLookupUtils.collectExtensionMemberNames(imported, className, mini)
|
||||||
for (name in ext) {
|
for (name in extensions) {
|
||||||
if (already.contains(name)) continue
|
if (already.contains(name)) continue
|
||||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name)
|
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name, mini)
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
when (val member = resolved.second) {
|
val m = resolved.second
|
||||||
|
val ci = when (m) {
|
||||||
is MiniMemberFunDecl -> {
|
is MiniMemberFunDecl -> {
|
||||||
val params = member.params.joinToString(", ") { it.name }
|
val params = m.params.joinToString(", ") { it.name }
|
||||||
val ci = CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(member.returnType))
|
CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType))
|
||||||
if (ci.name.startsWith(prefix, true)) out += ci
|
|
||||||
already.add(name)
|
|
||||||
}
|
}
|
||||||
is MiniMemberValDecl -> {
|
is MiniFunDecl -> {
|
||||||
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(member.type))
|
val params = m.params.joinToString(", ") { it.name }
|
||||||
if (ci.name.startsWith(prefix, true)) out += ci
|
CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType))
|
||||||
already.add(name)
|
|
||||||
}
|
}
|
||||||
is MiniInitDecl -> {}
|
is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type))
|
||||||
|
is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type))
|
||||||
|
else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null)
|
||||||
|
}
|
||||||
|
if (ci.name.startsWith(prefix, true)) {
|
||||||
|
out += ci
|
||||||
|
already.add(name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: emit simple method name without detailed types
|
|
||||||
val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null)
|
val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null)
|
||||||
if (ci.name.startsWith(prefix, true)) out += ci
|
if (ci.name.startsWith(prefix, true)) {
|
||||||
already.add(name)
|
out += ci
|
||||||
|
already.add(name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,14 +91,15 @@ object DocLookupUtils {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findTypeByRange(mini: MiniScript?, name: String, startOffset: Int): MiniTypeRef? {
|
fun findTypeByRange(mini: MiniScript?, name: String, startOffset: Int, text: String? = null, imported: List<String>? = null): MiniTypeRef? {
|
||||||
if (mini == null) return null
|
if (mini == null) return null
|
||||||
val src = mini.range.start.source
|
val src = mini.range.start.source
|
||||||
|
fun matches(p: net.sergeych.lyng.Pos, len: Int) = src.offsetOf(p).let { s -> startOffset >= s && startOffset < s + len }
|
||||||
|
|
||||||
for (d in mini.declarations) {
|
for (d in mini.declarations) {
|
||||||
if (d.name == name && src.offsetOf(d.nameStart) == startOffset) {
|
if (d.name == name && matches(d.nameStart, d.name.length)) {
|
||||||
return when (d) {
|
return when (d) {
|
||||||
is MiniValDecl -> d.type
|
is MiniValDecl -> d.type ?: if (text != null && imported != null) inferTypeRefForVal(d, text, imported, mini) else null
|
||||||
is MiniFunDecl -> d.returnType
|
is MiniFunDecl -> d.returnType
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@ -106,25 +107,27 @@ object DocLookupUtils {
|
|||||||
|
|
||||||
if (d is MiniFunDecl) {
|
if (d is MiniFunDecl) {
|
||||||
for (p in d.params) {
|
for (p in d.params) {
|
||||||
if (p.name == name && src.offsetOf(p.nameStart) == startOffset) return p.type
|
if (p.name == name && matches(p.nameStart, p.name.length)) return p.type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (d is MiniClassDecl) {
|
if (d is MiniClassDecl) {
|
||||||
for (m in d.members) {
|
for (m in d.members) {
|
||||||
if (m.name == name && src.offsetOf(m.nameStart) == startOffset) {
|
if (m.name == name && matches(m.nameStart, m.name.length)) {
|
||||||
return when (m) {
|
return when (m) {
|
||||||
is MiniMemberFunDecl -> m.returnType
|
is MiniMemberFunDecl -> m.returnType
|
||||||
is MiniMemberValDecl -> m.type
|
is MiniMemberValDecl -> m.type ?: if (text != null && imported != null) {
|
||||||
|
inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
|
||||||
|
} else null
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (cf in d.ctorFields) {
|
for (cf in d.ctorFields) {
|
||||||
if (cf.name == name && src.offsetOf(cf.nameStart) == startOffset) return cf.type
|
if (cf.name == name && matches(cf.nameStart, cf.name.length)) return cf.type
|
||||||
}
|
}
|
||||||
for (cf in d.classFields) {
|
for (cf in d.classFields) {
|
||||||
if (cf.name == name && src.offsetOf(cf.nameStart) == startOffset) return cf.type
|
if (cf.name == name && matches(cf.nameStart, cf.name.length)) return cf.type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,6 +160,21 @@ object DocLookupUtils {
|
|||||||
return result.toList()
|
return result.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun extractLocalsAt(text: String, offset: Int): Set<String> {
|
||||||
|
val res = mutableSetOf<String>()
|
||||||
|
// 1) find val/var declarations
|
||||||
|
val re = Regex("(?:^|[\\n;])\\s*(?:val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)")
|
||||||
|
re.findAll(text).forEach { m ->
|
||||||
|
if (m.range.first < offset) res.add(m.groupValues[1])
|
||||||
|
}
|
||||||
|
// 2) find implicit assignments
|
||||||
|
val re2 = Regex("(?:^|[\\n;])\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*=[^=]")
|
||||||
|
re2.findAll(text).forEach { m ->
|
||||||
|
if (m.range.first < offset) res.add(m.groupValues[1])
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
fun extractImportsFromText(text: String): List<String> {
|
fun extractImportsFromText(text: String): List<String> {
|
||||||
val result = LinkedHashSet<String>()
|
val result = LinkedHashSet<String>()
|
||||||
val re = Regex("^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", RegexOption.MULTILINE)
|
val re = Regex("^\\s*import\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)", RegexOption.MULTILINE)
|
||||||
@ -232,24 +250,65 @@ object DocLookupUtils {
|
|||||||
for ((name, list) in buckets) {
|
for ((name, list) in buckets) {
|
||||||
result[name] = mergeClassDecls(name, list)
|
result[name] = mergeClassDecls(name, list)
|
||||||
}
|
}
|
||||||
|
// Root object alias
|
||||||
|
if (result.containsKey("Obj") && !result.containsKey("Any")) {
|
||||||
|
result["Any"] = result["Obj"]!!
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? {
|
fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String, localMini: MiniScript? = null): Pair<String, MiniNamedDecl>? {
|
||||||
val classes = aggregateClasses(importedModules, localMini)
|
val classes = aggregateClasses(importedModules, localMini)
|
||||||
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniMemberDecl>? {
|
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniNamedDecl>? {
|
||||||
val cls = classes[name] ?: return null
|
|
||||||
cls.members.firstOrNull { it.name == member }?.let { return name to it }
|
|
||||||
if (!visited.add(name)) return null
|
if (!visited.add(name)) return null
|
||||||
for (baseName in cls.bases) {
|
val cls = classes[name]
|
||||||
dfs(baseName, visited)?.let { return it }
|
if (cls != null) {
|
||||||
|
cls.members.firstOrNull { it.name == member }?.let { return name to it }
|
||||||
|
for (baseName in cls.bases) {
|
||||||
|
dfs(baseName, visited)?.let { return it }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Check for local extensions in this class or bases
|
||||||
|
localMini?.declarations?.firstOrNull { d ->
|
||||||
|
(d is MiniFunDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name && d.name == member) ||
|
||||||
|
(d is MiniValDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name && d.name == member)
|
||||||
|
}?.let { return name to it }
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return dfs(className, mutableSetOf())
|
return dfs(className, mutableSetOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findMemberAcrossClasses(importedModules: List<String>, member: String, localMini: MiniScript? = null): Pair<String, MiniMemberDecl>? {
|
fun collectExtensionMemberNames(importedModules: List<String>, className: String, localMini: MiniScript? = null): Set<String> {
|
||||||
|
val classes = aggregateClasses(importedModules, localMini)
|
||||||
|
val visited = mutableSetOf<String>()
|
||||||
|
val result = mutableSetOf<String>()
|
||||||
|
|
||||||
|
fun dfs(name: String) {
|
||||||
|
if (!visited.add(name)) return
|
||||||
|
// 1) stdlib extensions from BuiltinDocRegistry
|
||||||
|
result.addAll(BuiltinDocRegistry.extensionMemberNamesFor(name))
|
||||||
|
// 2) local extensions from mini
|
||||||
|
localMini?.declarations?.forEach { d ->
|
||||||
|
if (d is MiniFunDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name) result.add(d.name)
|
||||||
|
if (d is MiniValDecl && d.receiver != null && simpleClassNameOf(d.receiver) == name) result.add(d.name)
|
||||||
|
}
|
||||||
|
// 3) bases
|
||||||
|
classes[name]?.bases?.forEach { dfs(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
dfs(className)
|
||||||
|
// Hardcoded supplements for common containers if not explicitly in bases
|
||||||
|
if (className == "List" || className == "Array") {
|
||||||
|
dfs("Collection")
|
||||||
|
dfs("Iterable")
|
||||||
|
}
|
||||||
|
dfs("Any")
|
||||||
|
dfs("Obj")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findMemberAcrossClasses(importedModules: List<String>, member: String, localMini: MiniScript? = null): Pair<String, MiniNamedDecl>? {
|
||||||
val classes = aggregateClasses(importedModules, localMini)
|
val classes = aggregateClasses(importedModules, localMini)
|
||||||
// Preferred order for ambiguous common ops
|
// Preferred order for ambiguous common ops
|
||||||
val preference = listOf("Iterable", "Iterator", "List")
|
val preference = listOf("Iterable", "Iterator", "List")
|
||||||
@ -301,6 +360,12 @@ object DocLookupUtils {
|
|||||||
if (mini == null) return null
|
if (mini == null) return null
|
||||||
val i = prevNonWs(text, dotPos - 1)
|
val i = prevNonWs(text, dotPos - 1)
|
||||||
if (i < 0) return null
|
if (i < 0) return null
|
||||||
|
|
||||||
|
// Handle indexing x[0]. or literal [1].
|
||||||
|
if (text[i] == ']') {
|
||||||
|
return guessReceiverClass(text, dotPos, imported, mini)
|
||||||
|
}
|
||||||
|
|
||||||
val wordRange = wordRangeAt(text, i + 1) ?: return null
|
val wordRange = wordRangeAt(text, i + 1) ?: return null
|
||||||
val ident = text.substring(wordRange.first, wordRange.second)
|
val ident = text.substring(wordRange.first, wordRange.second)
|
||||||
|
|
||||||
@ -310,14 +375,14 @@ object DocLookupUtils {
|
|||||||
if (ref != null) {
|
if (ref != null) {
|
||||||
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
|
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId }
|
||||||
if (sym != null) {
|
if (sym != null) {
|
||||||
val type = findTypeByRange(mini, sym.name, sym.declStart)
|
val type = findTypeByRange(mini, sym.name, sym.declStart, text, imported)
|
||||||
simpleClassNameOf(type)?.let { return it }
|
simpleClassNameOf(type)?.let { return it }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check if it's a declaration (e.g. static access to a class)
|
// Check if it's a declaration (e.g. static access to a class)
|
||||||
val sym = binding.symbols.firstOrNull { it.declStart == wordRange.first && it.name == ident }
|
val sym = binding.symbols.firstOrNull { it.declStart == wordRange.first && it.name == ident }
|
||||||
if (sym != null) {
|
if (sym != null) {
|
||||||
val type = findTypeByRange(mini, sym.name, sym.declStart)
|
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) return sym.name
|
||||||
@ -325,13 +390,17 @@ object DocLookupUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Global declarations in current file (val/var/fun/class/enum)
|
// 1) Declarations in current file (val/var/fun/class/enum), prioritized by proximity
|
||||||
val d = mini.declarations.firstOrNull { it.name == ident }
|
val src = mini.range.start.source
|
||||||
|
val d = mini.declarations
|
||||||
|
.filter { it.name == ident && src.offsetOf(it.nameStart) < dotPos }
|
||||||
|
.maxByOrNull { src.offsetOf(it.nameStart) }
|
||||||
|
|
||||||
if (d != null) {
|
if (d != null) {
|
||||||
return when (d) {
|
return when (d) {
|
||||||
is MiniClassDecl -> d.name
|
is MiniClassDecl -> d.name
|
||||||
is MiniEnumDecl -> d.name
|
is MiniEnumDecl -> d.name
|
||||||
is MiniValDecl -> simpleClassNameOf(d.type)
|
is MiniValDecl -> simpleClassNameOf(d.type ?: inferTypeRefForVal(d, text, imported, mini))
|
||||||
is MiniFunDecl -> simpleClassNameOf(d.returnType)
|
is MiniFunDecl -> simpleClassNameOf(d.returnType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,6 +412,9 @@ object DocLookupUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2a) Try to find plain assignment in text if not found in declarations: x = test()
|
||||||
|
inferTypeFromAssignmentInText(ident, text, imported, mini, beforeOffset = dotPos)?.let { return simpleClassNameOf(it) }
|
||||||
|
|
||||||
// 3) Recursive chaining: Base.ident.
|
// 3) Recursive chaining: Base.ident.
|
||||||
val dotBefore = findDotLeft(text, wordRange.first)
|
val dotBefore = findDotLeft(text, wordRange.first)
|
||||||
if (dotBefore != null) {
|
if (dotBefore != null) {
|
||||||
@ -353,7 +425,7 @@ object DocLookupUtils {
|
|||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
val rt = when (val m = resolved.second) {
|
val rt = when (val m = resolved.second) {
|
||||||
is MiniMemberFunDecl -> m.returnType
|
is MiniMemberFunDecl -> m.returnType
|
||||||
is MiniMemberValDecl -> m.type
|
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, text, imported, mini)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
return simpleClassNameOf(rt)
|
return simpleClassNameOf(rt)
|
||||||
@ -368,6 +440,43 @@ object DocLookupUtils {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun inferTypeFromAssignmentInText(ident: String, text: String, imported: List<String>, mini: MiniScript?, beforeOffset: Int = Int.MAX_VALUE): MiniTypeRef? {
|
||||||
|
// Heuristic: search for "val ident =" or "ident =" in text
|
||||||
|
val re = Regex("(?:^|[\\n;])\\s*(?:val|var)?\\s*${ident}\\s*(?::\\s*([A-Za-z_][A-Za-z0-9_]*))?\\s*(?:=|by)\\s*([^\\n;]+)")
|
||||||
|
val match = re.findAll(text)
|
||||||
|
.filter { it.range.first < beforeOffset }
|
||||||
|
.lastOrNull() ?: return null
|
||||||
|
val explicitType = match.groupValues.getOrNull(1)?.takeIf { it.isNotBlank() }
|
||||||
|
if (explicitType != null) return syntheticTypeRef(explicitType)
|
||||||
|
val expr = match.groupValues.getOrNull(2)?.let { stripComments(it) } ?: return null
|
||||||
|
return inferTypeRefFromExpression(expr, imported, mini, contextText = text, beforeOffset = beforeOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stripComments(text: String): String {
|
||||||
|
var result = ""
|
||||||
|
var i = 0
|
||||||
|
var inString = false
|
||||||
|
while (i < text.length) {
|
||||||
|
val ch = text[i]
|
||||||
|
if (ch == '"' && (i == 0 || text[i - 1] != '\\')) {
|
||||||
|
inString = !inString
|
||||||
|
}
|
||||||
|
if (!inString && ch == '/' && i + 1 < text.length) {
|
||||||
|
if (text[i + 1] == '/') break // single line comment
|
||||||
|
if (text[i + 1] == '*') {
|
||||||
|
// Skip block comment
|
||||||
|
i += 2
|
||||||
|
while (i + 1 < text.length && !(text[i] == '*' && text[i + 1] == '/')) i++
|
||||||
|
i += 2 // Skip '*/'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result += ch
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return result.trim()
|
||||||
|
}
|
||||||
|
|
||||||
fun guessReturnClassFromMemberCallBeforeMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>, binding: BindingSnapshot? = null): String? {
|
fun guessReturnClassFromMemberCallBeforeMini(mini: MiniScript?, text: String, dotPos: Int, imported: List<String>, binding: BindingSnapshot? = null): String? {
|
||||||
if (mini == null) return null
|
if (mini == null) return null
|
||||||
var i = prevNonWs(text, dotPos - 1)
|
var i = prevNonWs(text, dotPos - 1)
|
||||||
@ -420,7 +529,9 @@ 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 MiniInitDecl -> null
|
is MiniFunDecl -> m.returnType
|
||||||
|
is MiniValDecl -> m.type
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
simpleClassNameOf(rt)
|
simpleClassNameOf(rt)
|
||||||
}
|
}
|
||||||
@ -455,13 +566,25 @@ object DocLookupUtils {
|
|||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null): String? {
|
fun guessReceiverClass(text: String, dotPos: Int, imported: List<String>, mini: MiniScript? = null, beforeOffset: Int = dotPos): String? {
|
||||||
guessClassFromCallBefore(text, dotPos, imported, mini)?.let { return it }
|
guessClassFromCallBefore(text, dotPos, imported, mini)?.let { return it }
|
||||||
var i = prevNonWs(text, dotPos - 1)
|
var i = prevNonWs(text, dotPos - 1)
|
||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
when (text[i]) {
|
when (text[i]) {
|
||||||
'"' -> return "String"
|
'"' -> return "String"
|
||||||
']' -> return "List"
|
']' -> {
|
||||||
|
// Check if literal or indexing
|
||||||
|
val matchingOpen = findMatchingOpenBracket(text, i)
|
||||||
|
if (matchingOpen != null && matchingOpen > 0) {
|
||||||
|
val beforeOpen = prevNonWs(text, matchingOpen - 1)
|
||||||
|
if (beforeOpen >= 0 && (isIdentChar(text[beforeOpen]) || text[beforeOpen] == ')' || text[beforeOpen] == ']')) {
|
||||||
|
// Likely indexing: infer type of full expression
|
||||||
|
val exprText = text.substring(0, i + 1)
|
||||||
|
return simpleClassNameOf(inferTypeRefFromExpression(exprText, imported, mini, beforeOffset = beforeOffset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "List"
|
||||||
|
}
|
||||||
'}' -> return "Dict"
|
'}' -> return "Dict"
|
||||||
')' -> {
|
')' -> {
|
||||||
// Parenthesized expression: walk back to matching '(' and inspect the inner expression
|
// Parenthesized expression: walk back to matching '(' and inspect the inner expression
|
||||||
@ -564,7 +687,9 @@ object DocLookupUtils {
|
|||||||
val ret = when (member) {
|
val ret = when (member) {
|
||||||
is MiniMemberFunDecl -> member.returnType
|
is MiniMemberFunDecl -> member.returnType
|
||||||
is MiniMemberValDecl -> member.type
|
is MiniMemberValDecl -> member.type
|
||||||
is MiniInitDecl -> null
|
is MiniFunDecl -> member.returnType
|
||||||
|
is MiniValDecl -> member.type
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
return simpleClassNameOf(ret)
|
return simpleClassNameOf(ret)
|
||||||
}
|
}
|
||||||
@ -627,11 +752,216 @@ object DocLookupUtils {
|
|||||||
val ret = when (member) {
|
val ret = when (member) {
|
||||||
is MiniMemberFunDecl -> member.returnType
|
is MiniMemberFunDecl -> member.returnType
|
||||||
is MiniMemberValDecl -> member.type
|
is MiniMemberValDecl -> member.type
|
||||||
is MiniInitDecl -> null
|
is MiniFunDecl -> member.returnType
|
||||||
|
is MiniValDecl -> member.type
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
return simpleClassNameOf(ret)
|
return simpleClassNameOf(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun inferTypeRefFromExpression(text: String, imported: List<String>, mini: MiniScript? = null, contextText: String? = null, beforeOffset: Int = Int.MAX_VALUE): MiniTypeRef? {
|
||||||
|
val trimmed = stripComments(text)
|
||||||
|
if (trimmed.isEmpty()) return null
|
||||||
|
val fullText = contextText ?: text
|
||||||
|
|
||||||
|
// 1) Literals
|
||||||
|
if (trimmed.startsWith("\"")) return syntheticTypeRef("String")
|
||||||
|
if (trimmed.startsWith("[")) return syntheticTypeRef("List")
|
||||||
|
if (trimmed.startsWith("{")) return syntheticTypeRef("Dict")
|
||||||
|
if (trimmed == "true" || trimmed == "false") return syntheticTypeRef("Boolean")
|
||||||
|
if (trimmed.all { it.isDigit() || it == '.' || it == '_' || it == 'e' || it == 'E' }) {
|
||||||
|
val hasDigits = trimmed.any { it.isDigit() }
|
||||||
|
if (hasDigits)
|
||||||
|
return if (trimmed.contains('.') || trimmed.contains('e', ignoreCase = true)) syntheticTypeRef("Real") else syntheticTypeRef("Int")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Function/Constructor calls or Indexing
|
||||||
|
if (trimmed.endsWith(")")) {
|
||||||
|
val openParen = findMatchingOpenParen(trimmed, trimmed.length - 1)
|
||||||
|
if (openParen != null && openParen > 0) {
|
||||||
|
var j = openParen - 1
|
||||||
|
while (j >= 0 && trimmed[j].isWhitespace()) j--
|
||||||
|
val end = j + 1
|
||||||
|
while (j >= 0 && isIdentChar(trimmed[j])) j--
|
||||||
|
val start = j + 1
|
||||||
|
if (start < end) {
|
||||||
|
val callee = trimmed.substring(start, end)
|
||||||
|
|
||||||
|
// Check if it's a member call (dot before callee)
|
||||||
|
var k = start - 1
|
||||||
|
while (k >= 0 && trimmed[k].isWhitespace()) k--
|
||||||
|
if (k >= 0 && trimmed[k] == '.') {
|
||||||
|
val prevDot = k
|
||||||
|
// Recursive: try to infer type of what's before the dot
|
||||||
|
val receiverText = trimmed.substring(0, prevDot)
|
||||||
|
val receiverType = inferTypeRefFromExpression(receiverText, imported, mini, contextText = fullText, beforeOffset = beforeOffset)
|
||||||
|
val receiverClass = simpleClassNameOf(receiverType)
|
||||||
|
if (receiverClass != null) {
|
||||||
|
val resolved = resolveMemberWithInheritance(imported, receiverClass, callee, mini)
|
||||||
|
if (resolved != null) {
|
||||||
|
return when (val m = resolved.second) {
|
||||||
|
is MiniMemberFunDecl -> m.returnType
|
||||||
|
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Top-level call or constructor
|
||||||
|
val classes = aggregateClasses(imported, mini)
|
||||||
|
if (classes.containsKey(callee)) return syntheticTypeRef(callee)
|
||||||
|
|
||||||
|
for (mod in imported) {
|
||||||
|
val decls = BuiltinDocRegistry.docsForModule(mod)
|
||||||
|
val fn = decls.asSequence().filterIsInstance<MiniFunDecl>().firstOrNull { it.name == callee }
|
||||||
|
if (fn != null) return fn.returnType
|
||||||
|
}
|
||||||
|
mini?.declarations?.filterIsInstance<MiniFunDecl>()?.firstOrNull { it.name == callee }?.let { return it.returnType }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.endsWith("]")) {
|
||||||
|
val openBracket = findMatchingOpenBracket(trimmed, trimmed.length - 1)
|
||||||
|
if (openBracket != null && openBracket > 0) {
|
||||||
|
val receiverText = trimmed.substring(0, openBracket).trim()
|
||||||
|
if (receiverText.isNotEmpty()) {
|
||||||
|
val receiverType = inferTypeRefFromExpression(receiverText, imported, mini, contextText = fullText, beforeOffset = beforeOffset)
|
||||||
|
if (receiverType is MiniGenericType) {
|
||||||
|
val baseName = simpleClassNameOf(receiverType.base)
|
||||||
|
if (baseName == "List" && receiverType.args.isNotEmpty()) {
|
||||||
|
return receiverType.args[0]
|
||||||
|
}
|
||||||
|
if (baseName == "Map" && receiverType.args.size >= 2) {
|
||||||
|
return receiverType.args[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback for non-generic collections or if base name matches
|
||||||
|
val baseName = simpleClassNameOf(receiverType)
|
||||||
|
if (baseName == "List" || baseName == "Array" || baseName == "String") {
|
||||||
|
if (baseName == "String") return syntheticTypeRef("Char")
|
||||||
|
return syntheticTypeRef("Any")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Member field or simple identifier at the end
|
||||||
|
val lastWord = wordRangeAt(trimmed, trimmed.length)
|
||||||
|
if (lastWord != null && lastWord.second == trimmed.length) {
|
||||||
|
val ident = trimmed.substring(lastWord.first, lastWord.second)
|
||||||
|
var k = lastWord.first - 1
|
||||||
|
while (k >= 0 && trimmed[k].isWhitespace()) k--
|
||||||
|
if (k >= 0 && trimmed[k] == '.') {
|
||||||
|
// Member field: receiver.ident
|
||||||
|
val receiverText = trimmed.substring(0, k).trim()
|
||||||
|
val receiverType = inferTypeRefFromExpression(receiverText, imported, mini, contextText = fullText, beforeOffset = beforeOffset)
|
||||||
|
val receiverClass = simpleClassNameOf(receiverType)
|
||||||
|
if (receiverClass != null) {
|
||||||
|
val resolved = resolveMemberWithInheritance(imported, receiverClass, ident, mini)
|
||||||
|
if (resolved != null) {
|
||||||
|
return when (val m = resolved.second) {
|
||||||
|
is MiniMemberFunDecl -> m.returnType
|
||||||
|
is MiniMemberValDecl -> m.type ?: inferTypeRefFromInitRange(m.initRange, m.nameStart, fullText, imported, mini)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Simple identifier
|
||||||
|
// 1) Declarations in current file (val/var/fun/class/enum), prioritized by proximity
|
||||||
|
val src = mini?.range?.start?.source
|
||||||
|
val d = if (src != null) {
|
||||||
|
mini.declarations
|
||||||
|
.filter { it.name == ident && src.offsetOf(it.nameStart) < beforeOffset }
|
||||||
|
.maxByOrNull { src.offsetOf(it.nameStart) }
|
||||||
|
} else {
|
||||||
|
mini?.declarations?.firstOrNull { it.name == ident }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d != null) {
|
||||||
|
return when (d) {
|
||||||
|
is MiniClassDecl -> syntheticTypeRef(d.name)
|
||||||
|
is MiniEnumDecl -> syntheticTypeRef(d.name)
|
||||||
|
is MiniValDecl -> d.type ?: inferTypeRefForVal(d, fullText, imported, mini)
|
||||||
|
is MiniFunDecl -> d.returnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Parameters in any function
|
||||||
|
for (fd in mini?.declarations?.filterIsInstance<MiniFunDecl>() ?: emptyList()) {
|
||||||
|
for (p in fd.params) {
|
||||||
|
if (p.name == ident) return p.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Try to find plain assignment in text: ident = expr
|
||||||
|
inferTypeFromAssignmentInText(ident, fullText, imported, mini, beforeOffset = beforeOffset)?.let { return it }
|
||||||
|
|
||||||
|
// 4) Check if it's a known class (static access)
|
||||||
|
val classes = aggregateClasses(imported, mini)
|
||||||
|
if (classes.containsKey(ident)) return syntheticTypeRef(ident)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findMatchingOpenBracket(text: String, closeBracketPos: Int): Int? {
|
||||||
|
if (closeBracketPos < 0 || closeBracketPos >= text.length || text[closeBracketPos] != ']') return null
|
||||||
|
var depth = 0
|
||||||
|
var i = closeBracketPos - 1
|
||||||
|
while (i >= 0) {
|
||||||
|
when (text[i]) {
|
||||||
|
']' -> depth++
|
||||||
|
'[' -> if (depth == 0) return i else depth--
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findMatchingOpenParen(text: String, closeParenPos: Int): Int? {
|
||||||
|
if (closeParenPos < 0 || closeParenPos >= text.length || text[closeParenPos] != ')') return null
|
||||||
|
var depth = 0
|
||||||
|
var i = closeParenPos - 1
|
||||||
|
while (i >= 0) {
|
||||||
|
when (text[i]) {
|
||||||
|
')' -> depth++
|
||||||
|
'(' -> if (depth == 0) return i else depth--
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syntheticTypeRef(name: String): MiniTypeRef =
|
||||||
|
MiniTypeName(MiniRange(net.sergeych.lyng.Pos.builtIn, net.sergeych.lyng.Pos.builtIn),
|
||||||
|
listOf(MiniTypeName.Segment(name, MiniRange(net.sergeych.lyng.Pos.builtIn, net.sergeych.lyng.Pos.builtIn))), false)
|
||||||
|
|
||||||
|
fun inferTypeRefForVal(vd: MiniValDecl, text: String, imported: List<String>, mini: MiniScript?): MiniTypeRef? {
|
||||||
|
return inferTypeRefFromInitRange(vd.initRange, vd.nameStart, text, imported, mini)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inferTypeRefFromInitRange(initRange: MiniRange?, nameStart: net.sergeych.lyng.Pos, text: String, imported: List<String>, mini: MiniScript?): MiniTypeRef? {
|
||||||
|
val range = initRange ?: return null
|
||||||
|
val src = mini?.range?.start?.source ?: return null
|
||||||
|
val start = src.offsetOf(range.start)
|
||||||
|
val end = src.offsetOf(range.end)
|
||||||
|
if (start < 0 || start >= end || end > text.length) return null
|
||||||
|
|
||||||
|
var exprText = text.substring(start, end).trim()
|
||||||
|
if (exprText.startsWith("=")) {
|
||||||
|
exprText = exprText.substring(1).trim()
|
||||||
|
}
|
||||||
|
if (exprText.startsWith("by")) {
|
||||||
|
exprText = exprText.substring(2).trim()
|
||||||
|
}
|
||||||
|
val beforeOffset = src.offsetOf(nameStart)
|
||||||
|
return inferTypeRefFromExpression(exprText, imported, mini, contextText = text, beforeOffset = beforeOffset)
|
||||||
|
}
|
||||||
|
|
||||||
fun simpleClassNameOf(t: MiniTypeRef?): String? = when (t) {
|
fun simpleClassNameOf(t: MiniTypeRef?): String? = when (t) {
|
||||||
null -> null
|
null -> null
|
||||||
is MiniTypeName -> t.segments.lastOrNull()?.name
|
is MiniTypeName -> t.segments.lastOrNull()?.name
|
||||||
@ -667,13 +997,13 @@ object DocLookupUtils {
|
|||||||
fun enumToSyntheticClass(en: MiniEnumDecl): MiniClassDecl {
|
fun enumToSyntheticClass(en: MiniEnumDecl): MiniClassDecl {
|
||||||
val staticMembers = mutableListOf<MiniMemberDecl>()
|
val staticMembers = mutableListOf<MiniMemberDecl>()
|
||||||
// entries: List
|
// entries: List
|
||||||
staticMembers.add(MiniMemberValDecl(en.range, "entries", false, null, null, en.nameStart, isStatic = true))
|
staticMembers.add(MiniMemberValDecl(en.range, "entries", false, null, null, null, en.nameStart, isStatic = true))
|
||||||
// valueOf(name: String): Enum
|
// valueOf(name: String): Enum
|
||||||
staticMembers.add(MiniMemberFunDecl(en.range, "valueOf", listOf(MiniParam("name", null, en.nameStart)), null, null, en.nameStart, isStatic = true))
|
staticMembers.add(MiniMemberFunDecl(en.range, "valueOf", listOf(MiniParam("name", null, en.nameStart)), null, null, en.nameStart, isStatic = true))
|
||||||
|
|
||||||
// Also add each entry as a static member (const)
|
// Also add each entry as a static member (const)
|
||||||
for (entry in en.entries) {
|
for (entry in en.entries) {
|
||||||
staticMembers.add(MiniMemberValDecl(en.range, entry, false, MiniTypeName(en.range, listOf(MiniTypeName.Segment(en.name, en.range)), false), null, en.nameStart, isStatic = true))
|
staticMembers.add(MiniMemberValDecl(en.range, entry, false, MiniTypeName(en.range, listOf(MiniTypeName.Segment(en.name, en.range)), false), null, null, en.nameStart, isStatic = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
return MiniClassDecl(
|
return MiniClassDecl(
|
||||||
|
|||||||
@ -81,7 +81,7 @@ data class MiniTypeVar(
|
|||||||
) : MiniTypeRef
|
) : MiniTypeRef
|
||||||
|
|
||||||
// Script and declarations (lean subset; can be extended later)
|
// Script and declarations (lean subset; can be extended later)
|
||||||
sealed interface MiniDecl : MiniNode {
|
sealed interface MiniNamedDecl : MiniNode {
|
||||||
val name: String
|
val name: String
|
||||||
val doc: MiniDoc?
|
val doc: MiniDoc?
|
||||||
// Start position of the declaration name identifier in source; end can be derived as start + name.length
|
// Start position of the declaration name identifier in source; end can be derived as start + name.length
|
||||||
@ -89,6 +89,8 @@ sealed interface MiniDecl : MiniNode {
|
|||||||
val isExtern: Boolean
|
val isExtern: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed interface MiniDecl : MiniNamedDecl
|
||||||
|
|
||||||
data class MiniScript(
|
data class MiniScript(
|
||||||
override val range: MiniRange,
|
override val range: MiniRange,
|
||||||
val declarations: MutableList<MiniDecl> = mutableListOf(),
|
val declarations: MutableList<MiniDecl> = mutableListOf(),
|
||||||
@ -172,12 +174,8 @@ data class MiniIdentifier(
|
|||||||
) : MiniNode
|
) : MiniNode
|
||||||
|
|
||||||
// --- Class member declarations (for built-in/registry docs) ---
|
// --- Class member declarations (for built-in/registry docs) ---
|
||||||
sealed interface MiniMemberDecl : MiniNode {
|
sealed interface MiniMemberDecl : MiniNamedDecl {
|
||||||
val name: String
|
|
||||||
val doc: MiniDoc?
|
|
||||||
val nameStart: Pos
|
|
||||||
val isStatic: Boolean
|
val isStatic: Boolean
|
||||||
val isExtern: Boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MiniMemberFunDecl(
|
data class MiniMemberFunDecl(
|
||||||
@ -189,6 +187,7 @@ data class MiniMemberFunDecl(
|
|||||||
override val nameStart: Pos,
|
override val nameStart: Pos,
|
||||||
override val isStatic: Boolean = false,
|
override val isStatic: Boolean = false,
|
||||||
override val isExtern: Boolean = false,
|
override val isExtern: Boolean = false,
|
||||||
|
val body: MiniBlock? = null
|
||||||
) : MiniMemberDecl
|
) : MiniMemberDecl
|
||||||
|
|
||||||
data class MiniMemberValDecl(
|
data class MiniMemberValDecl(
|
||||||
@ -196,6 +195,7 @@ data class MiniMemberValDecl(
|
|||||||
override val name: String,
|
override val name: String,
|
||||||
val mutable: Boolean,
|
val mutable: Boolean,
|
||||||
val type: MiniTypeRef?,
|
val type: MiniTypeRef?,
|
||||||
|
val initRange: MiniRange?,
|
||||||
override val doc: MiniDoc?,
|
override val doc: MiniDoc?,
|
||||||
override val nameStart: Pos,
|
override val nameStart: Pos,
|
||||||
override val isStatic: Boolean = false,
|
override val isStatic: Boolean = false,
|
||||||
@ -222,6 +222,9 @@ interface MiniAstSink {
|
|||||||
fun onEnterClass(node: MiniClassDecl) {}
|
fun onEnterClass(node: MiniClassDecl) {}
|
||||||
fun onExitClass(end: Pos) {}
|
fun onExitClass(end: Pos) {}
|
||||||
|
|
||||||
|
fun onEnterFunction(node: MiniFunDecl?) {}
|
||||||
|
fun onExitFunction(end: Pos) {}
|
||||||
|
|
||||||
fun onImport(node: MiniImport) {}
|
fun onImport(node: MiniImport) {}
|
||||||
fun onFunDecl(node: MiniFunDecl) {}
|
fun onFunDecl(node: MiniFunDecl) {}
|
||||||
fun onValDecl(node: MiniValDecl) {}
|
fun onValDecl(node: MiniValDecl) {}
|
||||||
@ -254,6 +257,7 @@ class MiniAstBuilder : MiniAstSink {
|
|||||||
private val classStack = ArrayDeque<MiniClassDecl>()
|
private val classStack = ArrayDeque<MiniClassDecl>()
|
||||||
private var lastDoc: MiniDoc? = null
|
private var lastDoc: MiniDoc? = null
|
||||||
private var scriptDepth: Int = 0
|
private var scriptDepth: Int = 0
|
||||||
|
private var functionDepth: Int = 0
|
||||||
|
|
||||||
fun build(): MiniScript? = currentScript
|
fun build(): MiniScript? = currentScript
|
||||||
|
|
||||||
@ -291,6 +295,14 @@ class MiniAstBuilder : MiniAstSink {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onEnterFunction(node: MiniFunDecl?) {
|
||||||
|
functionDepth++
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onExitFunction(end: Pos) {
|
||||||
|
functionDepth--
|
||||||
|
}
|
||||||
|
|
||||||
override fun onImport(node: MiniImport) {
|
override fun onImport(node: MiniImport) {
|
||||||
currentScript?.imports?.add(node)
|
currentScript?.imports?.add(node)
|
||||||
}
|
}
|
||||||
@ -298,7 +310,7 @@ class MiniAstBuilder : MiniAstSink {
|
|||||||
override fun onFunDecl(node: MiniFunDecl) {
|
override fun onFunDecl(node: MiniFunDecl) {
|
||||||
val attach = node.copy(doc = node.doc ?: lastDoc)
|
val attach = node.copy(doc = node.doc ?: lastDoc)
|
||||||
val currentClass = classStack.lastOrNull()
|
val currentClass = classStack.lastOrNull()
|
||||||
if (currentClass != null) {
|
if (currentClass != null && functionDepth == 0) {
|
||||||
// Convert MiniFunDecl to MiniMemberFunDecl for inclusion in members
|
// Convert MiniFunDecl to MiniMemberFunDecl for inclusion in members
|
||||||
val member = MiniMemberFunDecl(
|
val member = MiniMemberFunDecl(
|
||||||
range = attach.range,
|
range = attach.range,
|
||||||
@ -308,13 +320,29 @@ class MiniAstBuilder : MiniAstSink {
|
|||||||
doc = attach.doc,
|
doc = attach.doc,
|
||||||
nameStart = attach.nameStart,
|
nameStart = attach.nameStart,
|
||||||
isStatic = false, // TODO: track static if needed
|
isStatic = false, // TODO: track static if needed
|
||||||
isExtern = attach.isExtern
|
isExtern = attach.isExtern,
|
||||||
|
body = attach.body
|
||||||
)
|
)
|
||||||
// Need to update the class in the stack since it's immutable-ish (data class)
|
// Need to update the class in the stack since it's immutable-ish (data class)
|
||||||
classStack.removeLast()
|
// Check if we already have this member (from a previous onFunDecl call for the same function)
|
||||||
classStack.addLast(currentClass.copy(members = currentClass.members + member))
|
val existing = currentClass.members.filterIsInstance<MiniMemberFunDecl>().find { it.name == attach.name && it.nameStart == attach.nameStart }
|
||||||
|
if (existing != null) {
|
||||||
|
val members = currentClass.members.map { if (it === existing) member else it }
|
||||||
|
classStack.removeLast()
|
||||||
|
classStack.addLast(currentClass.copy(members = members))
|
||||||
|
} else {
|
||||||
|
classStack.removeLast()
|
||||||
|
classStack.addLast(currentClass.copy(members = currentClass.members + member))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
currentScript?.declarations?.add(attach)
|
// Check if already in declarations to avoid duplication
|
||||||
|
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
|
lastDoc = null
|
||||||
}
|
}
|
||||||
@ -322,21 +350,36 @@ class MiniAstBuilder : MiniAstSink {
|
|||||||
override fun onValDecl(node: MiniValDecl) {
|
override fun onValDecl(node: MiniValDecl) {
|
||||||
val attach = node.copy(doc = node.doc ?: lastDoc)
|
val attach = node.copy(doc = node.doc ?: lastDoc)
|
||||||
val currentClass = classStack.lastOrNull()
|
val currentClass = classStack.lastOrNull()
|
||||||
if (currentClass != null) {
|
if (currentClass != null && functionDepth == 0) {
|
||||||
val member = MiniMemberValDecl(
|
val member = MiniMemberValDecl(
|
||||||
range = attach.range,
|
range = attach.range,
|
||||||
name = attach.name,
|
name = attach.name,
|
||||||
mutable = attach.mutable,
|
mutable = attach.mutable,
|
||||||
type = attach.type,
|
type = attach.type,
|
||||||
|
initRange = attach.initRange,
|
||||||
doc = attach.doc,
|
doc = attach.doc,
|
||||||
nameStart = attach.nameStart,
|
nameStart = attach.nameStart,
|
||||||
isStatic = false, // TODO: track static if needed
|
isStatic = false, // TODO: track static if needed
|
||||||
isExtern = attach.isExtern
|
isExtern = attach.isExtern
|
||||||
)
|
)
|
||||||
classStack.removeLast()
|
// Duplicates for vals are rare but possible if Compiler calls it twice
|
||||||
classStack.addLast(currentClass.copy(members = currentClass.members + member))
|
val existing = currentClass.members.filterIsInstance<MiniMemberValDecl>().find { it.name == attach.name && it.nameStart == attach.nameStart }
|
||||||
|
if (existing != null) {
|
||||||
|
val members = currentClass.members.map { if (it === existing) member else it }
|
||||||
|
classStack.removeLast()
|
||||||
|
classStack.addLast(currentClass.copy(members = members))
|
||||||
|
} else {
|
||||||
|
classStack.removeLast()
|
||||||
|
classStack.addLast(currentClass.copy(members = currentClass.members + member))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
currentScript?.declarations?.add(attach)
|
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
|
lastDoc = null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
package net.sergeych.lyng.miniast
|
package net.sergeych.lyng.miniast
|
||||||
|
|
||||||
import net.sergeych.lyng.obj.ObjString
|
|
||||||
|
|
||||||
object StdlibDocsBootstrap {
|
object StdlibDocsBootstrap {
|
||||||
// Simple idempotent guard; races are harmless as initializer side-effects are idempotent
|
// Simple idempotent guard; races are harmless as initializer side-effects are idempotent
|
||||||
private var ensured = false
|
private var ensured = false
|
||||||
@ -16,7 +14,25 @@ object StdlibDocsBootstrap {
|
|||||||
// Touch core Obj* types whose docs are registered via addFnDoc/addConstDoc
|
// Touch core Obj* types whose docs are registered via addFnDoc/addConstDoc
|
||||||
// Accessing .type forces their static initializers to run and register docs.
|
// Accessing .type forces their static initializers to run and register docs.
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
val _string = ObjString.type
|
val _string = net.sergeych.lyng.obj.ObjString.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _any = net.sergeych.lyng.obj.Obj.rootObjectType
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _list = net.sergeych.lyng.obj.ObjList.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _map = net.sergeych.lyng.obj.ObjMap.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _int = net.sergeych.lyng.obj.ObjInt.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _real = net.sergeych.lyng.obj.ObjReal.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _bool = net.sergeych.lyng.obj.ObjBool.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _regex = net.sergeych.lyng.obj.ObjRegex.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _range = net.sergeych.lyng.obj.ObjRange.type
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val _buffer = net.sergeych.lyng.obj.ObjBuffer.type
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
// Best-effort; absence should not break consumers
|
// Best-effort; absence should not break consumers
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -25,6 +25,9 @@ import kotlinx.serialization.json.JsonElement
|
|||||||
import kotlinx.serialization.json.JsonNull
|
import kotlinx.serialization.json.JsonNull
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
import net.sergeych.lyng.*
|
import net.sergeych.lyng.*
|
||||||
|
import net.sergeych.lyng.miniast.ParamDoc
|
||||||
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
|
import net.sergeych.lyng.miniast.type
|
||||||
import net.sergeych.lynon.LynonDecoder
|
import net.sergeych.lynon.LynonDecoder
|
||||||
import net.sergeych.lynon.LynonEncoder
|
import net.sergeych.lynon.LynonEncoder
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
@ -524,20 +527,46 @@ open class Obj {
|
|||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val rootObjectType = ObjClass("Obj").apply {
|
val rootObjectType = ObjClass("Obj").apply {
|
||||||
addFn("toString", true) {
|
addFnDoc(
|
||||||
|
name = "toString",
|
||||||
|
doc = "Returns a string representation of the object.",
|
||||||
|
returns = type("lyng.String"),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
thisObj.toString(this, true)
|
thisObj.toString(this, true)
|
||||||
}
|
}
|
||||||
addFn("inspect", true) {
|
addFnDoc(
|
||||||
|
name = "inspect",
|
||||||
|
doc = "Returns a detailed string representation for debugging.",
|
||||||
|
returns = type("lyng.String"),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
thisObj.inspect(this).toObj()
|
thisObj.inspect(this).toObj()
|
||||||
}
|
}
|
||||||
addFn("contains") {
|
addFnDoc(
|
||||||
|
name = "contains",
|
||||||
|
doc = "Returns true if the object contains the given element.",
|
||||||
|
params = listOf(ParamDoc("element")),
|
||||||
|
returns = type("lyng.Bool"),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
ObjBool(thisObj.contains(this, args.firstAndOnly()))
|
ObjBool(thisObj.contains(this, args.firstAndOnly()))
|
||||||
}
|
}
|
||||||
// utilities
|
// utilities
|
||||||
addFn("let") {
|
addFnDoc(
|
||||||
|
name = "let",
|
||||||
|
doc = "Calls the specified function block with `this` value as its argument and returns its result.",
|
||||||
|
params = listOf(ParamDoc("block")),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
|
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
|
||||||
}
|
}
|
||||||
addFn("apply") {
|
addFnDoc(
|
||||||
|
name = "apply",
|
||||||
|
doc = "Calls the specified function block with `this` value as its receiver and returns `this` value.",
|
||||||
|
params = listOf(ParamDoc("block")),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
val body = args.firstAndOnly()
|
val body = args.firstAndOnly()
|
||||||
(thisObj as? ObjInstance)?.let {
|
(thisObj as? ObjInstance)?.let {
|
||||||
body.callOn(ApplyScope(this, it.instanceScope))
|
body.callOn(ApplyScope(this, it.instanceScope))
|
||||||
@ -546,11 +575,21 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
thisObj
|
thisObj
|
||||||
}
|
}
|
||||||
addFn("also") {
|
addFnDoc(
|
||||||
|
name = "also",
|
||||||
|
doc = "Calls the specified function block with `this` value as its argument and returns `this` value.",
|
||||||
|
params = listOf(ParamDoc("block")),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
|
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
|
||||||
thisObj
|
thisObj
|
||||||
}
|
}
|
||||||
addFn("run") {
|
addFnDoc(
|
||||||
|
name = "run",
|
||||||
|
doc = "Calls the specified function block with `this` value as its receiver and returns its result.",
|
||||||
|
params = listOf(ParamDoc("block")),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
args.firstAndOnly().callOn(this)
|
args.firstAndOnly().callOn(this)
|
||||||
}
|
}
|
||||||
addFn("getAt") {
|
addFn("getAt") {
|
||||||
@ -563,7 +602,12 @@ open class Obj {
|
|||||||
thisObj.putAt(this, requiredArg<Obj>(0), newValue)
|
thisObj.putAt(this, requiredArg<Obj>(0), newValue)
|
||||||
newValue
|
newValue
|
||||||
}
|
}
|
||||||
addFn("toJsonString") {
|
addFnDoc(
|
||||||
|
name = "toJsonString",
|
||||||
|
doc = "Encodes this object to a JSON string.",
|
||||||
|
returns = type("lyng.String"),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
thisObj.toJson(this).toString().toObj()
|
thisObj.toJson(this).toString().toObj()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ package net.sergeych.lyng.obj
|
|||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
import net.sergeych.lynon.LynonDecoder
|
import net.sergeych.lynon.LynonDecoder
|
||||||
import net.sergeych.lynon.LynonEncoder
|
import net.sergeych.lynon.LynonEncoder
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
@ -178,7 +179,12 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
|||||||
else -> scope.raiseIllegalState("illegal type code for Int: $lynonType")
|
else -> scope.raiseIllegalState("illegal type code for Int: $lynonType")
|
||||||
}
|
}
|
||||||
}.apply {
|
}.apply {
|
||||||
addFn("toInt") {
|
addFnDoc(
|
||||||
|
name = "toInt",
|
||||||
|
doc = "Returns this integer (identity operation).",
|
||||||
|
returns = net.sergeych.lyng.miniast.type("lyng.Int"),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
thisObj
|
thisObj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
package net.sergeych.lyng
|
package net.sergeych.lyng
|
||||||
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
@ -274,6 +275,89 @@ class MiniAstTest {
|
|||||||
assertEquals("Doc6", e1.doc?.summary)
|
assertEquals("Doc6", e1.doc?.summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun resolve_inferred_member_type() = runTest {
|
||||||
|
val code = """
|
||||||
|
object O3 {
|
||||||
|
val name = "ozone"
|
||||||
|
}
|
||||||
|
val x = O3.name
|
||||||
|
""".trimIndent()
|
||||||
|
val (_, sink) = compileWithMini(code)
|
||||||
|
val mini = sink.build()
|
||||||
|
val type = DocLookupUtils.findTypeByRange(mini, "x", code.indexOf("val x") + 4, code, emptyList())
|
||||||
|
assertEquals("String", DocLookupUtils.simpleClassNameOf(type))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun resolve_inferred_val_type_from_extern_fun() = runTest {
|
||||||
|
val code = """
|
||||||
|
extern fun test(a: Int): List<Int>
|
||||||
|
val x = test(1)
|
||||||
|
""".trimIndent()
|
||||||
|
val (_, sink) = compileWithMini(code)
|
||||||
|
val mini = sink.build()
|
||||||
|
assertNotNull(mini)
|
||||||
|
val vd = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
|
||||||
|
assertNotNull(vd)
|
||||||
|
|
||||||
|
val inferred = DocLookupUtils.inferTypeRefForVal(vd, code, emptyList(), mini)
|
||||||
|
assertNotNull(inferred)
|
||||||
|
assertTrue(inferred is MiniGenericType)
|
||||||
|
assertEquals("List", (inferred.base as MiniTypeName).segments.last().name)
|
||||||
|
|
||||||
|
val code2 = """
|
||||||
|
extern fun test2(a: Int): String
|
||||||
|
val y = test2(1)
|
||||||
|
""".trimIndent()
|
||||||
|
val (_, sink2) = compileWithMini(code2)
|
||||||
|
val mini2 = sink2.build()
|
||||||
|
val vd2 = mini2?.declarations?.filterIsInstance<MiniValDecl>()?.firstOrNull { it.name == "y" }
|
||||||
|
assertNotNull(vd2)
|
||||||
|
val inferred2 = DocLookupUtils.inferTypeRefForVal(vd2, code2, emptyList(), mini2)
|
||||||
|
assertNotNull(inferred2)
|
||||||
|
assertTrue(inferred2 is MiniTypeName)
|
||||||
|
assertEquals("String", inferred2.segments.last().name)
|
||||||
|
|
||||||
|
val code3 = """
|
||||||
|
extern object API {
|
||||||
|
fun getData(): List<String>
|
||||||
|
}
|
||||||
|
val x = API.getData()
|
||||||
|
""".trimIndent()
|
||||||
|
val (_, sink3) = compileWithMini(code3)
|
||||||
|
val mini3 = sink3.build()
|
||||||
|
val vd3 = mini3?.declarations?.filterIsInstance<MiniValDecl>()?.firstOrNull { it.name == "x" }
|
||||||
|
assertNotNull(vd3)
|
||||||
|
val inferred3 = DocLookupUtils.inferTypeRefForVal(vd3, code3, emptyList(), mini3)
|
||||||
|
assertNotNull(inferred3)
|
||||||
|
assertTrue(inferred3 is MiniGenericType)
|
||||||
|
assertEquals("List", (inferred3.base as MiniTypeName).segments.last().name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun resolve_inferred_val_type_cross_script() = runTest {
|
||||||
|
val dCode = "extern fun test(a: Int): List<Int>"
|
||||||
|
val mainCode = "val x = test(1)"
|
||||||
|
|
||||||
|
val (_, dSink) = compileWithMini(dCode)
|
||||||
|
val dMini = dSink.build()!!
|
||||||
|
|
||||||
|
val (_, mainSink) = compileWithMini(mainCode)
|
||||||
|
val mainMini = mainSink.build()!!
|
||||||
|
|
||||||
|
// Merge manually
|
||||||
|
val merged = mainMini.copy(declarations = (mainMini.declarations + dMini.declarations).toMutableList())
|
||||||
|
|
||||||
|
val vd = merged.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
|
||||||
|
assertNotNull(vd)
|
||||||
|
|
||||||
|
val inferred = DocLookupUtils.inferTypeRefForVal(vd, mainCode, emptyList(), merged)
|
||||||
|
assertNotNull(inferred)
|
||||||
|
assertTrue(inferred is MiniGenericType)
|
||||||
|
assertEquals("List", (inferred.base as MiniTypeName).segments.last().name)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun miniAst_captures_user_sample_extern_doc() = runTest {
|
fun miniAst_captures_user_sample_extern_doc() = runTest {
|
||||||
val code = """
|
val code = """
|
||||||
@ -311,4 +395,48 @@ class MiniAstTest {
|
|||||||
assertEquals("O3", resolved.first)
|
assertEquals("O3", resolved.first)
|
||||||
assertEquals("doc for name", resolved.second.doc?.summary)
|
assertEquals("doc for name", resolved.second.doc?.summary)
|
||||||
}
|
}
|
||||||
|
@Test
|
||||||
|
fun miniAst_captures_nested_generics() = runTest {
|
||||||
|
val code = """
|
||||||
|
val x: Map<String, List<Int>> = {}
|
||||||
|
"""
|
||||||
|
val (_, sink) = compileWithMini(code)
|
||||||
|
val mini = sink.build()
|
||||||
|
assertNotNull(mini)
|
||||||
|
val vd = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
|
||||||
|
assertNotNull(vd)
|
||||||
|
val ty = vd.type as MiniGenericType
|
||||||
|
assertEquals("Map", (ty.base as MiniTypeName).segments.last().name)
|
||||||
|
assertEquals(2, ty.args.size)
|
||||||
|
|
||||||
|
val arg1 = ty.args[0] as MiniTypeName
|
||||||
|
assertEquals("String", arg1.segments.last().name)
|
||||||
|
|
||||||
|
val arg2 = ty.args[1] as MiniGenericType
|
||||||
|
assertEquals("List", (arg2.base as MiniTypeName).segments.last().name)
|
||||||
|
assertEquals(1, arg2.args.size)
|
||||||
|
val innerArg = arg2.args[0] as MiniTypeName
|
||||||
|
assertEquals("Int", innerArg.segments.last().name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun inferTypeForValWithInference() = runTest {
|
||||||
|
val code = """
|
||||||
|
extern fun test(): List<Int>
|
||||||
|
val x = test()
|
||||||
|
""".trimIndent()
|
||||||
|
val (_, sink) = compileWithMini(code)
|
||||||
|
val mini = sink.build()
|
||||||
|
assertNotNull(mini)
|
||||||
|
|
||||||
|
val vd = mini.declarations.filterIsInstance<MiniValDecl>().firstOrNull { it.name == "x" }
|
||||||
|
assertNotNull(vd)
|
||||||
|
|
||||||
|
val imported = listOf("lyng.stdlib")
|
||||||
|
val src = mini.range.start.source
|
||||||
|
val type = DocLookupUtils.findTypeByRange(mini, "x", src.offsetOf(vd.nameStart), code, imported)
|
||||||
|
assertNotNull(type)
|
||||||
|
val className = DocLookupUtils.simpleClassNameOf(type)
|
||||||
|
assertEquals("List", className)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -174,4 +174,308 @@ class CompletionEngineLightTest {
|
|||||||
val ns = names(items)
|
val ns = names(items)
|
||||||
assertTrue(ns.contains("myField"), "Class field 'myField' should be proposed, but got: $ns")
|
assertTrue(ns.contains("myField"), "Class field 'myField' should be proposed, but got: $ns")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun inferredTypeFromFunctionCall() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(a: Int): List<Int>
|
||||||
|
val x = test(1)
|
||||||
|
val y = x.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for inferred List type, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun inferredTypeFromMemberCall() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern class MyClass {
|
||||||
|
fun getList(): List<String>
|
||||||
|
}
|
||||||
|
extern val c: MyClass
|
||||||
|
val x = c.getList()
|
||||||
|
val y = x.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for inferred List type from member call, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun inferredTypeFromListLiteral() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
val x = [1, 2, 3]
|
||||||
|
val y = x.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for inferred List type from literal, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun inferredTypeAfterIndexing() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(): List<String>
|
||||||
|
val x = test()
|
||||||
|
val y = x[0].<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
// Should contain String members, e.g., 'length' or 're'
|
||||||
|
assertTrue(ns.contains("length"), "String member 'length' should be suggested after indexing List<String>, but got: $ns")
|
||||||
|
assertTrue(ns.contains("re"), "String member 're' should be suggested after indexing List<String>, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun inferredTypeFromAssignmentWithoutVal() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(): List<String>
|
||||||
|
x = test()
|
||||||
|
x.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for variable assigned without 'val', but got: $ns")
|
||||||
|
assertTrue(ns.contains("add"), "List member 'add' should be suggested for variable assigned without 'val', but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun inferredTypeAfterIndexingWithoutVal() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(): List<String>
|
||||||
|
x = test()
|
||||||
|
x[0].<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
// String members include 'trim', 'lower', etc.
|
||||||
|
assertTrue(ns.contains("trim"), "String member 'trim' should be suggested for x[0] where x assigned without val, but got: $ns")
|
||||||
|
assertFalse(ns.contains("add"), "List member 'add' should NOT be suggested for x[0], but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun transitiveInferenceWithoutVal() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(): List<String>
|
||||||
|
x = test()
|
||||||
|
y = x
|
||||||
|
y.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for transitive inference, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun objectMemberReturnInference() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
object O {
|
||||||
|
fun getList(): List<String> = []
|
||||||
|
}
|
||||||
|
O.getList().<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for object member call, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun directFunctionCallCompletion() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(value: Int): List<String>
|
||||||
|
test(1).<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for direct function call, but got: $ns")
|
||||||
|
assertTrue(ns.contains("map"), "Inherited member 'map' should be suggested for List, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun completionWithTrailingDotError() = runBlocking {
|
||||||
|
// This simulates typing mid-expression where the script is technically invalid
|
||||||
|
val code = """
|
||||||
|
extern fun test(): List<String>
|
||||||
|
test().<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested even if script ends with a dot, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun listLiteralCompletion() = runBlocking {
|
||||||
|
val code = "[].<caret>"
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for [], but got: $ns")
|
||||||
|
assertTrue(ns.contains("map"), "Inherited member 'map' should be suggested for [], but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun userReportedSample() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(value: Int): List<String>
|
||||||
|
x = test(1)
|
||||||
|
x.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for x, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun userReportedSampleIndexed() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(value: Int): List<String>
|
||||||
|
x = test(1)
|
||||||
|
x[0].<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "String member 'size' should be suggested for x[0], but got: $ns")
|
||||||
|
assertTrue(ns.contains("trim"), "String member 'trim' should be suggested for x[0], but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun userReportedSampleImplicitVariable() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(): List<String>
|
||||||
|
x = test()
|
||||||
|
x.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for implicit variable x, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun userReportedSampleNoDot() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(value: Int): List<String>
|
||||||
|
x = test(1)
|
||||||
|
x[0]<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("x"), "Implicit variable 'x' should be suggested as global, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun userReportedIssue_X_equals_test2() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test2(): List<String>
|
||||||
|
x = test2
|
||||||
|
x.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
// Since test2 is a function, x = test2 (without parens) should probably be the function itself,
|
||||||
|
// but current DocLookupUtils returns returnType.
|
||||||
|
// If it returns List<String>, then size should be there.
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for x = test2, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun anyMembersOnInferred() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
x = 42
|
||||||
|
x.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("toString"), "Any member 'toString' should be suggested for x=42, but got: $ns")
|
||||||
|
assertTrue(ns.contains("let"), "Any member 'let' should be suggested for x=42, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun charMembersOnIndexedString() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
x = "hello"
|
||||||
|
x[0].<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("code"), "Char member 'code' should be suggested for indexed string x[0], but got: $ns")
|
||||||
|
assertTrue(ns.contains("toString"), "Any member 'toString' should be suggested for x[0], but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun extensionMemberOnInferredList() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun getNames(): List<String>
|
||||||
|
ns = getNames()
|
||||||
|
ns.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("map"), "Extension member 'map' should be suggested for List, but got: $ns")
|
||||||
|
assertTrue(ns.contains("filter"), "Extension member 'filter' should be suggested for List, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun inferredTypeFromExternFunWithVal() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(a: Int): List<Int>
|
||||||
|
val x = test(1)
|
||||||
|
x.<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for val x = test(1), but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun userReportedNestedSample() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(value: Int): List<String>
|
||||||
|
class X(fld1, fld2) {
|
||||||
|
var prop
|
||||||
|
get() { 12 }
|
||||||
|
set(value) {
|
||||||
|
val x = test(2)
|
||||||
|
x.<caret>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "List member 'size' should be suggested for local val x inside set(), but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun userReportedNestedSampleIndexed() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
extern fun test(value: Int): List<String>
|
||||||
|
class X(fld1, fld2) {
|
||||||
|
var prop
|
||||||
|
get() { 12 }
|
||||||
|
set(value) {
|
||||||
|
val x = test(2)
|
||||||
|
x[0].<caret>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("size"), "String member 'size' should be suggested for local x[0] inside set(), but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nestedShadowingCompletion() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
val x = 42
|
||||||
|
class X {
|
||||||
|
fun test() {
|
||||||
|
val x = "hello"
|
||||||
|
x.<caret>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
// Should contain String members (like trim)
|
||||||
|
assertTrue(ns.contains("trim"), "String member 'trim' should be suggested for shadowed x, but got: $ns")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user