Pith - wedge_android
wedge_android/app/src/main/java/com/vgmlr/wedge/WedgeEditorEngine.kt [10.7 kb]
Modified: 23:08:24 55 026 (13 May 026)
17 Days Ago
package com.vgmlr.wedge

import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale

data class EditorResult(
    val text: TextFieldValue,
    val undoHistory: List<TextFieldValue>,
    val undoLineIndex: Int,
    val lastDel: Long
)

object WedgeEditorEngine {
    fun applyTextChange(
        newValue: TextFieldValue,
        textState: TextFieldValue,
        undoHistory: List<TextFieldValue>,
        undoLineIndex: Int,
        lastDel: Long
    ): EditorResult {
        var currentUndoHistory = undoHistory
        var currentUndoLineIndex = undoLineIndex
        var currentLastDel = lastDel

        if (newValue.text.length < textState.text.length) {
            val currentLine = textState.text.substring(
                0,
                textState.selection.start.coerceIn(0, textState.text.length)
            ).count { it == '\n' }
            if (currentUndoHistory.isEmpty() || currentLine != currentUndoLineIndex) {
                currentUndoHistory = (listOf(textState) + currentUndoHistory).take(2)
                currentUndoLineIndex = currentLine
            }
        } else if (newValue.text != textState.text && currentUndoHistory.isNotEmpty()) {
            val currentLine = newValue.text.substring(
                0,
                newValue.selection.start.coerceIn(0, newValue.text.length)
            ).count { it == '\n' }
            if (currentLine != currentUndoLineIndex) currentUndoHistory = emptyList()
        }

        if (newValue.text.length < textState.text.length) {
            val now = System.currentTimeMillis()
            if (now - currentLastDel < 45) return EditorResult(textState, currentUndoHistory, currentUndoLineIndex, currentLastDel)
            currentLastDel = now

            val oldText = textState.text
            val oldSel = textState.selection

            if ((oldText.length - newValue.text.length) > 1 && oldSel.collapsed) {
                val start = (oldSel.start - 1).coerceAtLeast(0)
                val suppressedText = oldText.removeRange(start, oldSel.start)
                return EditorResult(
                    newValue.copy(text = suppressedText, selection = TextRange(start)),
                    currentUndoHistory, currentUndoLineIndex, currentLastDel
                )
            }

            val expectedSuffix = oldText.substring(oldSel.max)
            val actualSuffix =
                newValue.text.substring(newValue.selection.max.coerceAtMost(newValue.text.length))

            if (expectedSuffix != actualSuffix) {
                val safePrefix = newValue.text.substring(
                    0,
                    newValue.selection.max.coerceAtMost(newValue.text.length)
                )
                return EditorResult(
                    newValue.copy(text = safePrefix + expectedSuffix),
                    currentUndoHistory, currentUndoLineIndex, currentLastDel
                )
            } else {
                return EditorResult(newValue, currentUndoHistory, currentUndoLineIndex, currentLastDel)
            }
        }

        var updatedValue = newValue
        val lengthDelta = newValue.text.length - textState.text.length
        val newCursor = newValue.selection.start
        val txt = newValue.text

        if (lengthDelta == 1 && newCursor > 0 && newCursor <= txt.length && (txt[newCursor - 1] == '\n' || txt[newCursor - 1] == ' ')) {
            try {
                val textBeforeCursor =
                    txt.substring(0, (newCursor - 1).coerceAtLeast(0))
                val lastNewlineIndex = textBeforeCursor.lastIndexOf('\n')
                val currentLine =
                    if (lastNewlineIndex == -1) textBeforeCursor else textBeforeCursor.substring(
                        lastNewlineIndex + 1
                    )
                val dateMatch = WedgeRegex.DATE_MATH_TRIGGER.find(currentLine)
                val mathMatch = WedgeRegex.MATH_TRIGGER.find(currentLine)
                val hrMatch = WedgeRegex.HR_TRIGGER.find(currentLine)
                val dashMatch = WedgeRegex.DASH_TRIGGER.find(currentLine)
                val listMatch = WedgeRegex.LIST_TRIGGER.find(currentLine)
                if (dateMatch != null) {
                    val d = dateMatch.groupValues[1].toInt()
                    val m = dateMatch.groupValues[2]
                    val dateResult = WedgeCalendar.getDateMath(d, m)
                    if (dateResult != null) {
                        val textBeforeLine =
                            txt.substring(0, (newCursor - 1).coerceAtLeast(0) - 3)
                        val textAfterLine =
                            txt.substring((if (txt[newCursor - 1] == ' ') newCursor else newCursor - 1).coerceAtMost(txt.length))
                        val finalNewText =
                            textBeforeLine + dateResult + textAfterLine
                        updatedValue = newValue.copy(
                            text = finalNewText,
                            selection = TextRange(newCursor + dateResult.length - 3 - (if (txt[newCursor - 1] == ' ') 1 else 0))
                        )
                    }
                } else if (mathMatch != null) {
                    val result = evaluateMathSequential(mathMatch.value)
                    if (result != null) {
                        val symbols = DecimalFormatSymbols(Locale.US)
                        val df = DecimalFormat("0.##", symbols)
                        val formattedResult = df.format(result)
                        val textBeforeLine =
                            txt.substring(0, (newCursor - 1).coerceAtLeast(0))
                        val textAfterLine =
                            txt.substring((if (txt[newCursor - 1] == ' ') newCursor else newCursor - 1).coerceAtMost(txt.length))
                        val finalNewText =
                            textBeforeLine + formattedResult + textAfterLine
                        updatedValue = newValue.copy(
                            text = finalNewText,
                            selection = TextRange(newCursor + formattedResult.length - (if (txt[newCursor - 1] == ' ') 1 else 0))
                        )
                    }
                } else if (hrMatch != null) {
                    val count = hrMatch.groupValues[1].toIntOrNull() ?: 0
                    if (count > 0) {
                        val hrline = "—".repeat(count.coerceAtMost(100))
                        val matchStart = (newCursor - 1) - hrMatch.value.length
                        val finalNewText = txt.substring(0, matchStart) + hrline + txt.substring(newCursor)
                        updatedValue = newValue.copy(
                            text = finalNewText,
                            selection = TextRange(matchStart + hrline.length)
                        )
                    }
                } else if (dashMatch != null) {
                    val counts = dashMatch.groupValues[1].toIntOrNull() ?: 0
                    if (counts > 0) {
                        val dashes = "— ".repeat(counts.coerceAtMost(100))
                        val dashStart = (newCursor - 1) - dashMatch.value.length
                        val finalNewText = txt.substring(0, dashStart) + dashes + txt.substring(newCursor)
                        updatedValue = newValue.copy(
                            text = finalNewText,
                            selection = TextRange(dashStart + dashes.length)
                        )
                    }
                } else if (listMatch != null && txt[newCursor - 1] == '\n') {
                    val ws = listMatch.groupValues[1]
                    val currentNum = listMatch.groupValues[2].toInt()
                    val prefix = "$ws${currentNum + 1}. "
                    val textBeforeLine = txt.substring(0, newCursor)
                    val textAfterLine = txt.substring(newCursor)
                    val lines = textAfterLine.split("\n").toMutableList()
                    var nextExpected = currentNum + 1
                    for (i in lines.indices) {
                        val m = WedgeRegex.LIST_TRIGGER.find(lines[i])
                        if (m != null) {
                            val lws = m.groupValues[1]
                            val foundNum = m.groupValues[2].toInt()
                            if (lws == ws && foundNum == nextExpected) {
                                lines[i] =
                                    "$lws${foundNum + 1}. " + lines[i].substring(m.range.last + 1)
                                nextExpected++
                            } else break
                        } else if (i > 0) break
                    }
                    val finalNewText =
                        textBeforeLine + prefix + lines.joinToString("\n")
                    updatedValue = newValue.copy(
                        text = finalNewText,
                        selection = TextRange(newCursor + prefix.length)
                    )
                } else if (txt[newCursor - 1] == '\n') {
                    val indent = currentLine.takeWhile { it == ' ' || it == '\t' }
                    if (indent.isNotEmpty()) {
                        val finalNewText = txt.substring(0, newCursor) + indent + txt.substring(newCursor)
                        updatedValue = newValue.copy(
                            text = finalNewText,
                            selection = TextRange(newCursor + indent.length)
                        )
                    }
                }
            } catch (_: Exception) {
            }
        }
        return EditorResult(updatedValue, currentUndoHistory, currentUndoLineIndex, currentLastDel)
    }

