Merge remote-tracking branch 'origin/main'

# Conflicts:
#	app/androidApp/src/main/kotlin/com/example/aiapp/Sizes.kt
This commit is contained in:
iris committed 2026-09-04 15:03:37 -04:00
commit e3e02d55f7
29 files changed
+2737 -479

No files matched your search

@@ -0,0 +1,87 @@
package com.example.aiapp
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* The line arithmetic behind the file viewer.
*
* Worth a test rather than an eye: a line number that is one out is invisible in a short file and
* obvious in a long one, and a colour that stops at a line break is invisible until the file has a
* block comment in it.
*/
class FileLinesTest {
@Test
fun `a file that ends with a newline has the number of lines its author would count`() {
assertEquals(listOf("one", "two"), FileLines.of("one\ntwo\n", null).lines)
assertEquals(listOf("one", "two"), FileLines.of("one\ntwo", null).lines)
// Only one is dropped: a blank line at the end of a file is a line somebody typed.
assertEquals(listOf("one", "two", ""), FileLines.of("one\ntwo\n\n", null).lines)
}
@Test
fun `an empty file is one empty line`() {
val lines = FileLines.of("", null)
assertEquals(1, lines.size)
assertEquals("", lines.line(0).text)
}
@Test
fun `a comment that spans lines is coloured on every line it covers`() {
val text = "fn a() {}\n/* still\n a comment */\nfn b() {}\n"
val lines = FileLines.of(text, Language.RUST)
assertEquals(4, lines.size)
val comment = catppuccinSyntax().of(Kind.COMMENT)
// The whole of the middle line, and the part of the third up to the closer.
assertTrue(lines.line(1).spanStyles.any { it.item.color == comment && it.start == 0 })
val third = lines.line(2)
assertTrue(third.spanStyles.any { it.item.color == comment && it.end == third.length })
// And the code around it is not commented.
assertTrue(lines.line(0).spanStyles.none { it.item.color == comment })
assertTrue(lines.line(3).spanStyles.none { it.item.color == comment })
}
@Test
fun `a span never runs past the line it was cut into`() {
val lines = FileLines.of("val x = \"a\nb\"\nval y = 1\n", Language.KOTLIN)
for (index in 0 until lines.size) {
val line = lines.line(index)
assertTrue(
line.spanStyles.all { it.start >= 0 && it.end <= line.length },
"line $index",
)
}
}
/**
* The number every row in the viewer is sized to. It has to be the widest line, because rows of
* their natural widths scroll sideways by different amounts -- see [FileViewer].
*/
@Test
fun `the column count is the widest line, counting a tab as eight`() {
assertEquals(5, FileLines.of("one\nthree\nx\n", null).columns)
// A tab counts up to eight, and upwards on purpose: over-estimating leaves empty space
// past the longest line, under-estimating puts its end out of reach.
assertEquals(9, FileLines.of("\tx\nshort\n", null).columns)
// An empty file is one empty line, which is no columns at all rather than an error.
assertEquals(0, FileLines.of("", null).columns)
}
@Test
fun `a file with no language is plain`() {
val lines = FileLines.of("fn main() {}\n", null)
assertTrue(lines.line(0).spanStyles.isEmpty())
}
@Test
fun `a language comes from the extension, and only from a real one`() {
assertEquals(Language.KOTLIN, fileLanguage("Main.kt"))
assertEquals(Language.KOTLIN, fileLanguage("build.gradle.kts"))
assertEquals(Language.RUST, fileLanguage("files.rs"))
assertEquals(Language.TOML, fileLanguage("Cargo.toml"))
assertEquals(null, fileLanguage("Makefile"))
assertEquals(null, fileLanguage(".bashrc"))
assertEquals(null, fileLanguage("notes.txt"))
}
}
@@ -16,9 +16,7 @@ import kotlin.test.assertTrue
class HighlighterTest {
/** Every span of [kind] in [code], as the text it covers. */
private fun spans(code: String, language: Language, kind: Kind): List<String> =
scan(code, rulesOf(language))
.filter { it.kind == kind }
.map { code.substring(it.start, it.end) }
spansOf(code, language).filter { it.kind == kind }.map { code.substring(it.start, it.end) }
private fun assertSpans(
code: String,
@@ -182,6 +180,158 @@ class HighlighterTest {
assertSpans(code, Language.RON, Kind.LITERAL, "3")
}
// Markdown, which has a scanner of its own: what a character means there is decided by where
// it sits rather than by what it is, so most of these are about the cases where it means
// nothing at all.
@Test
fun `a heading is coloured whole and a hash inside a word is not one`() {
val code = "## Layout\nissue #12 is fixed\n#hashtag"
assertSpans(code, Language.MARKDOWN, Kind.KEYWORD, "## Layout")
}
@Test
fun `seven hashes are not a heading`() {
assertSpans("####### deep", Language.MARKDOWN, Kind.KEYWORD)
}
@Test
fun `a fence carries its language as metadata and its body as one string`() {
val code = "text\n```kotlin\nval x = 1\n```\nmore"
assertSpans(code, Language.MARKDOWN, Kind.METADATA, "kotlin")
assertSpans(code, Language.MARKDOWN, Kind.STRING, "```", "val x = 1", "```")
}
/** The state that crosses a line, so the one worth asking about at both ends. */
@Test
fun `a longer fence is not closed by a shorter one, and a heading inside it is not a heading`() {
val code = "````\n```\n# not a heading\n````\nafter"
assertSpans(code, Language.MARKDOWN, Kind.KEYWORD)
assertSpans(code, Language.MARKDOWN, Kind.STRING, "````", "```", "# not a heading", "````")
}
@Test
fun `an unclosed fence runs to the end rather than throwing`() {
assertSpans("```\nstill going", Language.MARKDOWN, Kind.STRING, "```", "still going")
}
@Test
fun `list markers and quote markers colour without their text`() {
val code = "- one\n2. two\n> quoted"
assertSpans(code, Language.MARKDOWN, Kind.MARK, "-", "2.", ">")
}
@Test
fun `a rule and a setext underline are the same mark`() {
assertSpans("Title\n=====\n\n---", Language.MARKDOWN, Kind.MARK, "=====", "---")
}
@Test
fun `emphasis needs something on both sides of it`() {
assertSpans("**bold** and *thin*", Language.MARKDOWN, Kind.LITERAL, "**bold**", "*thin*")
// The case the guards exist for: a C fragment written in a paragraph.
assertSpans("a * b * c and *p = *q", Language.MARKDOWN, Kind.LITERAL)
}
@Test
fun `an underscore inside a word emphasises nothing`() {
assertSpans("snake_case_name and _real_", Language.MARKDOWN, Kind.LITERAL, "_real_")
}
@Test
fun `a code span holds a backtick when opened with two`() {
assertSpans("``a ` b`` and `c`", Language.MARKDOWN, Kind.STRING, "``a ` b``", "`c`")
}
@Test
fun `an unclosed code span is ordinary text`() {
assertSpans("a ` b", Language.MARKDOWN, Kind.STRING)
}
@Test
fun `a link marks its brackets and colours its destination`() {
val code = "see [the plan](PLAN.md) now"
assertSpans(code, Language.MARKDOWN, Kind.MARK, "[", "]")
assertSpans(code, Language.MARKDOWN, Kind.METADATA, "(PLAN.md)")
}
@Test
fun `a table is found by its delimiter row, and pipes elsewhere are plain`() {
val code = "| a | b |\n|---|---|\n| 1 | 2 |\n\nrun a | b in a paragraph"
assertSpans(
code,
Language.MARKDOWN,
Kind.MARK,
"|",
"|",
"|",
"|---|---|",
"|",
"|",
"|",
)
}
@Test
fun `a table without outer pipes still colours, and the table ends with the rows`() {
val code = "a | b\n--- | ---\nnot a row"
assertSpans(code, Language.MARKDOWN, Kind.MARK, "|", "--- | ---")
}
/**
* The tag in the last case is not an autolink and is left plain, but the address inside it is
* still an address and the bare-URL pass finds it. That is the intended reading: raw HTML is
* not something this scanner knows, and a URL is a URL wherever it was written.
*/
@Test
fun `an autolink colours and an HTML tag does not`() {
val code = "<https://example.com> and <a@b.com> and <div> and <img src=\"http://x\">"
assertSpans(
code,
Language.MARKDOWN,
Kind.METADATA,
"<https://example.com>",
"<a@b.com>",
"http://x",
)
}
@Test
fun `a bare URL gives back the sentence's punctuation`() {
assertSpans(
"see https://example.com/a., and ssh://host/x)",
Language.MARKDOWN,
Kind.METADATA,
"https://example.com/a",
"ssh://host/x",
)
}
@Test
fun `a bracket a URL opened itself stays in it`() {
assertSpans(
"https://en.wikipedia.org/wiki/A_(b) here",
Language.MARKDOWN,
Kind.METADATA,
"https://en.wikipedia.org/wiki/A_(b)",
)
}
@Test
fun `a URL inside a link destination is not coloured twice`() {
assertSpans(
"[x](https://example.com)",
Language.MARKDOWN,
Kind.METADATA,
"(https://example.com)",
)
}
@Test
fun `a bracket with no destination after it is left plain`() {
assertSpans("an [aside] here", Language.MARKDOWN, Kind.MARK)
}
@Test
fun `an unknown fence language is drawn plain`() {
assertEquals(null, fenceLanguage("brainfuck"))
@@ -189,8 +339,8 @@ class HighlighterTest {
}
@Test
fun `every alias the fence table knows has rules`() {
Language.entries.forEach { rulesOf(it) }
fun `every language the fence table knows has a scanner`() {
Language.entries.forEach { spansOf("x", it) }
}
/**
@@ -218,11 +368,26 @@ class HighlighterTest {
"0x",
"1.2.3",
"a#b//c/*d*/'e\"f",
"```",
"*",
"**",
"~~",
"> ",
"- ",
"1.",
"[x](",
"#######",
"|",
"|---|",
"<",
"<>",
"http://",
"a://",
"\n\n \n",
)
for (language in Language.entries) {
for (code in nasty) {
val spans = scan(code, rulesOf(language))
val spans = spansOf(code, language)
spans.forEach {
assertTrue(
it.start in 0..it.end && it.end <= code.length,