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:
sergeych committed 2026-01-11 01:42:53 +01:00
1 parent 827df9c8cd
commit 6b957ae6a3
28 files changed
+1339 -506

No files matched your search

@@ -167,7 +167,11 @@ class LyngCompletionContributor : CompletionContributor() {
.withIcon(AllIcons.Nodes.Field)
.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
if (memberDotPos != null) {
@@ -401,7 +405,7 @@ class LyngCompletionContributor : CompletionContributor() {
}
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() }
for (name in keys) {
val list = map[name] ?: continue
@@ -428,7 +432,11 @@ class LyngCompletionContributor : CompletionContributor() {
.withTailText(tail, true)
.withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler)
emit(builder)
if (groupPriority != 0.0) {
emit(PrioritizedLookupElement.withPriority(builder, groupPriority))
} else {
emit(builder)
}
}
is MiniMemberValDecl -> {
val icon = if (rep.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field
@@ -439,7 +447,11 @@ class LyngCompletionContributor : CompletionContributor() {
val builder = LookupElementBuilder.create(name)
.withIcon(icon)
.withTypeText(typeOf(chosen.type), true)
emit(builder)
if (groupPriority != 0.0) {
emit(PrioritizedLookupElement.withPriority(builder, groupPriority))
} else {
emit(builder)
}
}
is MiniInitDecl -> {}
}
@@ -447,8 +459,8 @@ class LyngCompletionContributor : CompletionContributor() {
}
// Emit what we have first
emitGroup(directMap)
emitGroup(inheritedMap)
emitGroup(directMap, 100.0)
emitGroup(inheritedMap, 0.0)
// If suggestions are suspiciously sparse for known container classes,
// try to conservatively supplement using a curated list resolved via docs registry.
@@ -509,7 +521,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withInsertHandler(ParenInsertHandler)
}
}
emit(builder)
emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name)
} else {
// Synthetic fallback: method without detailed params/types to improve UX in absence of docs
@@ -523,7 +535,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withTailText("()", true)
.withInsertHandler(ParenInsertHandler)
}
emit(builder)
emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name)
}
}
@@ -576,7 +588,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withInsertHandler(ParenInsertHandler)
}
}
emit(builder)
emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name)
continue
}
@@ -585,7 +597,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withIcon(AllIcons.Nodes.Method)
.withTailText("()", true)
.withInsertHandler(ParenInsertHandler)
emit(builder)
emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name)
}
}
@@ -461,25 +461,39 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
private fun ensureExternalDocsRegistered() { @Suppress("UNUSED_EXPRESSION") externalDocsLoaded }
private fun tryLoadExternalDocs(): Boolean {
return try {
var anyLoaded = false
try {
// Try known registrars; ignore failures if module is absent
val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs")
val m = cls.getMethod("ensure")
m.invoke(null)
log.info("[LYNG_DEBUG] QuickDoc: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK")
true
} catch (_: Throwable) {
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] 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
val seeded = try {
FsDocsFallback.ensureOnce()
ProcessDocsFallback.ensureOnce()
true
} catch (_: Throwable) { false }
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 {
log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found (lyngio absent on classpath)")
}
seeded
return seeded
}
return true
}
override fun getCustomDocumentationElement(
@@ -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
}
}
}
@@ -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");
* you may not use this file except in compliance with the License.
@@ -54,6 +54,8 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
} catch (_: Throwable) { false }
override fun getTokenizer(element: PsiElement): Tokenizer<*> {
if (element is com.intellij.psi.PsiFile) return EMPTY_TOKENIZER
val hasGrazie = grazieInstalled()
val hasGrazieApi = grazieApiAvailable()
val settings = LyngFormatterSettings.getInstance(element.project)
@@ -63,27 +65,40 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
}
val file = element.containingFile ?: return EMPTY_TOKENIZER
val index = LyngSpellIndex.getUpToDate(file) ?: run {
// Suspend legacy spellcheck until MiniAst-based index is ready
return EMPTY_TOKENIZER
}
val elRange = element.textRange ?: return EMPTY_TOKENIZER
fun overlaps(list: List<TextRange>) = list.any { it.intersects(elRange) }
val et = element.node?.elementType
val index = LyngSpellIndex.getUpToDate(file)
// Decide responsibility per settings
// 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 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
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
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
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
}
@@ -93,7 +108,7 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
}
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) {
val text = element.text
if (text.isNullOrEmpty()) return
@@ -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
* 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 net.sergeych.lyng.idea.docs.FsDocsFallback
import net.sergeych.lyng.idea.docs.ProcessDocsFallback
object DocsBootstrap {
private val log = Logger.getInstance(DocsBootstrap::class.java)
@@ -20,20 +38,32 @@ object DocsBootstrap {
}
}
private fun tryLoadExternal(): Boolean = try {
val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs")
val m = cls.getMethod("ensure")
m.invoke(null)
log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK")
true
} catch (_: Throwable) {
false
private fun tryLoadExternal(): Boolean {
var anyLoaded = false
try {
val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs")
val m = cls.getMethod("ensure")
m.invoke(null)
log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK")
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 {
val seeded = FsDocsFallback.ensureOnce()
val seededFs = FsDocsFallback.ensureOnce()
val seededProcess = ProcessDocsFallback.ensureOnce()
val seeded = seededFs || seededProcess
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 {
log.info("[LYNG_DEBUG] DocsBootstrap: external docs not found; no fallback seeded")
}
@@ -16,8 +16,8 @@
-->
<idea-plugin>
<!-- Open-ended compatibility: 2024.3+ (build 243 and newer) -->
<idea-version since-build="243"/>
<!-- Open-ended compatibility: 2024.1+ (build 241 and newer) -->
<idea-version since-build="241"/>
<id>net.sergeych.lyng.idea</id>
<name>Lyng</name>
<vendor email="real.sergeych@gmail.com">Sergey Chernov</vendor>
@@ -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
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)
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())
}
}