PdObjects.kt
src/jvmMain/kotlin/afield/audio/pd/PdObjects.kt · 90 lines
@file:afield.runtime.completion.CompletionCategory("Synthesis")
package afield.audio.pd
import java.nio.file.Files
import java.nio.file.Path
The catalog of object names the built libpd can create, used by PdObjects.suggest to correct a mistyped object box.
object PdObjects
The objects this Pd can create, and the nearest of them to one it could not.
The list is pd-objects.txt, which tools/build_libpd_native.py writes beside the library from the
same pinned sources it compiled: every class_new and class_addcreator name in Pd's src and
extra. So it names exactly what PdLibrary.resolvedPath can make, vanilla objects and their
aliases (r, t, hsl), and nothing a Pd distribution adds on top. Without a built libpd it is
empty and suggest says nothing.
object PdObjects {
const val FILE_NAME = "pd-objects.txt"
The file catalog reads, beside the library.
private var cachedFrom: Pair<Path, Long>? = null
private var cached: Set<String> = emptySet()
fun catalog(): Set<String>
The names in the catalog beside the resolved libpd; empty when there is none.
@Synchronized
fun catalog(): Set<String> {
val file = PdLibrary.resolvedPath?.parent?.resolve(FILE_NAME)?.takeIf { Files.isRegularFile(it) } ?: return emptySet()
val stamp = file to Files.getLastModifiedTime(file).toMillis()
if (stamp != cachedFrom) {
cached = Files.readAllLines(file).map { it.trim() }.filter { it.isNotEmpty() && !it.startsWith("#") }.toSet()
cachedFrom = stamp
}
return cached
}
fun suggest(text: String, known: Set<String> = catalog()): List<String>
What text — an object box's contents, osc 440 — probably meant, as whole corrected texts
(osc~ 440); empty when nothing in known is close.
In order: the signal or control twin (osc → osc~, metro~ → metro), a difference of case
only, then the names within two edits (one for names of three letters or fewer), the closest
only; up to three are offered when they are equally close. A name Pd does know gives nothing —
that object failed for another reason, usually its arguments.
fun suggest(text: String, known: Set<String> = catalog()): List<String> {
val words = text.trim().split(Regex("\\s+"))
val name = words.firstOrNull()?.takeIf { it.isNotEmpty() } ?: return emptyList()
if (known.isEmpty() || name in known) return emptyList()
val args = words.drop(1).joinToString(" ")
fun with(n: String) = if (args.isEmpty()) n else "$n $args"
val twin = if (name.endsWith("~")) name.dropLast(1) else "$name~"
if (twin in known) return listOf(with(twin))
known.firstOrNull { it.equals(name, ignoreCase = true) }?.let { return listOf(with(it)) }
// Compare against single names only: "list append" is the object "list" with an argument.
val candidates = known.filter { ' ' !in it }
val limit = if (name.length <= 3) 1 else 2
val scored = candidates.map { it to editDistance(name, it) }.filter { it.second <= limit }
val best = scored.minOfOrNull { it.second } ?: return emptyList()
return scored.filter { it.second == best }.map { it.first }.sorted().take(3).map(::with)
}
fun hint(text: String, known: Set<String> = catalog()): String
"did you mean [osc~ 440]?" for text, or "" when suggest has nothing.
fun hint(text: String, known: Set<String> = catalog()): String {
val offers = suggest(text, known).map { "[$it]" }
return when (offers.size) {
0 -> ""
1 -> "did you mean ${offers[0]}?"
else -> "did you mean " + offers.dropLast(1).joinToString(", ") + " or " + offers.last() + "?"
}
}
internal fun editDistance(a: String, b: String): Int
Optimal-string-alignment distance: insertions, deletions, substitutions and adjacent swaps.
internal fun editDistance(a: String, b: String): Int {
val d = Array(a.length + 1) { IntArray(b.length + 1) }
for (i in 0..a.length) d[i][0] = i
for (j in 0..b.length) d[0][j] = j
for (i in 1..a.length) for (j in 1..b.length) {
val cost = if (a[i - 1] == b[j - 1]) 0 else 1
d[i][j] = minOf(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost)
if (i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1]) d[i][j] = minOf(d[i][j], d[i - 2][j - 2] + 1)
}
return d[a.length][b.length]
}
}