Files
ai-app/app/androidApp/src/main/kotlin/com/example/aiapp/FileLines.kt
T
irisandClaude Opus 5 edc39c7371 Thin the app's comments
The same pass the server had, on the Kotlin side: comments restating what
the code says are gone, and the ones recording a measurement, a constraint
or an incident are kept but cut to a few lines each. 6540 comment lines to
5674, and 920 lines off the app.

Two doc comments had drifted onto the item above the one they describe --
`contextAfter`'s onto `sessionWorking` in Events.kt, and `UsageMonitor`'s
equivalent on the server was fixed in the previous commit. Each is back on
its own item, which is the only non-comment line this diff moves.

The comments are reflowed to the column limit at their own indentation:
several were written wide, and ktfmt re-wrapped them into lines holding a
single orphan word. `/tmp` script, not kept -- ktfmt is idempotent over the
result, which is the check.

Left alone deliberately: this codebase's remaining comment density is high
because the comments carry things the code cannot say -- what a null means,
what a number was measured against, which bug a guard exists for. Of the
238 one-line doc comments in the app, five were pure restatement of the
name and were removed; the rest each say something the signature does not.

ktfmtFormat, compileDebugKotlin, lintDebug and testDebugUnitTest pass;
cargo test (127), clippy --all-targets and fmt still clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:20:16 -04:00

123 lines
5.5 KiB
Kotlin

package com.example.aiapp
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
/**
* A file split into lines, with the highlighter's colours already worked out for each one.
*
* The pure half of the viewer, so it has a JVM unit test and so [of] can run off the main thread:
* scanning a megabyte is work, and doing it inside a composable would do it on the drawing thread
* and again on every recomposition.
*
* Why per line at all: the viewer is a `LazyColumn` of lines rather than one `Text`, because text
* layout is linear in the text. That means each row needs *its* colours, and the scanner answers in
* offsets into the whole file -- so the spans are bucketed here, once, in one pass.
*/
class FileLines
private constructor(
/** The text of each line, without its newline. */
val lines: List<String>,
/** Per line, the spans that fall in it, with offsets relative to that line's start. */
private val spans: List<List<Span>>,
/**
* The longest line, in character columns -- what the viewer sizes every row to.
*
* Every row has to be the *same* width or they scroll sideways by different amounts; see
* [FileViewer]. Columns rather than measured pixels because the face is monospace, so one
* number and one character's advance give the width of the widest line without measuring twenty
* thousand strings.
*/
val columns: Int,
) {
val size: Int
get() = lines.size
/**
* One line, coloured. Built when the row is composed rather than up front: a file has far more
* lines than a screen shows, and an `AnnotatedString` per line for all of them is the cost the
* lazy list exists to avoid.
*/
fun line(index: Int): AnnotatedString {
val text = lines[index]
val here = spans[index]
if (here.isEmpty()) return AnnotatedString(text)
val palette = catppuccinSyntax()
return buildAnnotatedString {
append(text)
here.forEach { addStyle(SpanStyle(color = palette.of(it.kind)), it.start, it.end) }
}
}
companion object {
/**
* [text] scanned as [language] and cut into lines.
*
* Exactly one trailing newline is dropped before splitting, so a file that ends the way
* text files are supposed to end has the number of lines its author would count -- `wc -l`
* agrees. Without that, every well-formed file gained a phantom empty last line. An empty
* file is one empty line numbered 1, which is what it is.
*/
fun of(text: String, language: Language?): FileLines =
// Timed, and always, for the reason everything else here is: the cost of opening a
// large file is the number that decides whether the server's size limit is right, and
// an instrument that is only in the build nobody is running answers nothing.
DebugStats.timed("file scanned and cut into lines") {
val body = text.removeSuffix("\n")
val lines = body.split('\n')
val scanned = if (language == null) emptyList() else spansOf(body, language)
FileLines(lines, bucket(lines, scanned), lines.maxOf(::columnsOf))
}
/**
* How many columns a line occupies.
*
* A tab counts as eight rather than one, and deliberately upwards: this decides how far the
* viewer can scroll, and over-estimating leaves a little empty space past the longest line
* where under-estimating makes the end of that line unreachable.
*/
private fun columnsOf(line: String): Int {
var count = 0
for (character in line) count += if (character == '\t') 8 else 1
return count
}
/**
* The scanner's spans, in file offsets, as spans per line in line offsets.
*
* One walk down both lists, which is what the scanner's guarantee buys: its spans come out
* ordered, non-overlapping and inside the text. A span crossing a line break is cut at each
* break and appears in each line it covers, because a row is drawn on its own and cannot
* inherit a colour from the row above.
*/
private fun bucket(lines: List<String>, spans: List<Span>): List<List<Span>> {
val out = ArrayList<List<Span>>(lines.size)
var lineStart = 0
var next = 0
for (line in lines) {
val lineEnd = lineStart + line.length
var here: ArrayList<Span>? = null
// Spans that ended before this line begins are behind the walk for good.
while (next < spans.size && spans[next].end <= lineStart) next++
var at = next
while (at < spans.size && spans[at].start < lineEnd) {
val span = spans[at]
val start = maxOf(span.start, lineStart) - lineStart
val end = minOf(span.end, lineEnd) - lineStart
if (end > start) {
(here ?: ArrayList<Span>().also { here = it }).add(
Span(start, end, span.kind)
)
}
at++
}
out.add(here ?: emptyList())
// The newline itself, which is in the text and not in any line.
lineStart = lineEnd + 1
}
return out
}
}
}