    private fun evaluateMathSequential(expression: String): Double? {
        try {
            val ops = expression.filter { it in "+-/*" }.map { it.toString() }
            val terms = expression.split(WedgeRegex.MATH_SPLIT).filter { it.isNotEmpty() }
            if (terms.size != ops.size + 1) return null
            var res = terms[0].toDouble()
            for (i in ops.indices) {
                val op = ops[i]
                val term = terms[i + 1]
                val isPct = term.endsWith("%")
                val num = if (isPct) term.dropLast(1).toDouble() else term.toDouble()
                val next = if (isPct) {
                    if (op == "+" || op == "-") res * (num / 100.0) else num / 100.0
                } else num
                res = when (op[0]) {
                    '+' -> res + next
                    '-' -> res - next
                    '*' -> res * next
                    '/' -> if (next == 0.0) return null else res / next
                    else -> res
                }
            }
            return res
        } catch (_: Exception) { return null }
    }
}
Updates
Shim - Android 70.026.1
Wedge - Linux 68.026.1
Wedge - Android 68.026.1
Taper - Linux 64.026.1
Ayh Extension - Chrome 63.026.1
Dev
TVShow (227) 'CSA'
TVShow (228) 'APT'
TVProgram (83) 'BXT'
Miter Update(s)
Shim (Dictation)

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)