Refactored ObjRef evaluation logic for enhanced performance and consistency across unary, binary, and field operations. Added evalValue overrides for more streamlined value resolution.

This commit is contained in:
Sergey Chernov 2026-01-11 01:42:47 +01:00
parent 827df9c8cd
commit 6b957ae6a3
28 changed files with 1339 additions and 506 deletions

View File

@ -17,7 +17,7 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
id("org.jetbrains.intellij") version "1.17.3" id("org.jetbrains.intellij") version "1.17.4"
} }
group = "net.sergeych.lyng" group = "net.sergeych.lyng"
@ -52,7 +52,7 @@ dependencies {
intellij { intellij {
type.set("IC") type.set("IC")
// Build against a modern baseline. Install range is controlled by since/until below. // Build against a modern baseline. Install range is controlled by since/until below.
version.set("2024.3.1") version.set("2024.1.6")
// We manage <idea-version> ourselves in plugin.xml to keep it open-ended (no upper cap) // We manage <idea-version> ourselves in plugin.xml to keep it open-ended (no upper cap)
updateSinceUntilBuild.set(false) updateSinceUntilBuild.set(false)
// Include only available bundled plugins for this IDE build // Include only available bundled plugins for this IDE build

View File

@ -167,7 +167,11 @@ class LyngCompletionContributor : CompletionContributor() {
.withIcon(AllIcons.Nodes.Field) .withIcon(AllIcons.Nodes.Field)
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b } .let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
} }
emit(builder) if (ci.priority != 0.0) {
emit(PrioritizedLookupElement.withPriority(builder, ci.priority))
} else {
emit(builder)
}
} }
// In member context, ensure stdlib extension-like methods (e.g., String.re) are present // In member context, ensure stdlib extension-like methods (e.g., String.re) are present
if (memberDotPos != null) { if (memberDotPos != null) {
@ -401,7 +405,7 @@ class LyngCompletionContributor : CompletionContributor() {
} }
supplementPreferredBases(className) supplementPreferredBases(className)
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>) { fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>, groupPriority: Double) {
val keys = map.keys.sortedBy { it.lowercase() } val keys = map.keys.sortedBy { it.lowercase() }
for (name in keys) { for (name in keys) {
val list = map[name] ?: continue val list = map[name] ?: continue
@ -428,7 +432,11 @@ class LyngCompletionContributor : CompletionContributor() {
.withTailText(tail, true) .withTailText(tail, true)
.withTypeText(ret, true) .withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
emit(builder) if (groupPriority != 0.0) {
emit(PrioritizedLookupElement.withPriority(builder, groupPriority))
} else {
emit(builder)
}
} }
is MiniMemberValDecl -> { is MiniMemberValDecl -> {
val icon = if (rep.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field val icon = if (rep.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field
@ -439,7 +447,11 @@ class LyngCompletionContributor : CompletionContributor() {
val builder = LookupElementBuilder.create(name) val builder = LookupElementBuilder.create(name)
.withIcon(icon) .withIcon(icon)
.withTypeText(typeOf(chosen.type), true) .withTypeText(typeOf(chosen.type), true)
emit(builder) if (groupPriority != 0.0) {
emit(PrioritizedLookupElement.withPriority(builder, groupPriority))
} else {
emit(builder)
}
} }
is MiniInitDecl -> {} is MiniInitDecl -> {}
} }
@ -447,8 +459,8 @@ class LyngCompletionContributor : CompletionContributor() {
} }
// Emit what we have first // Emit what we have first
emitGroup(directMap) emitGroup(directMap, 100.0)
emitGroup(inheritedMap) emitGroup(inheritedMap, 0.0)
// If suggestions are suspiciously sparse for known container classes, // If suggestions are suspiciously sparse for known container classes,
// try to conservatively supplement using a curated list resolved via docs registry. // try to conservatively supplement using a curated list resolved via docs registry.
@ -509,7 +521,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
} }
} }
emit(builder) emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name) 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
@ -523,7 +535,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withTailText("()", true) .withTailText("()", true)
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
} }
emit(builder) emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name) already.add(name)
} }
} }
@ -576,7 +588,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
} }
} }
emit(builder) emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name) already.add(name)
continue continue
} }
@ -585,7 +597,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withIcon(AllIcons.Nodes.Method) .withIcon(AllIcons.Nodes.Method)
.withTailText("()", true) .withTailText("()", true)
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
emit(builder) emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name) already.add(name)
} }
} }

View File

