Skip to content

CommonMemberPrior.kt

src/jvmMain/kotlin/afield/runtime/completion/CommonMemberPrior.kt · 136 lines

package afield.runtime.completion

import afield.app.protocol.Metadata
import afield.data.table.div
import afield.data.table.get
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.runtime.execution.plus
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.runtime.lifecycle.then
import afield.ui.notation.string
import afield.ui.visualization.typed

import afield.audio.synth.*
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import java.util.concurrent.ConcurrentHashMap

A ranking prior for the completion popup taken from a type's CommonMembers annotation, so that an empty-prefix popup such as a bare f. lists the members that matter first.

CommonMemberPrior.rankOf gives a member's declared position and CommonMemberPrior.scoreOf turns it into popup points, below a user's own recency and above the category weight.

object CommonMemberPrior

Resolves a receiver type's CommonMembers declaration into the ranking prior the popup consumes (Metadata.commonMemberRank, scored by COMMON_MEMBER_BASE in fieldSuggest.js).

Why this exists

At an empty prefix — a bare f. — there is nothing to fuzzy-match, so fieldSuggest.js ranks on category weight (0–28) plus the server's own order, which contributes about 3 points spread across the whole list (originalIndex * 0.012). The term that should decide it is recency, worth up to ~62 — but recency is localStorage, so it is empty for a new user, empty in a fresh profile, and empty in every test. The popup therefore had no way to know that moveTo matters more than breakNext, and no way for a test to assert that it should. This supplies one from the source.

Authority is deliberately ordered recency > this prior > category > server order: what a person actually does beats what the author guessed, which beats a bucket, which beats alphabetical.

Resolution

By qualified receiver name (AaCompletionClient forwards it as receiverQualifiedName), via Class.forName and then up the superclass chain, nearest class winning — a subclass may re-order its parent's list, and a receiver typed as a subclass still gets the parent's prior. Interfaces are walked too, since a receiver is often typed as one.

Both hits and misses are cached: this is on the completion path, which runs per keystroke, and the overwhelmingly common answer is "this type has no annotation".

object CommonMemberPrior {

const val BASE = 40

Score for the first-listed member. Below recency's ~62 ceiling, above CATEGORY_WEIGHT's 28.

const val STEP = 2

Points shed per position, so a declared order survives into the popup without swamping it.

    private val byReceiver = ConcurrentHashMap<String, Map<String, Int>>()
    private val NONE: Map<String, Int> = emptyMap()

fun rankOf(receiverQualifiedName: String?, memberName: String?): Int

1-based position of memberName in receiverQualifiedName's list, or 0 if either is absent.

memberName is matched on the bare name, so moveTo(x: Number, y: Number) and moveTo(v: Vec3) both resolve — the caller may pass a display string.

    fun rankOf(receiverQualifiedName: String?, memberName: String?): Int {
        if (receiverQualifiedName.isNullOrBlank() || memberName.isNullOrBlank()) return 0
        val ranks = ranksFor(receiverQualifiedName)
        if (ranks.isEmpty()) return 0
        return ranks[bareName(memberName)] ?: 0
    }

fun scoreOf(rank: Int): Int = if (rank <= 0) 0 else (BASE - (rank - 1) * STEP).coerceAtLeast(1)

Turns a rank into the client-side ranking term. Rank 0 scores nothing; the list decays by STEP.

fun ranksFor(receiverQualifiedName: String?): Map<String, Int>

Declared common members of receiverQualifiedName, name to 1-based position. Empty if none.

    fun ranksFor(receiverQualifiedName: String?): Map<String, Int> {
        val key = receiverQualifiedName?.trim()?.takeIf { it.isNotBlank() } ?: return NONE
        return byReceiver.getOrPut(key) { computeRanks(key) }
    }

fun declaredNames(type: Class<*>): List<String>

Names declared directly on type's own annotation, in declared order — what the guard test checks against the class's actual members. A superclass's list is not included: a class is only answerable for the list it writes.

    fun declaredNames(type: Class<*>): List<String> =
        type.getAnnotation(CommonMembers::class.java)?.value?.toList().orEmpty()

    private fun computeRanks(qualifiedName: String): Map<String, Int> {
        val type = loadClass(qualifiedName) ?: return NONE
        // Nearest declaration wins, so visit the class before anything it inherits from and let the first
        // writer of a name keep its position.
        val ordered = LinkedHashMap<String, Int>()
        for (candidate in selfThenSupertypes(type)) {
            val names = declaredNames(candidate)
            if (names.isEmpty()) continue
            for (name in names) {
                val bare = bareName(name)
                if (bare.isNotBlank() && bare !in ordered) ordered[bare] = ordered.size + 1
            }
        }
        return if (ordered.isEmpty()) NONE else ordered
    }

Breadth-first over superclasses and interfaces, self first, each class visited once.

    private fun selfThenSupertypes(type: Class<*>): List<Class<*>> {
        val seen = LinkedHashSet<Class<*>>()
        val queue = ArrayDeque<Class<*>>()
        queue += type
        while (queue.isNotEmpty()) {
            val next = queue.removeFirst()
            if (next == Any::class.java || !seen.add(next)) continue
            next.superclass?.let { queue += it }
            queue += next.interfaces
        }
        return seen.toList()
    }

    private fun loadClass(qualifiedName: String): Class<*>? = runCatching {
        Class.forName(qualifiedName, false, CommonMemberPrior::class.java.classLoader)
    }.getOrNull()

moveTo(v: Vec3) / moveTo<T> / FLine.moveTo all name the member moveTo.

    private fun bareName(raw: String): String =
        raw.substringBefore('<').substringBefore('(').substringAfterLast('.').trim()
}