421 lines
14 KiB
Kotlin
421 lines
14 KiB
Kotlin
package com.example.aiapp
|
|
|
|
import kotlin.test.Test
|
|
import kotlin.test.assertEquals
|
|
import kotlin.test.assertTrue
|
|
|
|
/**
|
|
* What the scanner colours, asserted as the text under each span rather than as offsets, so a
|
|
* failure prints the code that was got wrong instead of a pair of numbers.
|
|
*
|
|
* Most of these are the mistakes dev.snipme:highlights 1.1.0 made -- the library this scanner
|
|
* replaced -- measured against it directly before it was removed. They are here rather than in the
|
|
* `highlights-repro.sh` script they came from because a case that only a script can ask about is a
|
|
* case nobody asks about.
|
|
*/
|
|
class HighlighterTest {
|
|
/** Every span of [kind] in [code], as the text it covers. */
|
|
private fun spans(code: String, language: Language, kind: Kind): List<String> =
|
|
spansOf(code, language).filter { it.kind == kind }.map { code.substring(it.start, it.end) }
|
|
|
|
private fun assertSpans(
|
|
code: String,
|
|
language: Language,
|
|
kind: Kind,
|
|
vararg expected: String,
|
|
) {
|
|
assertEquals(expected.toList(), spans(code, language, kind), "$kind in: $code")
|
|
}
|
|
|
|
// The four inputs the repro script asked the library about, and what it answered.
|
|
|
|
@Test
|
|
fun `a quoted glob is one string, not a comment`() {
|
|
// The library answered a span whose end preceded its start here, which crashed the app.
|
|
assertSpans("x '*/a/*'", Language.SHELL, Kind.STRING, "'*/a/*'")
|
|
assertSpans("x '*/a/*'", Language.SHELL, Kind.COMMENT)
|
|
}
|
|
|
|
@Test
|
|
fun `a find with globs has no comment in it`() {
|
|
val code = "find . -path '*/.git/*' -prune -o -name '*.kt' -print"
|
|
assertSpans(code, Language.SHELL, Kind.STRING, "'*/.git/*'", "'*.kt'")
|
|
assertSpans(code, Language.SHELL, Kind.COMMENT)
|
|
}
|
|
|
|
@Test
|
|
fun `a URL does not comment out the rest of a shell line`() {
|
|
val code = "curl https://example.com/x && echo done"
|
|
assertSpans(code, Language.SHELL, Kind.COMMENT)
|
|
assertSpans(code, Language.SHELL, Kind.KEYWORD, "echo")
|
|
}
|
|
|
|
@Test
|
|
fun `a URL inside a Kotlin string stays a string`() {
|
|
val code = "val url = \"https://example.com\"\nfun f() = 1"
|
|
assertSpans(code, Language.KOTLIN, Kind.COMMENT)
|
|
assertSpans(code, Language.KOTLIN, Kind.STRING, "\"https://example.com\"")
|
|
assertSpans(code, Language.KOTLIN, Kind.KEYWORD, "val", "fun")
|
|
}
|
|
|
|
// Attributes, which the library greyed out as comments.
|
|
|
|
@Test
|
|
fun `a Rust attribute is metadata and the struct after it still colours`() {
|
|
val code = "#[derive(Debug)]\nstruct A { b: u8 }"
|
|
assertSpans(code, Language.RUST, Kind.METADATA, "#[derive(Debug)]")
|
|
assertSpans(code, Language.RUST, Kind.COMMENT)
|
|
assertSpans(code, Language.RUST, Kind.KEYWORD, "struct")
|
|
}
|
|
|
|
@Test
|
|
fun `an inner Rust attribute closes at its own bracket`() {
|
|
val code = "#![allow(dead_code)]\nfn f() {}"
|
|
assertSpans(code, Language.RUST, Kind.METADATA, "#![allow(dead_code)]")
|
|
assertSpans(code, Language.RUST, Kind.KEYWORD, "fn")
|
|
}
|
|
|
|
@Test
|
|
fun `a C preprocessor line is metadata rather than a comment`() {
|
|
val code = "#include <stdio.h>\nint main() { return 0; }"
|
|
assertSpans(code, Language.C, Kind.METADATA, "#include <stdio.h>")
|
|
assertSpans(code, Language.C, Kind.COMMENT)
|
|
assertSpans(code, Language.C, Kind.KEYWORD, "int", "return")
|
|
}
|
|
|
|
@Test
|
|
fun `a Kotlin annotation is metadata`() {
|
|
assertSpans("@Composable fun f() {}", Language.KOTLIN, Kind.METADATA, "@Composable")
|
|
}
|
|
|
|
// Strings whose contents the library read as code.
|
|
|
|
@Test
|
|
fun `a hash inside a Kotlin string is not a comment`() {
|
|
val code = "val c = \"#FF0000\"\nval d = 1"
|
|
assertSpans(code, Language.KOTLIN, Kind.COMMENT)
|
|
assertSpans(code, Language.KOTLIN, Kind.STRING, "\"#FF0000\"")
|
|
}
|
|
|
|
@Test
|
|
fun `an apostrophe inside a Kotlin string does not open one`() {
|
|
val code = "val a = \"don't\"\nval b = \"x\""
|
|
assertSpans(code, Language.KOTLIN, Kind.STRING, "\"don't\"", "\"x\"")
|
|
}
|
|
|
|
@Test
|
|
fun `a Rust lifetime does not open a string but a character literal does`() {
|
|
val code = "fn f<'a>(x: &'a str) { let c = 'x'; }"
|
|
assertSpans(code, Language.RUST, Kind.STRING, "'x'")
|
|
}
|
|
|
|
@Test
|
|
fun `an escaped quote is inside the Rust character literal`() {
|
|
assertSpans("let c = '\\'';", Language.RUST, Kind.STRING, "'\\''")
|
|
}
|
|
|
|
@Test
|
|
fun `a Rust raw string keeps its inner quotes`() {
|
|
val code = "let s = r#\"a \"quoted\" b\"#;"
|
|
assertSpans(code, Language.RUST, Kind.STRING, "r#\"a \"quoted\" b\"#")
|
|
}
|
|
|
|
@Test
|
|
fun `a Kotlin triple quoted string is one string`() {
|
|
assertSpans(
|
|
"val s = \"\"\"a \"b\" c\"\"\"",
|
|
Language.KOTLIN,
|
|
Kind.STRING,
|
|
"\"\"\"a \"b\" c\"\"\"",
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `a shell single quoted string takes no escapes`() {
|
|
// `\` is literal inside shell single quotes, so the string ends at the next apostrophe.
|
|
assertSpans("echo 'a\\' b", Language.SHELL, Kind.STRING, "'a\\'")
|
|
}
|
|
|
|
// Comments.
|
|
|
|
@Test
|
|
fun `Rust and Kotlin nest block comments`() {
|
|
val code = "/* a /* b */ c */ x"
|
|
assertSpans(code, Language.RUST, Kind.COMMENT, "/* a /* b */ c */")
|
|
assertSpans(code, Language.KOTLIN, Kind.COMMENT, "/* a /* b */ c */")
|
|
}
|
|
|
|
@Test
|
|
fun `C ends a block comment at the first close`() {
|
|
assertSpans("/* a /* b */ c */ x", Language.C, Kind.COMMENT, "/* a /* b */")
|
|
}
|
|
|
|
@Test
|
|
fun `a shell comment starts only at a word boundary`() {
|
|
val code = "\${#x} \$# a#b # real"
|
|
assertSpans(code, Language.SHELL, Kind.COMMENT, "# real")
|
|
}
|
|
|
|
@Test
|
|
fun `a hash anywhere is a Python comment`() {
|
|
assertSpans("x = 1 # note", Language.PYTHON, Kind.COMMENT, "# note")
|
|
}
|
|
|
|
// TOML, which the library has no rules for at all.
|
|
|
|
@Test
|
|
fun `a TOML table header is metadata and a hash in a value is not a comment`() {
|
|
val code = "[server]\ncolour = \"#FF0000\"\nport = 8080 # the real one"
|
|
assertSpans(code, Language.TOML, Kind.METADATA, "[server]")
|
|
assertSpans(code, Language.TOML, Kind.STRING, "\"#FF0000\"")
|
|
assertSpans(code, Language.TOML, Kind.COMMENT, "# the real one")
|
|
assertSpans(code, Language.TOML, Kind.LITERAL, "8080")
|
|
}
|
|
|
|
@Test
|
|
fun `a RON attribute and its values colour`() {
|
|
val code = "#![enable(implicit_some)]\n(count: 3, on: true)"
|
|
assertSpans(code, Language.RON, Kind.METADATA, "#![enable(implicit_some)]")
|
|
assertSpans(code, Language.RON, Kind.KEYWORD, "true")
|
|
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"))
|
|
assertEquals("+[-]", highlight("+[-]", fenceLanguage("brainfuck")).text)
|
|
}
|
|
|
|
@Test
|
|
fun `a diff colours changes and identifies its framing separately`() {
|
|
val code = "--- a/file\n+++ b/file\n@@ -1 +1 @@\n-old\n context\n+new"
|
|
assertSpans(code, Language.DIFF, Kind.DELETION, "-old")
|
|
assertSpans(code, Language.DIFF, Kind.ADDITION, "+new")
|
|
assertSpans(
|
|
code,
|
|
Language.DIFF,
|
|
Kind.METADATA,
|
|
"--- a/file",
|
|
"+++ b/file",
|
|
"@@ -1 +1 @@",
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun `every language the fence table knows has a scanner`() {
|
|
Language.entries.forEach { spansOf("x", it) }
|
|
}
|
|
|
|
/**
|
|
* The scanner must never throw and must never answer a span the code does not contain: the
|
|
* library's reversed range is exactly the shape that crashed a card, and a fence still being
|
|
* written is an unterminated string or comment on every keystroke.
|
|
*/
|
|
@Test
|
|
fun `spans stay inside the code for every language and every nasty input`() {
|
|
val nasty =
|
|
listOf(
|
|
"",
|
|
"'",
|
|
"\"",
|
|
"\"unterminated",
|
|
"/* unterminated",
|
|
"###",
|
|
"#",
|
|
"#
|
|
for (language in Language.entries) {
|
|
for (code in nasty) {
|
|
val spans = spansOf(code, language)
|
|
spans.forEach {
|
|
assertTrue(
|
|
it.start in 0..it.end && it.end <= code.length,
|
|
"$language answered $it for ${code.replace("\n", "\\n")}",
|
|
)
|
|
}
|
|
assertEquals(
|
|
spans.sortedBy { it.start },
|
|
spans,
|
|
"$language answered spans out of order for ${code.replace("\n", "\\n")}",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|