@ -461,25 +461,39 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
private fun ensureExternalDocsRegistered() { @Suppress("UNUSED_EXPRESSION") externalDocsLoaded } private fun ensureExternalDocsRegistered() { @Suppress("UNUSED_EXPRESSION") externalDocsLoaded }
private fun tryLoadExternalDocs(): Boolean { private fun tryLoadExternalDocs(): Boolean {
return try { var anyLoaded = false
try {
// Try known registrars; ignore failures if module is absent // Try known registrars; ignore failures if module is absent
val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs") val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs")
val m = cls.getMethod("ensure") val m = cls.getMethod("ensure")
m.invoke(null) m.invoke(null)
log.info("[LYNG_DEBUG] QuickDoc: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK") log.info("[LYNG_DEBUG] QuickDoc: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK")
true anyLoaded = true
} catch (_: Throwable) { } catch (_: Throwable) {}
try {
val cls = Class.forName("net.sergeych.lyngio.docs.ProcessBuiltinDocs")
val m = cls.getMethod("ensure")
m.invoke(null)
log.info("[LYNG_DEBUG] QuickDoc: external docs loaded: net.sergeych.lyngio.docs.ProcessBuiltinDocs.ensure() OK")
anyLoaded = true
} catch (_: Throwable) {}
if (!anyLoaded) {
// Seed a minimal plugin-local fallback so Path docs still work without lyngio // Seed a minimal plugin-local fallback so Path docs still work without lyngio
val seeded = try { val seeded = try {
FsDocsFallback.ensureOnce() FsDocsFallback.ensureOnce()
ProcessDocsFallback.ensureOnce()
true
} catch (_: Throwable) { false } } catch (_: Throwable) { false }
if (seeded) { if (seeded) {
log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found; seeded plugin fallback for lyng.io.fs") log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found; seeded plugin fallbacks")
} else { } else {
log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found (lyngio absent on classpath)") log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found (lyngio absent on classpath)")
} }
seeded return seeded
} }
return true
} }
override fun getCustomDocumentationElement( override fun getCustomDocumentationElement(

View File

@ -0,0 +1,69 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/*
* Minimal fallback docs seeding for `lyng.io.process` used only inside the IDEA plugin
* when external docs module (lyngio) is not present on the classpath.
*/
package net.sergeych.lyng.idea.docs
import net.sergeych.lyng.miniast.BuiltinDocRegistry
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.type
internal object ProcessDocsFallback {
@Volatile
private var seeded = false
fun ensureOnce(): Boolean {
if (seeded) return true
synchronized(this) {
if (seeded) return true
BuiltinDocRegistry.module("lyng.io.process") {
classDoc(name = "Process", doc = "Process execution and control.") {
method(
name = "execute",
doc = "Execute a process with arguments.",
params = listOf(ParamDoc("executable", type("lyng.String")), ParamDoc("args", type("lyng.List"))),
returns = type("RunningProcess"),
isStatic = true
)
method(
name = "shell",
doc = "Execute a command via system shell.",
params = listOf(ParamDoc("command", type("lyng.String"))),
returns = type("RunningProcess"),
isStatic = true
)
}
classDoc(name = "RunningProcess", doc = "Handle to a running process.") {
method(name = "stdout", doc = "Get standard output stream as a Flow of lines.", returns = type("lyng.Flow"))
method(name = "stderr", doc = "Get standard error stream as a Flow of lines.", returns = type("lyng.Flow"))
method(name = "waitFor", doc = "Wait for the process to exit.", returns = type("lyng.Int"))
method(name = "signal", doc = "Send a signal to the process.", params = listOf(ParamDoc("signal", type("lyng.String"))))
method(name = "destroy", doc = "Forcefully terminate the process.")
}
valDoc(name = "Process", doc = "Process execution and control.", type = type("Process"))
valDoc(name = "RunningProcess", doc = "Handle to a running process.", type = type("RunningProcess"))
}
seeded = true
return true
}
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -54,6 +54,8 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
} catch (_: Throwable) { false } } catch (_: Throwable) { false }
override fun getTokenizer(element: PsiElement): Tokenizer<*> { override fun getTokenizer(element: PsiElement): Tokenizer<*> {
if (element is com.intellij.psi.PsiFile) return EMPTY_TOKENIZER
val hasGrazie = grazieInstalled() val hasGrazie = grazieInstalled()
val hasGrazieApi = grazieApiAvailable() val hasGrazieApi = grazieApiAvailable()
val settings = LyngFormatterSettings.getInstance(element.project) val settings = LyngFormatterSettings.getInstance(element.project)
@ -63,27 +65,40 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
} }
val file = element.containingFile ?: return EMPTY_TOKENIZER val file = element.containingFile ?: return EMPTY_TOKENIZER
val index = LyngSpellIndex.getUpToDate(file) ?: run { val et = element.node?.elementType
// Suspend legacy spellcheck until MiniAst-based index is ready val index = LyngSpellIndex.getUpToDate(file)
return EMPTY_TOKENIZER
}
val elRange = element.textRange ?: return EMPTY_TOKENIZER
fun overlaps(list: List<TextRange>) = list.any { it.intersects(elRange) }
// Decide responsibility per settings // Decide responsibility per settings
// If Grazie is present but its public API is not available (IC-243), do NOT delegate to it. // If Grazie is present but its public API is not available (IC-243), do NOT delegate to it.
val preferGrazie = hasGrazie && hasGrazieApi && settings.preferGrazieForCommentsAndLiterals val preferGrazie = hasGrazie && hasGrazieApi && settings.preferGrazieForCommentsAndLiterals
val grazieIds = hasGrazie && hasGrazieApi && settings.grazieChecksIdentifiers val grazieIds = hasGrazie && hasGrazieApi && settings.grazieChecksIdentifiers
if (index == null) {
// Index not ready: fall back to Lexer-based token types.
// Identifiers are safe because LyngLexer separates keywords from identifiers.
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.IDENTIFIER) {
return if (grazieIds) EMPTY_TOKENIZER else IDENTIFIER_TOKENIZER
}
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.LINE_COMMENT || et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.BLOCK_COMMENT) {
return if (preferGrazie) EMPTY_TOKENIZER else COMMENT_TEXT_TOKENIZER
}
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.STRING && settings.spellCheckStringLiterals) {
return if (preferGrazie) EMPTY_TOKENIZER else STRING_WITH_PRINTF_EXCLUDES
}
return EMPTY_TOKENIZER
}
val elRange = element.textRange ?: return EMPTY_TOKENIZER
fun overlaps(list: List<TextRange>) = list.any { it.intersects(elRange) }
// Identifiers: only if range is within identifiers index and not delegated to Grazie // Identifiers: only if range is within identifiers index and not delegated to Grazie
if (overlaps(index.identifiers) && !grazieIds) return IDENTIFIER_TOKENIZER if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.IDENTIFIER && overlaps(index.identifiers) && !grazieIds) return IDENTIFIER_TOKENIZER
// Comments: only if not delegated to Grazie and overlapping indexed comments // Comments: only if not delegated to Grazie and overlapping indexed comments
if (!preferGrazie && overlaps(index.comments)) return COMMENT_TEXT_TOKENIZER if ((et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.LINE_COMMENT || et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.BLOCK_COMMENT) && overlaps(index.comments) && !preferGrazie) return COMMENT_TEXT_TOKENIZER
// Strings: only if not delegated to Grazie, literals checking enabled, and overlapping indexed strings // Strings: only if not delegated to Grazie, literals checking enabled, and overlapping indexed strings
if (!preferGrazie && settings.spellCheckStringLiterals && overlaps(index.strings)) return STRING_WITH_PRINTF_EXCLUDES if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.STRING && settings.spellCheckStringLiterals && overlaps(index.strings) && !preferGrazie) return STRING_WITH_PRINTF_EXCLUDES
return EMPTY_TOKENIZER return EMPTY_TOKENIZER
} }
@ -93,7 +108,7 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
} }
private object IDENTIFIER_TOKENIZER : Tokenizer<PsiElement>() { private object IDENTIFIER_TOKENIZER : Tokenizer<PsiElement>() {
private val splitter = PlainTextSplitter.getInstance() private val splitter = com.intellij.spellchecker.inspections.IdentifierSplitter.getInstance()
override fun tokenize(element: PsiElement, consumer: TokenConsumer) { override fun tokenize(element: PsiElement, consumer: TokenConsumer) {
val text = element.text val text = element.text
if (text.isNullOrEmpty()) return if (text.isNullOrEmpty()) return

View File

@ -1,3 +1,20 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/* /*
* Ensure external/bundled docs are registered in BuiltinDocRegistry * Ensure external/bundled docs are registered in BuiltinDocRegistry
* so completion/quickdoc can resolve things like lyng.io.fs.Path. * so completion/quickdoc can resolve things like lyng.io.fs.Path.
@ -6,6 +23,7 @@ package net.sergeych.lyng.idea.util
import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.Logger
import net.sergeych.lyng.idea.docs.FsDocsFallback import net.sergeych.lyng.idea.docs.FsDocsFallback
import net.sergeych.lyng.idea.docs.ProcessDocsFallback
object DocsBootstrap { object DocsBootstrap {
private val log = Logger.getInstance(DocsBootstrap::class.java) private val log = Logger.getInstance(DocsBootstrap::class.java)
@ -20,20 +38,32 @@ object DocsBootstrap {
} }
} }
private fun tryLoadExternal(): Boolean = try { private fun tryLoadExternal(): Boolean {
val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs") var anyLoaded = false
val m = cls.getMethod("ensure") try {
m.invoke(null) val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs")
log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK") val m = cls.getMethod("ensure")
true m.invoke(null)
} catch (_: Throwable) { log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK")
false anyLoaded = true
} catch (_: Throwable) {}
try {
val cls = Class.forName("net.sergeych.lyngio.docs.ProcessBuiltinDocs")
val m = cls.getMethod("ensure")
m.invoke(null)
log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.ProcessBuiltinDocs.ensure() OK")
anyLoaded = true
} catch (_: Throwable) {}
return anyLoaded
} }
private fun trySeedFallback(): Boolean = try { private fun trySeedFallback(): Boolean = try {
val seeded = FsDocsFallback.ensureOnce() val seededFs = FsDocsFallback.ensureOnce()
val seededProcess = ProcessDocsFallback.ensureOnce()
val seeded = seededFs || seededProcess
if (seeded) { if (seeded) {
log.info("[LYNG_DEBUG] DocsBootstrap: external docs not found; seeded plugin fallback for lyng.io.fs") log.info("[LYNG_DEBUG] DocsBootstrap: external docs not found; seeded plugin fallback for lyng.io.fs/process")
} else { } else {
log.info("[LYNG_DEBUG] DocsBootstrap: external docs not found; no fallback seeded") log.info("[LYNG_DEBUG] DocsBootstrap: external docs not found; no fallback seeded")
} }

View File

@ -16,8 +16,8 @@
--> -->
<idea-plugin> <idea-plugin>
<!-- Open-ended compatibility: 2024.3+ (build 243 and newer) --> <!-- Open-ended compatibility: 2024.1+ (build 241 and newer) -->
<idea-version since-build="243"/> <idea-version since-build="241"/>
<id>net.sergeych.lyng.idea</id> <id>net.sergeych.lyng.idea</id>
<name>Lyng</name> <name>Lyng</name>
<vendor email="real.sergeych@gmail.com">Sergey Chernov</vendor> <vendor email="real.sergeych@gmail.com">Sergey Chernov</vendor>

View File

@ -1,3 +1,20 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.idea.completion package net.sergeych.lyng.idea.completion
import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.testFramework.fixtures.BasePlatformTestCase
@ -118,4 +135,41 @@ class LyngCompletionMemberTest : BasePlatformTestCase() {
// Heuristic: we expect more than a couple of items (not just size/toList) // Heuristic: we expect more than a couple of items (not just size/toList)
assertTrue("Too few member suggestions after list literal: $items", items.size >= 3) assertTrue("Too few member suggestions after list literal: $items", items.size >= 3)
} }
fun test_ProcessModule_Completion() {
val code = """
import lyng.io.process
Process.<caret>
""".trimIndent()
val imported = listOf("lyng.io.process")
ensureDocs(imported)
val items = complete(code)
assertTrue("Should contain 'execute'", items.contains("execute"))
assertTrue("Should contain 'shell'", items.contains("shell"))
}
fun test_RunningProcess_Completion() {
val code = """
import lyng.io.process
val p = Process.shell("ls")
p.<caret>
""".trimIndent()
val imported = listOf("lyng.io.process")
ensureDocs(imported)
val items = complete(code)
assertTrue("Should contain 'stdout'", items.contains("stdout"))
assertTrue("Should contain 'waitFor'", items.contains("waitFor"))
assertTrue("Should contain 'signal'", items.contains("signal"))
}
fun test_RegistryDirect() {
DocsBootstrap.ensure()
val docs = BuiltinDocRegistry.docsForModule("lyng.io.process")
assertTrue("Docs for lyng.io.process should not be empty", docs.isNotEmpty())
val processClass = docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "Process" }
assertNotNull("Should contain Process class", processClass)
assertTrue("Process should have members", processClass!!.members.isNotEmpty())
}
} }

View File

@ -32,6 +32,7 @@ group = "net.sergeych"
version = "0.0.1-SNAPSHOT" version = "0.0.1-SNAPSHOT"
kotlin { kotlin {
jvmToolchain(17)
jvm() jvm()
androidTarget { androidTarget {
publishLibraryVariants("release") publishLibraryVariants("release")

View File

@ -39,43 +39,174 @@ object FsBuiltinDocs {
name = "Path", name = "Path",
doc = "Filesystem path class. Construct with a string: `Path(\"/tmp\")`." doc = "Filesystem path class. Construct with a string: `Path(\"/tmp\")`."
) { ) {
// Common instance methods (subset sufficient for Quick Docs) method(
name = "name",
doc = "Base name of the path (last segment).",
returns = type("lyng.String")
)
method(
name = "parent",
doc = "Parent directory as a Path or null if none.",
returns = type("Path", nullable = true)
)
method(
name = "segments",
doc = "List of path segments.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String")))
)
method( method(
name = "exists", name = "exists",
doc = "Whether the path exists on the filesystem.", doc = "Check whether this path exists on the filesystem.",
returns = type("lyng.Bool") returns = type("lyng.Bool")
) )
method( method(
name = "isFile", name = "isFile",
doc = "Whether the path exists and is a file.", doc = "True if this path is a regular file (based on cached metadata).",
returns = type("lyng.Bool") returns = type("lyng.Bool")
) )
method( method(
name = "isDir", name = "isDirectory",
doc = "Whether the path exists and is a directory.", doc = "True if this path is a directory (based on cached metadata).",
returns = type("lyng.Bool") returns = type("lyng.Bool")
) )
method(
name = "size",
doc = "File size in bytes, or null when unavailable.",
returns = type("lyng.Int", nullable = true)
)
method(
name = "createdAt",
doc = "Creation time as `Instant`, or null when unavailable.",
returns = type("lyng.Instant", nullable = true)
)
method(
name = "createdAtMillis",
doc = "Creation time in milliseconds since epoch, or null when unavailable.",
returns = type("lyng.Int", nullable = true)
)
method(
name = "modifiedAt",
doc = "Last modification time as `Instant`, or null when unavailable.",
returns = type("lyng.Instant", nullable = true)
)
method(
name = "modifiedAtMillis",
doc = "Last modification time in milliseconds since epoch, or null when unavailable.",
returns = type("lyng.Int", nullable = true)
)
method(
name = "list",
doc = "List directory entries as `Path` objects.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("Path")))
)
method(
name = "readBytes",
doc = "Read the entire file into a binary buffer.",
returns = type("lyng.Buffer")
)
method(
name = "writeBytes",
doc = "Write a binary buffer to the file, replacing content.",
params = listOf(ParamDoc("bytes", type("lyng.Buffer")))
)
method(
name = "appendBytes",
doc = "Append a binary buffer to the end of the file.",
params = listOf(ParamDoc("bytes", type("lyng.Buffer")))
)
method( method(
name = "readUtf8", name = "readUtf8",
doc = "Read the entire file as UTF-8 string.", doc = "Read the entire file as a UTF-8 string.",
returns = type("lyng.String") returns = type("lyng.String")
) )
method( method(
name = "writeUtf8", name = "writeUtf8",
doc = "Write UTF-8 string to the file (overwrite).", doc = "Write a UTF-8 string to the file, replacing content.",
params = listOf(ParamDoc("text", type("lyng.String"))) params = listOf(ParamDoc("text", type("lyng.String")))
) )
method( method(
name = "bytes", name = "appendUtf8",
doc = "Iterate file content as `Buffer` chunks.", doc = "Append UTF-8 text to the end of the file.",
params = listOf(ParamDoc("text", type("lyng.String")))
)
method(
name = "metadata",
doc = "Fetch cached metadata as a map of fields: `isFile`, `isDirectory`, `size`, `createdAtMillis`, `modifiedAtMillis`, `isSymlink`.",
returns = TypeGenericDoc(type("lyng.Map"), listOf(type("lyng.String"), type("lyng.Any")))
)
method(
name = "mkdirs",
doc = "Create directories (like `mkdir -p`). If `mustCreate` is true and the path already exists, the call fails.",
params = listOf(ParamDoc("mustCreate", type("lyng.Bool")))
)
method(
name = "move",
doc = "Move this path to a new location. `to` may be a `Path` or `String`.",
params = listOf(ParamDoc("to"), ParamDoc("overwrite", type("lyng.Bool")))
)
method(
name = "delete",
doc = "Delete this path. `recursively=true` removes directories with their contents.",
params = listOf(ParamDoc("mustExist", type("lyng.Bool")), ParamDoc("recursively", type("lyng.Bool")))
)
method(
name = "copy",
doc = "Copy this path to a new location. `to` may be a `Path` or `String`.",
params = listOf(ParamDoc("to"), ParamDoc("overwrite", type("lyng.Bool")))
)
method(
name = "glob",
doc = "List entries matching a glob pattern (no recursion).",
params = listOf(ParamDoc("pattern", type("lyng.String"))),
returns = TypeGenericDoc(type("lyng.List"), listOf(type("Path")))
)
method(
name = "readChunks",
doc = "Read file in fixed-size chunks as an iterator of `Buffer`.",
params = listOf(ParamDoc("size", type("lyng.Int"))), params = listOf(ParamDoc("size", type("lyng.Int"))),
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Buffer"))) returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Buffer")))
) )
method( method(
name = "lines", name = "readUtf8Chunks",
doc = "Iterate file as lines of text.", doc = "Read UTF-8 text in fixed-size chunks as an iterator of `String`.",
params = listOf(ParamDoc("size", type("lyng.Int"))),
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String"))) returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String")))
) )
method(
name = "lines",
doc = "Iterate lines of the file as `String` values.",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String")))
)
}
classDoc(
name = "BytesIterator",
doc = "Iterator over binary chunks."
) {
method("iterator", "Return this iterator instance.", returns = type("BytesIterator"))
method("hasNext", "Whether there is another chunk available.", returns = type("lyng.Bool"))
method("next", "Return the next chunk as a `Buffer`.", returns = type("lyng.Buffer"))
method("cancelIteration", "Stop the iteration early.")
}
classDoc(
name = "StringChunksIterator",
doc = "Iterator over UTF-8 text chunks."
) {
method("iterator", "Return this iterator instance.", returns = type("StringChunksIterator"))
method("hasNext", "Whether there is another chunk available.", returns = type("lyng.Bool"))
method("next", "Return the next UTF-8 chunk as a `String`.", returns = type("lyng.String"))
method("cancelIteration", "Stop the iteration early.")
}
classDoc(
name = "LinesIterator",
doc = "Iterator that yields lines of text."
) {
method("iterator", "Return this iterator instance.", returns = type("LinesIterator"))
method("hasNext", "Whether another line is available.", returns = type("lyng.Bool"))
method("next", "Return the next line as `String`.", returns = type("lyng.String"))
method("cancelIteration", "Stop the iteration early.")
} }
// Top-level exported constants // Top-level exported constants

View File

@ -0,0 +1,117 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyngio.docs
import net.sergeych.lyng.miniast.BuiltinDocRegistry
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.type
object ProcessBuiltinDocs {
private var registered = false
fun ensure() {
if (registered) return
BuiltinDocRegistry.module("lyng.io.process") {
classDoc(
name = "Process",
doc = "Process execution and control."
) {
method(
name = "execute",
doc = "Execute a process with arguments.",
params = listOf(ParamDoc("executable", type("lyng.String")), ParamDoc("args", type("lyng.List"))),
returns = type("RunningProcess"),
isStatic = true
)
method(
name = "shell",
doc = "Execute a command via system shell.",
params = listOf(ParamDoc("command", type("lyng.String"))),
returns = type("RunningProcess"),
isStatic = true
)
}
classDoc(
name = "Platform",
doc = "Platform information."
) {
method(
name = "details",
doc = "Get platform core details.",
returns = type("lyng.Map"),
isStatic = true
)
method(
name = "isSupported",
doc = "Check if processes are supported on this platform.",
returns = type("lyng.Bool"),
isStatic = true
)
}
classDoc(
name = "RunningProcess",
doc = "Handle to a running process."
) {
method(
name = "stdout",
doc = "Get standard output stream as a Flow of lines.",
returns = type("lyng.Flow")
)
method(
name = "stderr",
doc = "Get standard error stream as a Flow of lines.",
returns = type("lyng.Flow")
)
method(
name = "signal",
doc = "Send a signal to the process (e.g. 'SIGINT', 'SIGTERM', 'SIGKILL').",
params = listOf(ParamDoc("signal", type("lyng.String")))
)
method(
name = "waitFor",
doc = "Wait for the process to exit and return its exit code.",
returns = type("lyng.Int")
)
method(
name = "destroy",
doc = "Forcefully terminate the process."
)
}
// Top-level exported constants
valDoc(
name = "Process",
doc = "Process execution and control.",
type = type("Process")
)
valDoc(
name = "Platform",
doc = "Platform information.",
type = type("Platform")
)
valDoc(
name = "RunningProcess",
doc = "Handle to a running process.",
type = type("RunningProcess")
)
}
registered = true
}
}

View File

@ -46,6 +46,7 @@ buildkonfig {
} }
kotlin { kotlin {
jvmToolchain(17)
jvm() jvm()
androidTarget { androidTarget {
publishLibraryVariants("release") publishLibraryVariants("release")

View File

@ -169,9 +169,9 @@ class Compiler(
miniSink?.onScriptStart(start) miniSink?.onScriptStart(start)
do { do {
val t = cc.current() val t = cc.current()
if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINLGE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) {
when (t.type) { when (t.type) {
Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> pushPendingDocToken(t) Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> pushPendingDocToken(t)
Token.Type.NEWLINE -> { Token.Type.NEWLINE -> {
// A standalone newline not immediately following a comment resets doc buffer // A standalone newline not immediately following a comment resets doc buffer
if (!prevWasComment) clearPendingDoc() else prevWasComment = false if (!prevWasComment) clearPendingDoc() else prevWasComment = false
@ -316,7 +316,7 @@ class Compiler(
} }
Token.Type.LABEL -> continue Token.Type.LABEL -> continue
Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue
Token.Type.NEWLINE -> continue Token.Type.NEWLINE -> continue
@ -407,7 +407,7 @@ class Compiler(
val t = cc.next() val t = cc.next()
val startPos = t.pos val startPos = t.pos
when (t.type) { when (t.type) {
// Token.Type.NEWLINE, Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT-> { // Token.Type.NEWLINE, Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT-> {
// continue // continue
// } // }
@ -606,7 +606,7 @@ class Compiler(
// to skip in parseExpression: // to skip in parseExpression:
val current = cc.current() val current = cc.current()
val right = val right =
if (current.type == Token.Type.NEWLINE || current.type == Token.Type.SINLGE_LINE_COMMENT) if (current.type == Token.Type.NEWLINE || current.type == Token.Type.SINGLE_LINE_COMMENT)
null null
else else
parseExpression() parseExpression()
@ -887,7 +887,7 @@ class Compiler(
} }
Token.Type.NEWLINE -> {} Token.Type.NEWLINE -> {}
Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT -> {} Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT -> {}
Token.Type.ID -> { Token.Type.ID -> {
// visibility // visibility
@ -2734,7 +2734,8 @@ class Compiler(
doc = declDocLocal, doc = declDocLocal,
nameStart = nameStartPos, nameStart = nameStartPos,
receiver = receiverMini, receiver = receiverMini,
isExtern = actualExtern isExtern = actualExtern,
isStatic = isStatic
) )
miniSink?.onFunDecl(node) miniSink?.onFunDecl(node)
pendingDeclDoc = null pendingDeclDoc = null
@ -2941,10 +2942,11 @@ class Compiler(
doc = declDocLocal, doc = declDocLocal,
nameStart = nameStartPos, nameStart = nameStartPos,
receiver = receiverMini, receiver = receiverMini,
isExtern = actualExtern isExtern = actualExtern,
isStatic = isStatic
) )
miniSink?.onFunDecl(node)
miniSink?.onExitFunction(cc.currentPos()) miniSink?.onExitFunction(cc.currentPos())
miniSink?.onFunDecl(node)
} }
} }
@ -3002,7 +3004,8 @@ class Compiler(
initRange = null, initRange = null,
doc = pendingDeclDoc, doc = pendingDeclDoc,
nameStart = namePos, nameStart = namePos,
isExtern = actualExtern isExtern = actualExtern,
isStatic = false
) )
miniSink?.onValDecl(node) miniSink?.onValDecl(node)
} }
@ -3194,7 +3197,8 @@ class Compiler(
doc = pendingDeclDoc, doc = pendingDeclDoc,
nameStart = nameStartPos, nameStart = nameStartPos,
receiver = receiverMini, receiver = receiverMini,
isExtern = actualExtern isExtern = actualExtern,
isStatic = isStatic
) )
miniSink?.onValDecl(node) miniSink?.onValDecl(node)
pendingDeclDoc = null pendingDeclDoc = null

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -238,7 +238,7 @@ class CompilerContext(val tokens: List<Token>) {
} }
} }
Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT, Token.Type.NEWLINE -> {} Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT, Token.Type.NEWLINE -> {}
else -> { else -> {
restorePos(pos); return false restorePos(pos); return false
} }
@ -265,6 +265,6 @@ class CompilerContext(val tokens: List<Token>) {
} }
companion object { companion object {
val wstokens = setOf(Token.Type.NEWLINE, Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT) val wstokens = setOf(Token.Type.NEWLINE, Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT)
} }
} }

View File

@ -131,7 +131,7 @@ private class Parser(fromPos: Pos) {
pos.advance() pos.advance()
val body = loadToEndOfLine() val body = loadToEndOfLine()
// Include the leading '//' and do not trim; keep exact lexeme (excluding preceding codepoint) // Include the leading '//' and do not trim; keep exact lexeme (excluding preceding codepoint)
Token("//" + body, from, Token.Type.SINLGE_LINE_COMMENT) Token("//" + body, from, Token.Type.SINGLE_LINE_COMMENT)
} }
'*' -> { '*' -> {

View File

@ -22,7 +22,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
throw ScriptError(pos, text) throw ScriptError(pos, text)
} }
val isComment: Boolean by lazy { type == Type.SINLGE_LINE_COMMENT || type == Type.MULTILINE_COMMENT } val isComment: Boolean by lazy { type == Type.SINGLE_LINE_COMMENT || type == Type.MULTILINE_COMMENT }
fun isId(text: String) = fun isId(text: String) =
type == Type.ID && value == text type == Type.ID && value == text
@ -41,7 +41,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
SHUTTLE, SHUTTLE,
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON, AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
SHL, SHR, SHL, SHR,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT, SINGLE_LINE_COMMENT, MULTILINE_COMMENT,
LABEL, ATLABEL, // label@ at@label LABEL, ATLABEL, // label@ at@label
// type-checking/casting // type-checking/casting
AS, ASNULL, OBJECT, AS, ASNULL, OBJECT,

View File

@ -67,7 +67,7 @@ private fun kindOf(type: Type, value: String): HighlightKind? = when (type) {
Type.REGEX -> HighlightKind.Regex Type.REGEX -> HighlightKind.Regex
// comments // comments
Type.SINLGE_LINE_COMMENT, Type.MULTILINE_COMMENT -> HighlightKind.Comment Type.SINGLE_LINE_COMMENT, Type.MULTILINE_COMMENT -> HighlightKind.Comment
// punctuation // punctuation
Type.LPAREN, Type.RPAREN, Type.LBRACE, Type.RBRACE, Type.LBRACKET, Type.RBRACKET, Type.LPAREN, Type.RPAREN, Type.LBRACE, Type.RBRACE, Type.LBRACKET, Type.RBRACKET,

View File

@ -148,6 +148,8 @@ class ModuleDocsBuilder internal constructor(private val moduleName: String) {
body = null, body = null,
doc = md, doc = md,
nameStart = Pos.builtIn, nameStart = Pos.builtIn,
isExtern = false,
isStatic = false
) )
} }
@ -167,6 +169,8 @@ class ModuleDocsBuilder internal constructor(private val moduleName: String) {
initRange = null, initRange = null,
doc = md, doc = md,
nameStart = Pos.builtIn, nameStart = Pos.builtIn,
isExtern = false,
isStatic = false
) )
} }

View File

@ -33,6 +33,7 @@ data class CompletionItem(
val kind: Kind, val kind: Kind,
val tailText: String? = null, val tailText: String? = null,
val typeText: String? = null, val typeText: String? = null,
val priority: Double = 0.0,
) )
enum class Kind { Function, Class_, Enum, Value, Method, Field } enum class Kind { Function, Class_, Enum, Value, Method, Field }
@ -107,7 +108,7 @@ object CompletionEngineLight {
val locals = DocLookupUtils.extractLocalsAt(text, caret) val locals = DocLookupUtils.extractLocalsAt(text, caret)
for (name in locals) { for (name in locals) {
if (name.startsWith(prefix, true)) { if (name.startsWith(prefix, true)) {
out.add(CompletionItem(name, Kind.Value)) out.add(CompletionItem(name, Kind.Value, priority = 150.0))
} }
} }
@ -168,17 +169,17 @@ object CompletionEngineLight {
when (node) { when (node) {
is MiniFunDecl -> { is MiniFunDecl -> {
for (p in node.params) { for (p in node.params) {
add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type))) add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type), priority = 200.0))
} }
} }
is MiniClassDecl -> { is MiniClassDecl -> {
// Propose constructor parameters (ctorFields) // Propose constructor parameters (ctorFields)
for (p in node.ctorFields) { for (p in node.ctorFields) {
add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type))) add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type), priority = 200.0))
} }
// Propose class-level fields // Propose class-level fields
for (p in node.classFields) { for (p in node.classFields) {
add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type))) add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type), priority = 100.0))
} }
// Process members (methods/fields) // Process members (methods/fields)
for (m in node.members) { for (m in node.members) {
@ -189,10 +190,10 @@ object CompletionEngineLight {
when (m) { when (m) {
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {
val params = m.params.joinToString(", ") { it.name } val params = m.params.joinToString(", ") { it.name }
add(CompletionItem(m.name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType))) add(CompletionItem(m.name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType), priority = 100.0))
} }
is MiniMemberValDecl -> { is MiniMemberValDecl -> {
add(CompletionItem(m.name, if (m.mutable) Kind.Value else Kind.Field, typeText = typeOf(m.type))) add(CompletionItem(m.name, if (m.mutable) Kind.Value else Kind.Field, typeText = typeOf(m.type), priority = 100.0))
} }
is MiniInitDecl -> {} is MiniInitDecl -> {}
} }
@ -200,7 +201,7 @@ object CompletionEngineLight {
} }
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {
for (p in node.params) { for (p in node.params) {
add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type))) add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type), priority = 200.0))
} }
} }
else -> {} else -> {}
@ -250,7 +251,7 @@ object CompletionEngineLight {
"Array" -> listOf("Collection", "Iterable").forEach { if (visited.add(it)) addMembersOf(it, false) } "Array" -> listOf("Collection", "Iterable").forEach { if (visited.add(it)) addMembersOf(it, false) }
} }
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>) { fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>, groupPriority: Double) {
for (name in map.keys.sortedBy { it.lowercase() }) { for (name in map.keys.sortedBy { it.lowercase() }) {
val variants = map[name] ?: continue val variants = map[name] ?: continue
// Prefer a method with a known return type; else any method; else first variant // Prefer a method with a known return type; else any method; else first variant
@ -265,7 +266,7 @@ object CompletionEngineLight {
val params = rep.params.joinToString(", ") { it.name } val params = rep.params.joinToString(", ") { it.name }
val extra = variants.count { it is MiniMemberFunDecl } - 1 val extra = variants.count { it is MiniMemberFunDecl } - 1
val ov = if (extra > 0) " (+$extra overloads)" else "" val ov = if (extra > 0) " (+$extra overloads)" else ""
val ci = CompletionItem(name, Kind.Method, tailText = "(${params})$ov", typeText = typeOf(rep.returnType)) val ci = CompletionItem(name, Kind.Method, tailText = "(${params})$ov", typeText = typeOf(rep.returnType), priority = groupPriority)
if (ci.name.startsWith(prefix, true)) out += ci if (ci.name.startsWith(prefix, true)) out += ci
} }
is MiniMemberValDecl -> { is MiniMemberValDecl -> {
@ -273,7 +274,7 @@ object CompletionEngineLight {
val chosen = variants.asSequence() val chosen = variants.asSequence()
.filterIsInstance<MiniMemberValDecl>() .filterIsInstance<MiniMemberValDecl>()
.firstOrNull { it.type != null } ?: rep .firstOrNull { it.type != null } ?: rep
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(chosen.type)) val ci = CompletionItem(name, Kind.Field, typeText = typeOf(chosen.type), priority = groupPriority)
if (ci.name.startsWith(prefix, true)) out += ci if (ci.name.startsWith(prefix, true)) out += ci
} }
is MiniInitDecl -> {} is MiniInitDecl -> {}
@ -281,8 +282,8 @@ object CompletionEngineLight {
} }
} }
emitGroup(directMap) emitGroup(directMap, 100.0)
emitGroup(inheritedMap) emitGroup(inheritedMap, 0.0)
// Supplement with extension members (both stdlib and local) // Supplement with extension members (both stdlib and local)
run { run {
@ -296,22 +297,22 @@ object CompletionEngineLight {
val ci = when (m) { val ci = when (m) {
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {
val params = m.params.joinToString(", ") { it.name } val params = m.params.joinToString(", ") { it.name }
CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType)) CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType), priority = 50.0)
} }
is MiniFunDecl -> { is MiniFunDecl -> {
val params = m.params.joinToString(", ") { it.name } val params = m.params.joinToString(", ") { it.name }
CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType)) CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType), priority = 50.0)
} }
is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type)) is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0)
is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type)) is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0)
else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null) else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null, priority = 50.0)
} }
if (ci.name.startsWith(prefix, true)) { if (ci.name.startsWith(prefix, true)) {
out += ci out += ci
already.add(name) already.add(name)
} }
} else { } else {
val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null) val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null, priority = 50.0)
if (ci.name.startsWith(prefix, true)) { if (ci.name.startsWith(prefix, true)) {
out += ci out += ci
already.add(name) already.add(name)

View File

@ -87,6 +87,7 @@ sealed interface MiniNamedDecl : MiniNode {
// 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
val nameStart: Pos val nameStart: Pos
val isExtern: Boolean val isExtern: Boolean
val isStatic: Boolean
} }
sealed interface MiniDecl : MiniNamedDecl sealed interface MiniDecl : MiniNamedDecl
@ -114,7 +115,8 @@ data class MiniFunDecl(
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
val receiver: MiniTypeRef? = null, val receiver: MiniTypeRef? = null,
override val isExtern: Boolean = false override val isExtern: Boolean = false,
override val isStatic: Boolean = false,
) : MiniDecl ) : MiniDecl
data class MiniValDecl( data class MiniValDecl(
@ -126,7 +128,8 @@ data class MiniValDecl(
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
val receiver: MiniTypeRef? = null, val receiver: MiniTypeRef? = null,
override val isExtern: Boolean = false override val isExtern: Boolean = false,
override val isStatic: Boolean = false,
) : MiniDecl ) : MiniDecl
data class MiniClassDecl( data class MiniClassDecl(
@ -141,6 +144,7 @@ data class MiniClassDecl(
// Built-in extension: list of member declarations (functions and fields) // Built-in extension: list of member declarations (functions and fields)
val members: List<MiniMemberDecl> = emptyList(), val members: List<MiniMemberDecl> = emptyList(),
override val isExtern: Boolean = false, override val isExtern: Boolean = false,
override val isStatic: Boolean = false,
val isObject: Boolean = false val isObject: Boolean = false
) : MiniDecl ) : MiniDecl
@ -150,7 +154,8 @@ data class MiniEnumDecl(
val entries: List<String>, val entries: List<String>,
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
override val isExtern: Boolean = false override val isExtern: Boolean = false,
override val isStatic: Boolean = false,
) : MiniDecl ) : MiniDecl
data class MiniCtorField( data class MiniCtorField(
@ -175,7 +180,7 @@ data class MiniIdentifier(
// --- Class member declarations (for built-in/registry docs) --- // --- Class member declarations (for built-in/registry docs) ---
sealed interface MiniMemberDecl : MiniNamedDecl { sealed interface MiniMemberDecl : MiniNamedDecl {
val isStatic: Boolean override val isStatic: Boolean
} }
data class MiniMemberFunDecl( data class MiniMemberFunDecl(
@ -319,7 +324,7 @@ class MiniAstBuilder : MiniAstSink {
returnType = attach.returnType, returnType = attach.returnType,
doc = attach.doc, doc = attach.doc,
nameStart = attach.nameStart, nameStart = attach.nameStart,
isStatic = false, // TODO: track static if needed isStatic = attach.isStatic,
isExtern = attach.isExtern, isExtern = attach.isExtern,
body = attach.body body = attach.body
) )
@ -359,7 +364,7 @@ class MiniAstBuilder : MiniAstSink {
initRange = attach.initRange, initRange = attach.initRange,
doc = attach.doc, doc = attach.doc,
nameStart = attach.nameStart, nameStart = attach.nameStart,
isStatic = false, // TODO: track static if needed isStatic = attach.isStatic,
isExtern = attach.isExtern isExtern = attach.isExtern
) )
// Duplicates for vals are rare but possible if Compiler calls it twice // Duplicates for vals are rare but possible if Compiler calls it twice

View File

@ -106,6 +106,34 @@ open class ObjClass(
val classId: Long = ClassIdGen.nextId() val classId: Long = ClassIdGen.nextId()
var layoutVersion: Int = 0 var layoutVersion: Int = 0
private val mangledNameCache = mutableMapOf<String, String>()
fun mangledName(name: String): String = mangledNameCache.getOrPut(name) { "$className::$name" }
/**
* Map of public member names to their effective storage keys in instanceScope.objects.
* This is pre-calculated to avoid MRO traversal and string concatenation during common access.
*/
val publicMemberResolution: Map<String, String> by lazy {
val res = mutableMapOf<String, String>()
// Traverse MRO in REVERSED order so that child classes override parent classes in the map.
for (cls in mro.reversed()) {
if (cls.className == "Obj") continue
for ((name, rec) in cls.members) {
if (rec.visibility == Visibility.Public) {
val key = if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Delegated) cls.mangledName(name) else name
res[name] = key
}
}
cls.classScope?.objects?.forEach { (name, rec) ->
if (rec.visibility == Visibility.Public && (rec.value is Statement || rec.type == ObjRecord.Type.Delegated)) {
val key = if (rec.type == ObjRecord.Type.Delegated) cls.mangledName(name) else name
res[name] = key
}
}
}
res
}
val classNameObj by lazy { ObjString(className) } val classNameObj by lazy { ObjString(className) }
var constructorMeta: ArgsDeclaration? = null var constructorMeta: ArgsDeclaration? = null
@ -242,6 +270,43 @@ open class ObjClass(
return instance return instance
} }
/** Pre-calculated template for instanceScope.objects. */
private val instanceObjectsTemplate: Map<String, ObjRecord> by lazy {
val res = mutableMapOf<String, ObjRecord>()
for (cls in mro) {
// 1) members-defined methods and fields
for ((k, v) in cls.members) {
if (!v.isAbstract && (v.value is Statement || v.type == ObjRecord.Type.Delegated || v.type == ObjRecord.Type.Field)) {
val key = if (v.visibility == Visibility.Private || v.type == ObjRecord.Type.Field || v.type == ObjRecord.Type.Delegated) cls.mangledName(k) else k
if (!res.containsKey(key)) {
res[key] = v
}
}
}
// 2) class-scope members registered during class-body execution
cls.classScope?.objects?.forEach { (k, rec) ->
// ONLY copy methods and delegated members from class scope to instance scope.
// Fields in class scope are static fields and must NOT be per-instance.
if (!rec.isAbstract && (rec.value is Statement || rec.type == ObjRecord.Type.Delegated)) {
val key = if (rec.visibility == Visibility.Private || rec.type == ObjRecord.Type.Delegated) cls.mangledName(k) else k
// if not already present, copy reference for dispatch
if (!res.containsKey(key)) {
res[key] = rec
}
}
}
}
res
}
private val templateMethods: Map<String, ObjRecord> by lazy {
instanceObjectsTemplate.filter { it.value.type == ObjRecord.Type.Fun }
}
private val templateOthers: List<Pair<String, ObjRecord>> by lazy {
instanceObjectsTemplate.filter { it.value.type != ObjRecord.Type.Fun }.toList()
}
/** /**
* Create an instance of this class and initialize its [ObjInstance.instanceScope] with * Create an instance of this class and initialize its [ObjInstance.instanceScope] with
* methods. Does NOT run initializers or constructors. * methods. Does NOT run initializers or constructors.
@ -257,28 +322,9 @@ open class ObjClass(
// Expose instance methods (and other callable members) directly in the instance scope for fast lookup // Expose instance methods (and other callable members) directly in the instance scope for fast lookup
// This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust // This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust
for (cls in mro) { instance.instanceScope.objects.putAll(templateMethods)
// 1) members-defined methods and fields for (p in templateOthers) {
for ((k, v) in cls.members) { instance.instanceScope.objects[p.first] = p.second.copy()
if (!v.isAbstract && (v.value is Statement || v.type == ObjRecord.Type.Delegated || v.type == ObjRecord.Type.Field)) {
val key = if (v.visibility == Visibility.Private || v.type == ObjRecord.Type.Field || v.type == ObjRecord.Type.Delegated) "${cls.className}::$k" else k
if (!instance.instanceScope.objects.containsKey(key)) {
instance.instanceScope.objects[key] = if (v.type == ObjRecord.Type.Fun) v else v.copy()
}
}
}
// 2) class-scope members registered during class-body execution
cls.classScope?.objects?.forEach { (k, rec) ->
// ONLY copy methods and delegated members from class scope to instance scope.
// Fields in class scope are static fields and must NOT be per-instance.
if (!rec.isAbstract && (rec.value is Statement || rec.type == ObjRecord.Type.Delegated)) {
val key = if (rec.visibility == Visibility.Private || rec.type == ObjRecord.Type.Delegated) "${cls.className}::$k" else k
// if not already present, copy reference for dispatch
if (!instance.instanceScope.objects.containsKey(key)) {
instance.instanceScope.objects[key] = if (rec.type == ObjRecord.Type.Fun) rec else rec.copy()
}
}
}
} }
return instance return instance
} }
@ -327,7 +373,7 @@ open class ObjClass(
for (p in meta.params) { for (p in meta.params) {
val rec = instance.instanceScope.objects[p.name] val rec = instance.instanceScope.objects[p.name]
if (rec != null) { if (rec != null) {
val mangled = "${c.className}::${p.name}" val mangled = c.mangledName(p.name)
// Always point the mangled name to the current record to keep writes consistent // Always point the mangled name to the current record to keep writes consistent
// across re-bindings // across re-bindings
instance.instanceScope.objects[mangled] = rec instance.instanceScope.objects[mangled] = rec
@ -361,7 +407,7 @@ open class ObjClass(
for (p in meta.params) { for (p in meta.params) {
val rec = instance.instanceScope.objects[p.name] val rec = instance.instanceScope.objects[p.name]
if (rec != null) { if (rec != null) {
val mangled = "${c.className}::${p.name}" val mangled = c.mangledName(p.name)
instance.instanceScope.objects[mangled] = rec instance.instanceScope.objects[mangled] = rec
} }
} }

View File

@ -34,6 +34,18 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
override suspend fun readField(scope: Scope, name: String): ObjRecord { override suspend fun readField(scope: Scope, name: String): ObjRecord {
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
// Fast path for public members when outside any class context
if (caller == null) {
objClass.publicMemberResolution[name]?.let { key ->
instanceScope.objects[key]?.let { rec ->
// Directly return fields to bypass resolveRecord overhead
if ((rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.ConstructorField) && !rec.isAbstract)
return rec
return resolveRecord(scope, rec, name, rec.declaringClass)
}
}
}
// 0. Private mangled of current class context // 0. Private mangled of current class context
caller?.let { c -> caller?.let { c ->
// Check for private methods/properties // Check for private methods/properties
@ -43,7 +55,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
} }
// Check for private fields (stored in instanceScope) // Check for private fields (stored in instanceScope)
val mangled = "${c.className}::$name" val mangled = c.mangledName(name)
instanceScope.objects[mangled]?.let { rec -> instanceScope.objects[mangled]?.let { rec ->
if (rec.visibility == Visibility.Private) { if (rec.visibility == Visibility.Private) {
return resolveRecord(scope, rec, name, c) return resolveRecord(scope, rec, name, c)
@ -54,7 +66,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// 1. MRO mangled storage // 1. MRO mangled storage
for (cls in objClass.mro) { for (cls in objClass.mro) {
if (cls.className == "Obj") break if (cls.className == "Obj") break
val mangled = "${cls.className}::$name" val mangled = cls.mangledName(name)
instanceScope.objects[mangled]?.let { rec -> instanceScope.objects[mangled]?.let { rec ->
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, cls, caller)) { if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, cls, caller)) {
return resolveRecord(scope, rec, name, cls) return resolveRecord(scope, rec, name, cls)
@ -79,11 +91,11 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (obj.type.isArgument) return super.resolveRecord(scope, obj, name, decl) if (obj.type.isArgument) return super.resolveRecord(scope, obj, name, decl)
if (obj.type == ObjRecord.Type.Delegated) { if (obj.type == ObjRecord.Type.Delegated) {
val d = decl ?: obj.declaringClass val d = decl ?: obj.declaringClass
val storageName = "${d?.className}::$name" val storageName = d?.mangledName(name) ?: name
var del = instanceScope[storageName]?.delegate ?: obj.delegate var del = instanceScope[storageName]?.delegate ?: obj.delegate
if (del == null) { if (del == null) {
for (c in objClass.mro) { for (c in objClass.mro) {
del = instanceScope["${c.className}::$name"]?.delegate del = instanceScope[c.mangledName(name)]?.delegate
if (del != null) break if (del != null) break
} }
} }
@ -97,7 +109,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
var targetRec = obj var targetRec = obj
val d = decl ?: obj.declaringClass val d = decl ?: obj.declaringClass
if (d != null) { if (d != null) {
val mangled = "${d.className}::$name" val mangled = d.mangledName(name)
instanceScope.objects[mangled]?.let { instanceScope.objects[mangled]?.let {
targetRec = it targetRec = it
} }
@ -120,6 +132,24 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
willMutate(scope) willMutate(scope)
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
// Fast path for public members when outside any class context
if (caller == null) {
objClass.publicMemberResolution[name]?.let { key ->
instanceScope.objects[key]?.let { rec ->
if (rec.effectiveWriteVisibility == Visibility.Public) {
// Skip property/delegated overhead if it's a plain mutable field
if (rec.type == ObjRecord.Type.Field && rec.isMutable && !rec.isAbstract) {
if (rec.value.assign(scope, newValue) == null)
rec.value = newValue
return
}
updateRecord(scope, rec, name, newValue, rec.declaringClass)
return
}
}
}
}
// 0. Private mangled of current class context // 0. Private mangled of current class context
caller?.let { c -> caller?.let { c ->
// Check for private methods/properties // Check for private methods/properties
@ -130,7 +160,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
} }
// Check for private fields (stored in instanceScope) // Check for private fields (stored in instanceScope)
val mangled = "${c.className}::$name" val mangled = c.mangledName(name)
instanceScope.objects[mangled]?.let { rec -> instanceScope.objects[mangled]?.let { rec ->
if (rec.visibility == Visibility.Private) { if (rec.visibility == Visibility.Private) {
updateRecord(scope, rec, name, newValue, c) updateRecord(scope, rec, name, newValue, c)
@ -142,7 +172,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// 1. MRO mangled storage // 1. MRO mangled storage
for (cls in objClass.mro) { for (cls in objClass.mro) {
if (cls.className == "Obj") break if (cls.className == "Obj") break
val mangled = "${cls.className}::$name" val mangled = cls.mangledName(name)
instanceScope.objects[mangled]?.let { rec -> instanceScope.objects[mangled]?.let { rec ->
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, cls, caller)) { if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, cls, caller)) {
updateRecord(scope, rec, name, newValue, cls) updateRecord(scope, rec, name, newValue, cls)
@ -191,24 +221,42 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
scope: Scope, name: String, args: Arguments, scope: Scope, name: String, args: Arguments,
onNotFoundResult: (suspend () -> Obj?)? onNotFoundResult: (suspend () -> Obj?)?
): Obj { ): Obj {
// 0. Prefer private member of current class context val caller = scope.currentClassCtx
scope.currentClassCtx?.let { caller ->
val mangled = "${caller.className}::$name" // Fast path for public members when outside any class context
instanceScope.objects[mangled]?.let { rec -> if (caller == null) {
if (rec.visibility == Visibility.Private && !rec.isAbstract) { objClass.publicMemberResolution[name]?.let { key ->
if (rec.type == ObjRecord.Type.Property) { instanceScope.objects[key]?.let { rec ->
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller) if (rec.visibility == Visibility.Public && !rec.isAbstract) {
} else if (rec.type == ObjRecord.Type.Fun) { val decl = rec.declaringClass
return rec.value.invoke(instanceScope, this, args, caller) if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, decl)
}
} }
} }
} }
caller.members[name]?.let { rec -> }
// 0. Prefer private member of current class context
caller?.let { c ->
val mangled = c.mangledName(name)
instanceScope.objects[mangled]?.let { rec ->
if (rec.visibility == Visibility.Private && !rec.isAbstract) { if (rec.visibility == Visibility.Private && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Property) { if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller) if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
} else if (rec.type == ObjRecord.Type.Fun) { } else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, caller) return rec.value.invoke(instanceScope, this, args, c)
}
}
}
c.members[name]?.let { rec ->
if (rec.visibility == Visibility.Private && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, c)
} }
} }
} }
@ -220,7 +268,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract) { if (rec != null && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Delegated) { if (rec.type == ObjRecord.Type.Delegated) {
val storageName = "${cls.className}::$name" val storageName = cls.mangledName(name)
val del = instanceScope[storageName]?.delegate ?: rec.delegate val del = instanceScope[storageName]?.delegate ?: rec.delegate
?: scope.raiseError("Internal error: delegated member $name has no delegate (tried $storageName)") ?: scope.raiseError("Internal error: delegated member $name has no delegate (tried $storageName)")
@ -233,8 +281,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
}) })
} }
val decl = rec.declaringClass ?: cls val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, caller)) if (!canAccessMember(rec.visibility, decl, effectiveCaller))
scope.raiseError( scope.raiseError(
ObjIllegalAccessException( ObjIllegalAccessException(
scope, scope,

File diff suppressed because it is too large Load Diff

View File

@ -419,6 +419,25 @@ class MiniAstTest {
assertEquals("Int", innerArg.segments.last().name) assertEquals("Int", innerArg.segments.last().name)
} }
@Test
fun verify_class_member_structure() = runTest {
val code = """
class A {
fun member() {}
val field = 1
}
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()!!
println("[DEBUG_LOG] Mini declarations: ${mini.declarations.map { it.name }}")
assertEquals(1, mini.declarations.size, "Should only have one top-level declaration (class A)")
val cls = mini.declarations[0] as MiniClassDecl
assertEquals("A", cls.name)
assertEquals(2, cls.members.size, "Class A should have 2 members (member() and field)")
assertTrue(cls.members.any { it.name == "member" && it is MiniMemberFunDecl })
assertTrue(cls.members.any { it.name == "field" && it is MiniMemberValDecl })
}
@Test @Test
fun inferTypeForValWithInference() = runTest { fun inferTypeForValWithInference() = runTest {
val code = """ val code = """

View File

@ -1,3 +1,20 @@
/*
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng.miniast package net.sergeych.lyng.miniast
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -462,6 +479,34 @@ class CompletionEngineLightTest {
assertTrue(ns.contains("size"), "String member 'size' should be suggested for local x[0] inside set(), but got: $ns") assertTrue(ns.contains("size"), "String member 'size' should be suggested for local x[0] inside set(), but got: $ns")
} }
@Test
fun functionArgumentsInBody() = runBlocking {
val code = """
fun test(myArg1, myArg2) {
myA<caret>
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("myArg1"), "Function argument 'myArg1' should be proposed, but got: $ns")
assertTrue(ns.contains("myArg2"), "Function argument 'myArg2' should be proposed, but got: $ns")
}
@Test
fun methodArgumentsInBody() = runBlocking {
val code = """
class MyClass {
fun test(myArg1, myArg2) {
myA<caret>
}
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("myArg1"), "Method argument 'myArg1' should be proposed, but got: $ns")
assertTrue(ns.contains("myArg2"), "Method argument 'myArg2' should be proposed, but got: $ns")
}
@Test @Test
fun nestedShadowingCompletion() = runBlocking { fun nestedShadowingCompletion() = runBlocking {
val code = """ val code = """

View File

@ -98,6 +98,7 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
val lineStart = lineStartAt(text, start) val lineStart = lineStartAt(text, start)
val lineEnd = lineEndAt(text, start) val lineEnd = lineEndAt(text, start)
val indent = countIndentSpaces(text, lineStart, lineEnd) val indent = countIndentSpaces(text, lineStart, lineEnd)
val lineTrimmed = text.substring(lineStart, lineEnd).trim()
// Compute neighborhood characters early so rule precedence can use them // Compute neighborhood characters early so rule precedence can use them
val prevIdx = prevNonWs(text, start) val prevIdx = prevNonWs(text, start)
@ -107,7 +108,7 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
val before = text.substring(0, start) val before = text.substring(0, start)
val after = text.substring(start) val after = text.substring(start)
// 1) Between braces { | } -> two lines, inner indented // Rule 4: Between braces on the same line {|}
if (prevCh == '{' && nextCh == '}') { if (prevCh == '{' && nextCh == '}') {
val innerIndent = indent + tabSize val innerIndent = indent + tabSize
val insertion = "\n" + " ".repeat(innerIndent) + "\n" + " ".repeat(indent) val insertion = "\n" + " ".repeat(innerIndent) + "\n" + " ".repeat(indent)
@ -115,22 +116,51 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
val caret = start + 1 + innerIndent val caret = start + 1 + innerIndent
return EditResult(out, caret, caret) return EditResult(out, caret, caret)
} }
// 2) After '{'
// Rule 2: On a brace-only line '}'
if (lineTrimmed == "}") {
val newIndent = (indent - tabSize).coerceAtLeast(0)
val newCurrentLine = " ".repeat(newIndent) + "}"
val insertion = "\n" + " ".repeat(newIndent)
val out = safeSubstring(text, 0, lineStart) + newCurrentLine + insertion + safeSubstring(text, lineEnd, text.length)
val caret = lineStart + newCurrentLine.length + insertion.length
return EditResult(out, caret, caret)
}
// Rule 1: After '{'
if (prevCh == '{') { if (prevCh == '{') {
val insertion = "\n" + " ".repeat(indent + tabSize) val insertion = "\n" + " ".repeat(indent + tabSize)
val out = before + insertion + after val out = before + insertion + after
val caret = start + insertion.length val caret = start + insertion.length
return EditResult(out, caret, caret) return EditResult(out, caret, caret)
} }
// 3) Before '}'
if (nextCh == '}') { // Rule 3: End of a line before a brace-only next line
val insertion = "\n" + " ".repeat(indent) if (start == lineEnd && lineEnd < text.length) {
val nextLineStart = lineEnd + 1
val nextLineEnd = lineEndAt(text, nextLineStart)
val nextLineTrimmed = text.substring(nextLineStart, nextLineEnd).trim()
if (nextLineTrimmed == "}") {
val nextLineIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
val newNextLineIndent = (nextLineIndent - tabSize).coerceAtLeast(0)
val newNextLine = " ".repeat(newNextLineIndent) + "}"
val out = text.substring(0, nextLineStart) + newNextLine + text.substring(nextLineEnd)
val caret = nextLineStart + newNextLineIndent
return EditResult(out, caret, caret)
}
}
// Rule 5: After '}' with only spaces until end-of-line
val afterCaretOnLine = text.substring(start, lineEnd)
if (prevCh == '}' && afterCaretOnLine.trim().isEmpty()) {
val newIndent = (indent - tabSize).coerceAtLeast(0)
val insertion = "\n" + " ".repeat(newIndent)
val out = before + insertion + after val out = before + insertion + after
val caret = start + insertion.length val caret = start + insertion.length
return EditResult(out, caret, caret) return EditResult(out, caret, caret)
} }
// default keep same indent // Rule 6: Default smart indent
val insertion = "\n" + " ".repeat(indent) val insertion = "\n" + " ".repeat(indent)
val out = before + insertion + after val out = before + insertion + after
val caret = start + insertion.length val caret = start + insertion.length

View File

@ -329,7 +329,7 @@
<!-- Top-left version ribbon --> <!-- Top-left version ribbon -->
<div class="corner-ribbon bg-danger text-white"> <div class="corner-ribbon bg-danger text-white">
<span style="margin-left: -5em"> <span style="margin-left: -5em">
v1.1.1-SNAPSHOT v1.2.0-SNAPSHOT
</span> </span>
</div> </div>
<!-- Fixed top navbar for the whole site --> <!-- Fixed top navbar for the whole site -->

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,17 +41,23 @@ class EditorE2ETest {
// Programmatically type text into the textarea at current selection and dispatch an input event // Programmatically type text into the textarea at current selection and dispatch an input event
private fun typeText(ta: HTMLTextAreaElement, s: String) { private fun typeText(ta: HTMLTextAreaElement, s: String) {
for (ch in s) { for (ch in s) {
val start = ta.selectionStart ?: 0 val key = ch.toString()
val end = ta.selectionEnd ?: start val ev = js("new KeyboardEvent('keydown', {key: key, bubbles: true, cancelable: true})")
val before = ta.value.substring(0, start) val wasPrevented = !ta.dispatchEvent(ev.unsafeCast<org.w3c.dom.events.Event>())
val after = ta.value.substring(end)
ta.value = before + ch + after if (!wasPrevented) {
val newPos = start + 1 val start = ta.selectionStart ?: 0
ta.selectionStart = newPos val end = ta.selectionEnd ?: start
ta.selectionEnd = newPos val before = ta.value.substring(0, start)
// Fire input so EditorWithOverlay updates its state val after = ta.value.substring(end)
val ev = js("new Event('input', {bubbles:true})") ta.value = before + ch + after
ta.dispatchEvent(ev.unsafeCast<org.w3c.dom.events.Event>()) val newPos = start + 1
ta.selectionStart = newPos
ta.selectionEnd = newPos
// Fire input so EditorWithOverlay updates its state
val inputEv = js("new Event('input', {bubbles:true})")
ta.dispatchEvent(inputEv.unsafeCast<org.w3c.dom.events.Event>())
}
} }
} }
@ -243,10 +249,10 @@ class EditorE2ETest {
typeText(ta, "4"); nextFrame() typeText(ta, "4"); nextFrame()
dispatchKey(ta, key = "Enter"); nextFrame(); nextFrame() dispatchKey(ta, key = "Enter"); nextFrame(); nextFrame()
typeText(ta, "}"); nextFrame() typeText(ta, "}"); nextFrame(); nextFrame(); nextFrame()
dispatchKey(ta, key = "Enter"); nextFrame(); nextFrame() dispatchKey(ta, key = "Enter"); nextFrame(); nextFrame()
typeText(ta, "5"); nextFrame(); nextFrame() typeText(ta, "5"); nextFrame(); nextFrame(); nextFrame()
val expected = ( val expected = (
""" """