~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

Code search highlights its snippets, counts each kind, and shows in status

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:web/code-search-finish
.changeset/code-search.mdnew+7−0
@@ -0,0 +1,7 @@
---
default: minor
---
# Code search
Search finds code: pick Code beside Repositories, or the palette's code row, to search the default branch of every public repository, with `repo:owner/name` for one and `lang:` for a language. Each match is shown in its lines, highlighted as its file is. `knotless status` shows the server's code search index: whether it answers, and the repositories, shards and size it holds.
crates/knotless/src/status.rs+35−0
@@ -28,6 +28,22 @@ pub struct Status {
/// from servers older than the field.
#[serde(default)]
pub disk: Option<Disk>,
/// Its code search index, when it keeps one. Absent from servers older
/// than the field.
#[serde(default)]
pub code: Option<CodeIndex>,
}
/// The code search index: whether Zoekt answers, and what its shards hold.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct CodeIndex {
pub ok: bool,
pub error: Option<String>,
/// The repositories it has shards of.
pub repos: u64,
pub shards: u64,
/// The shards' size on disk.
pub bytes: u64,
}
/// The disk a server keeps its mirrors and caches on.
@@ -148,6 +164,11 @@ pub fn render(base: &str, s: &Status, out: &mut dyn Write) -> std::io::Result<()
let (ok, error) = (word(d.ok), detail(&d.error));
writeln!(out, "disk\t{}\t{ok}{state}{room}{error}", d.path)?;
}
if let Some(c) = &s.code {
let (ok, error) = (word(c.ok), detail(&c.error));
let held = format!("{} repos in {} shards, {}", c.repos, c.shards, mb(c.bytes));
writeln!(out, "code\t{ok}, {held}{error}")?;
}
Ok(())
}
@@ -167,6 +188,11 @@ fn gb(bytes: u64) -> String {
format!("{}G", bytes / 1_000_000_000)
}
/// Bytes, in whole megabytes.
fn mb(bytes: u64) -> String {
format!("{}M", bytes / 1_000_000)
}
/// A span of seconds, short.
fn ago(secs: u64) -> String {
match secs {
@@ -219,6 +245,13 @@ mod tests {
total_bytes: 150_000_000_000,
error: None,
}),
code: Some(CodeIndex {
ok: false,
error: Some("connection refused".into()),
repos: 21,
shards: 23,
bytes: 312_400_000,
}),
};
let text = |s: &Status| {
let mut out = Vec::new();
@@ -234,6 +267,7 @@ feed\tfirehose\tFAILING\tdisconnected, last event 2h ago (stale after 10m), 7 ev
index\tFAILING: file is not a database
mirror\ta.test/r\tok at 0123456789ab, 3 packs
disk\t/data/cache\tok (low), 18G free of 150G
code\tFAILING, 21 repos in 23 shards, 312M: connection refused
"
);
s = Status {
@@ -257,6 +291,7 @@ disk\t/data/cache\tok (low), 18G free of 150G
total_bytes: 0,
error: Some("no such directory".into()),
}),
code: None,
};
assert_eq!(
text(&s),
crates/knotless-serve/src/code.rs+87−7
@@ -21,6 +21,7 @@ use crate::{App, AppError};
use anyhow::{Context, Result};
use base64::Engine;
use futures::StreamExt;
use knotless::status::CodeIndex;
use knotless::telemetry::{self, Fields, Level};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -64,6 +65,11 @@ pub struct Chunk {
/// Its lines, joined by newlines, with none at the end.
pub content: String,
pub ranges: Vec<Found>,
/// What its highlighting paints over `content`, as a file's is
/// encoded ([`crate::highlight::Ranges::encode`]): the lines read
/// alone, without the rest of their file.
#[serde(skip_serializing_if = "Option::is_none")]
pub highlights: Option<String>,
}
/// What was found on one line, by UTF-8 bytes into it.
@@ -206,6 +212,36 @@ impl Code {
entries.map(|e| e.path()).filter(named).collect()
}
/// The index as `/_status` tells of it: whether Zoekt answers, and
/// the repositories, shards and bytes it holds.
pub async fn check(&self) -> CodeIndex {
let mut repos = std::collections::BTreeSet::new();
let (mut shards, mut bytes) = (0, 0);
for entry in std::fs::read_dir(&self.dir).into_iter().flatten().flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
// Its `meta` directory is no shard.
let Some((repo, _)) = name
.strip_suffix(".zoekt")
.and_then(|n| n.rsplit_once("_v"))
else {
continue;
};
repos.insert(repo.to_string());
shards += 1;
bytes += entry.metadata().map_or(0, |m| m.len());
}
let health = format!("{}/healthz", self.url);
let answered = self.http.get(health).send().await;
let error = answered.and_then(|r| r.error_for_status()).err();
CodeIndex {
ok: error.is_none(),
error: error.as_ref().map(ToString::to_string),
repos: repos.len() as u64,
shards,
bytes,
}
}
/// Whether `did`'s repository with `key` has shards.
pub fn has(&self, did: &str, key: &str) -> bool {
!self.shards(did, key).is_empty()
@@ -279,11 +315,13 @@ impl Code {
fn hit(file: ZFile) -> Option<Hit> {
let (did, key) = file.repository.rsplit_once('/')?;
let chunks = file.chunk_matches.unwrap_or_default();
let chunks = chunks
let mut chunks: Vec<Chunk> = chunks
.into_iter()
.filter(|c| !c.file_name)
.map(chunk)
.map(|c| chunk(c, &file.file_name))
.collect();
// Zoekt answers them best first: shown as the file reads.
chunks.sort_by_key(|c| c.start_line);
Some(Hit {
did: did.to_string(),
key: key.to_string(),
@@ -296,7 +334,7 @@ fn hit(file: ZFile) -> Option<Hit> {
/// A chunk's lines, and what was found, by line, out of Zoekt's offsets
/// into the file.
fn chunk(c: ZChunk) -> Chunk {
fn chunk(c: ZChunk, path: &str) -> Chunk {
let bytes = base64::engine::general_purpose::STANDARD
.decode(&c.content)
.unwrap_or_default();
@@ -324,9 +362,12 @@ fn chunk(c: ZChunk) -> Chunk {
}
}
}
let content = String::from_utf8_lossy(bytes).into_owned();
let painted = crate::highlight::code(path, &content);
Chunk {
start_line: first,
content: String::from_utf8_lossy(bytes).into_owned(),
highlights: painted.as_deref().map(crate::highlight::Ranges::encode),
content,
ranges,
}
}
@@ -402,6 +443,17 @@ async fn reindex(app: &App, code: &Code, did: &str, key: &str) -> Result<()> {
code.index(did, key, &git.dir).await
}
/// Zoekt over `dir`, as if its webserver had died: nothing answers.
#[cfg(test)]
pub(crate) fn dead(dir: &Path) -> Code {
Code {
dir: dir.to_path_buf(),
url: "http://127.0.0.1:1".into(),
http: reqwest::Client::new(),
_server: tokio::process::Command::new("true").spawn().unwrap(),
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -421,8 +473,8 @@ mod tests {
byte_offset,
line_number,
};
let c = chunk(ZChunk {
content,
let zc = ZChunk {
content: content.clone(),
// The chunk starts at byte 100, line 7, of its file.
content_start: at(100, 7),
// `a` in line 7, and from its `{}` to the end of the next line's `abc`.
@@ -437,7 +489,8 @@ mod tests {
},
],
file_name: false,
});
};
let c = chunk(zc, "src/a.rs");
assert_eq!(c.start_line, 7);
assert_eq!(c.content, "fn a() {}\nlet abc = a();");
let found = |line, byte_start, byte_end| Found {
@@ -446,6 +499,33 @@ mod tests {
byte_end,
};
assert_eq!(c.ranges, [found(7, 3, 4), found(7, 7, 9), found(8, 0, 7)]);
// Painted as Rust reads, by its path: `fn` and `let` keywords.
assert!(c.highlights.unwrap().contains("keyword:0,2,"));
// A file no grammar here reads is painted not at all.
let plain = ZChunk {
content,
content_start: at(0, 1),
ranges: vec![],
file_name: false,
};
assert_eq!(chunk(plain, "notes.txt").highlights, None);
// A file's chunks, in the order of its lines.
let at_line = |line| ZChunk {
content: base64::engine::general_purpose::STANDARD.encode("x\n"),
content_start: at(0, line),
ranges: vec![],
file_name: false,
};
let found = hit(ZFile {
file_name: "a".into(),
repository: "did:plc:a/k".into(),
language: String::new(),
version: String::new(),
chunk_matches: Some(vec![at_line(30), at_line(2)]),
})
.unwrap();
let lines: Vec<u32> = found.chunks.iter().map(|c| c.start_line).collect();
assert_eq!(lines, [2, 30]);
assert!(
hit(ZFile {
file_name: "a".into(),
crates/knotless-serve/src/status.rs+32−1
@@ -213,7 +213,15 @@ impl App {
problems.extend(failed.into_iter().flatten());
let disk = self.disk.as_deref().map(disk);
problems.extend(disk.as_ref().and_then(disk_problem));
let parts = index.as_ref().is_none_or(|i| i.ok) && mirror.as_ref().is_none_or(|m| m.ok);
let code = match self.code.as_deref() {
Some(code) => Some(code.check().await),
None => None,
};
let failing = code.as_ref().and_then(|c| c.error.as_deref());
problems.extend(failing.map(|e| format!("code: {e}")));
let parts = index.as_ref().is_none_or(|i| i.ok)
&& mirror.as_ref().is_none_or(|m| m.ok)
&& code.as_ref().is_none_or(|c| c.ok);
Status {
ok: fed && parts && disk.as_ref().is_none_or(|d| d.ok),
problems,
@@ -222,6 +230,7 @@ impl App {
index,
mirror,
disk,
code,
}
}
}
@@ -416,6 +425,28 @@ mod tests {
assert!(d.error.unwrap().contains(": "));
}
#[tokio::test]
async fn code() {
// Zoekt gone: told of, and failing.
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("meta")).unwrap();
for shard in [
"a_2fk_v16.00000.zoekt",
"a_2fk_v16.00001.zoekt",
"b_2fk_v16.00000.zoekt",
] {
std::fs::write(dir.path().join(shard), "1234").unwrap();
}
let mut app = app(None);
let code = crate::code::dead(dir.path());
Arc::get_mut(&mut app).unwrap().code = Some(Arc::new(code));
let s = app.status(None).await;
assert!(!s.ok);
let c = s.code.unwrap();
assert_eq!((c.ok, c.repos, c.shards, c.bytes), (false, 2, 3, 12));
assert_eq!(s.problems, [format!("code: {}", c.error.unwrap())]);
}
#[tokio::test]
async fn index() {
let dir = tempfile::tempdir().unwrap();
crates/knotless-serve/tests/web.rs+8−0
@@ -3503,6 +3503,9 @@ fn searching_code() {
chunk["ranges"],
serde_json::json!([{"line": 2, "byteStart": 3, "byteEnd": 11}])
);
// Painted as its file's language reads: the comment first.
let painted = chunk["highlights"].as_str().unwrap();
assert!(painted.starts_with("comment:0,"), "{painted}");
// In one repository, by its record's URI.
let within = |name: &str| {
@@ -3538,6 +3541,11 @@ fn searching_code() {
names.filter(mine).count()
};
assert_eq!(shards(), 2);
// `/_status` tells of the index.
let (_, status) = site.status("/_status");
let status: Value = serde_json::from_str(&status).unwrap();
assert_eq!(status["code"]["ok"], true, "{status}");
assert!(status["code"]["shards"].as_u64().unwrap() >= 2, "{status}");
ok(&s, &["delete", &s.url("needle-one"), "--yes"]);
wait_until(|| found("needle_1") == 0);
// And its shards are deleted.
deploy/README.md+3−0
@@ -613,6 +613,9 @@ the host to poll: 200 when it is, 503 and what failed when not.
needed a mirror answered 502 (`No space left on device`); 15% of its
150 GB is a day or more of deploys and runs to clear it in. A keyword
monitor (step 4 below) shows a `low` disk before it fails.
- **Code**, with `--code-index`: Zoekt's webserver answers, and the
repositories, shards and bytes its index holds show. A webserver that
died fails here, as every code search then fails.
It takes milliseconds, so polling it every minute costs nothing. A dead
upstream feed is not a reason to restart serve, so the compose healthcheck
docs/design/serve.md+7−2
@@ -419,7 +419,10 @@ repository deleted has its shards deleted when its turn comes.
`dev.knotless.searchCode` asks Zoekt and answers only files of
repositories the index still lists and has not taken down. So a takedown
hides a repository's code at once, before its shards are gone. Private
repositories are never indexed, as the index never sees them.
repositories are never indexed, as the index never sees them. Each run
of lines answered is highlighted as a file is, by its path's language,
the lines read alone: one that starts inside a long comment or string
can be painted as code.
### Shared steps
@@ -1472,7 +1475,9 @@ no reason to restart.
`/_status` says whether the server is doing its job: each feed is connected and
has brought an event recently, the index answers a read, and with `?repo=` that
repository's mirror reads as its code page would, and the disk its cache is on
has room: under 15% free it is told of, under 5% it fails. It answers 200 when
has room: under 15% free it is told of, under 5% it fails. With
`--code-index`, Zoekt answers too, and the repositories, shards and bytes
its index holds are shown. It answers 200 when
all hold and 503 otherwise. One feed bringing events is enough, though dead feeds are
still listed.
[../../deploy/README.md#monitoring](../../deploy/README.md#monitoring) says
docs/reference/api.md+2−1
@@ -167,7 +167,8 @@ Answers `files`, the best first, each with its `repo` (as `listRepos`
answers one), `path`, `language?`, `commit?` (the commit of the default
branch that was indexed, which its lines are of) and `chunks`: runs of lines holding
matches, each its `startLine`, its `content` and its `ranges`, a match
by `line`, `byteStart` and `byteEnd` in UTF-8 bytes. `fileCount` and
by `line`, `byteStart` and `byteEnd` in UTF-8 bytes, and its
`highlights?`, encoded as `getFile`'s `ranges` are. `fileCount` and
`matchCount` count all that was found. Only default branches are
searched, and taken-down owners and repositories are left out. A query
Zoekt cannot read answers 400, and a server without `--code-index` 501.
docs/reference/cli.md+1−1
@@ -351,7 +351,7 @@ MCP, `merge` takes `wait` (at most 300 s) and `force`.
| `admin reports [<limit>]` | as the operator, the reports received, newest first (default 50): a line per report of `<id>`, `<reporter did>`, `<reason>`, `<subject>`, `<text>`, tab-separated |
| `admin stats [--days <n>] [--json]` | as the operator, how the appview is used, day by day, from `dev.knotless.getUsage` (14 days by default) |
| `admin logs [--trace <id>] [--message <m>] [--level <l>] [--min-ms <n>] [--limit <n>]` | as the operator, the lines the appview logged, from `dev.knotless.getLogs`, one JSON line each, oldest first: a request's by its trace (up to 500, unless `--limit` says fewer), or the last (100 by default, at most 500) of a message, a level and above, or a time; one of them is needed |
| `status [<server>] [--repo <handle>/<name>] [--json]` | a `knotless-serve`'s health, from `/_status`: its feeds, index, a mirror and its disk's room; the server defaults to `KNOTLESS_APPVIEW` |
| `status [<server>] [--repo <handle>/<name>] [--json]` | a `knotless-serve`'s health, from `/_status`: its feeds, index, a mirror, its disk's room and its code search index; the server defaults to `KNOTLESS_APPVIEW` |
A report's reason is sent as `com.atproto.moderation.defs#reason<Name>`,
as `reasonSpam`. Its subject is a `com.atproto.admin.defs#repoRef` for a
harness/shots/fixtures.json+34−1
@@ -52,7 +52,9 @@
"releases": "/alice.test/tally/releases",
"releases-image": "/alice.test/tally/releases?package=tally-server",
"releases-empty": "/bob.test/tally/releases",
"notifications": "/notifications"
"notifications": "/notifications",
"code": "/search?type=code&q=count",
"code-repo": "/search?type=code&q=count+repo:alice.test/tally"
},
"answers": {
"/alice.test/tally/raw/main/assets/logo.svg": {
@@ -554,6 +556,37 @@
"content-type": "application/json"
},
"body": "{\"did\":\"did:plc:2ejydq2mzgqz2tzdtgk3figi\",\"handle\":\"textkit.test\",\"displayName\":\"Textkit\",\"description\":\"Tools for text, made together.\",\"followersCount\":0,\"followsCount\":0,\"organization\":true,\"badges\":[{\"kind\":\"carry-your-own\",\"tier\":1,\"count\":1}]}"
},
"/xrpc/dev.knotless.listRepos?q=count": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": "{\"repos\":[{\"createdAt\":\"2026-10-09T16:47:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"forkOf\":\"at://did:plc:abe24xewga4qdbzjcxmuzdt3/dev.knotless.repo/tally\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"handle\":\"bob.test\"}},{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}]}"
},
"/xrpc/dev.knotless.searchCode?q=count": {
"status": 200,
"headers": {
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"fileCount\":12,\"files\":[{\"chunks\":[{\"content\":\"//! Counting, kept apart from reading so it can be tested.\\n\",\"highlights\":\"comment:0,59\",\"ranges\":[{\"byteEnd\":9,\"byteStart\":4,\"line\":1}],\"startLine\":1},{\"content\":\"#[derive(Debug, Default, PartialEq)]\\npub struct Counts {\\n pub lines: usize,\",\"highlights\":\"keyword:37,3,1,6,14,3;type:9,5,2,7,2,9,14,6,18,5;attr:0,9,5,2,7,2,9,2\",\"ranges\":[{\"byteEnd\":16,\"byteStart\":11,\"line\":7}],\"startLine\":6},{\"content\":\"\\n/// Counts `text` in one pass.\\npub fn count(text: &str) -> Counts {\\n let mut counts = Counts { bytes: text.len(), ..Counts::default() };\\n for line in parse::lines(text) {\\n counts.lines += 1;\\n counts.words += parse::words(line);\\n }\\n counts\\n}\",\"highlights\":\"comment:1,31;keyword:32,3,1,2,35,3,1,3,65,3,6,2;number:202,1;function:39,5,67,3,14,7,29,5,67,5;type:52,3,5,6,24,6,24,6\",\"ranges\":[{\"byteEnd\":9,\"byteStart\":4,\"line\":13},{\"byteEnd\":12,\"byteStart\":7,\"line\":14},{\"byteEnd\":33,\"byteStart\":28,\"line\":14},{\"byteEnd\":17,\"byteStart\":12,\"line\":15},{\"byteEnd\":26,\"byteStart\":21,\"line\":15},{\"byteEnd\":56,\"byteStart\":51,\"line\":15},{\"byteEnd\":13,\"byteStart\":8,\"line\":17},{\"byteEnd\":13,\"byteStart\":8,\"line\":18},{\"byteEnd\":9,\"byteStart\":4,\"line\":20}],\"startLine\":12},{\"content\":\" #[test]\\n fn counts() {\\n let c = count(\\\"one two\\\\nthree\\\\n\\\");\\n assert_eq!(c, Counts { lines: 2, words: 3, bytes: 14 });\\n }\",\"highlights\":\"string:52,18;keyword:16,2,20,3;number:111,1,9,1,9,2;function:19,6,21,5,30,10;type:95,6;attr:4,7\",\"ranges\":[{\"byteEnd\":12,\"byteStart\":7,\"line\":28},{\"byteEnd\":21,\"byteStart\":16,\"line\":29},{\"byteEnd\":27,\"byteStart\":22,\"line\":30}],\"startLine\":27},{\"content\":\" fn a_last_line_without_a_newline() {\\n assert_eq!(count(\\\"a\\\\nb\\\").lines, 2);\\n }\",\"highlights\":\"string:66,6;keyword:4,2;number:81,1;function:7,29,13,10\",\"ranges\":[{\"byteEnd\":24,\"byteStart\":19,\"line\":35}],\"startLine\":34}],\"commit\":\"4c4f3483836ee19f1f64f7976d1bc44ebc9db642\",\"language\":\"Rust\",\"path\":\"src/lib.rs\",\"repo\":{\"createdAt\":\"2026-10-09T16:47:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"forkOf\":\"at://did:plc:abe24xewga4qdbzjcxmuzdt3/dev.knotless.repo/tally\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"handle\":\"bob.test\"}}},{\"chunks\":[{\"content\":\"//! Counting, kept apart from reading so it can be tested.\\n\",\"highlights\":\"comment:0,59\",\"ranges\":[{\"byteEnd\":9,\"byteStart\":4,\"line\":1}],\"startLine\":1},{\"content\":\"#[derive(Debug, Default, PartialEq)]\\npub struct Counts {\\n pub lines: usize,\",\"highlights\":\"keyword:37,3,1,6,14,3;type:9,5,2,7,2,9,14,6,18,5;attr:0,9,5,2,7,2,9,2\",\"ranges\":[{\"byteEnd\":16,\"byteStart\":11,\"line\":7}],\"startLine\":6},{\"content\":\"\\n/// Counts `text` in one pass.\\npub fn count(text: &str) -> Counts {\\n let mut counts = Counts { bytes: text.len(), ..Counts::default() };\\n for line in parse::lines(text) {\\n counts.lines += 1;\\n counts.words += parse::words(line);\\n }\\n counts\\n}\",\"highlights\":\"comment:1,31;keyword:32,3,1,2,35,3,1,3,65,3,6,2;number:202,1;function:39,5,67,3,14,7,29,5,67,5;type:52,3,5,6,24,6,24,6\",\"ranges\":[{\"byteEnd\":9,\"byteStart\":4,\"line\":13},{\"byteEnd\":12,\"byteStart\":7,\"line\":14},{\"byteEnd\":33,\"byteStart\":28,\"line\":14},{\"byteEnd\":17,\"byteStart\":12,\"line\":15},{\"byteEnd\":26,\"byteStart\":21,\"line\":15},{\"byteEnd\":56,\"byteStart\":51,\"line\":15},{\"byteEnd\":13,\"byteStart\":8,\"line\":17},{\"byteEnd\":13,\"byteStart\":8,\"line\":18},{\"byteEnd\":9,\"byteStart\":4,\"line\":20}],\"startLine\":12},{\"content\":\" #[test]\\n fn counts() {\\n let c = count(\\\"one two\\\\nthree\\\\n\\\");\\n assert_eq!(c, Counts { lines: 2, words: 3, bytes: 14 });\\n }\",\"highlights\":\"string:52,18;keyword:16,2,20,3;number:111,1,9,1,9,2;function:19,6,21,5,30,10;type:95,6;attr:4,7\",\"ranges\":[{\"byteEnd\":12,\"byteStart\":7,\"line\":28},{\"byteEnd\":21,\"byteStart\":16,\"line\":29},{\"byteEnd\":27,\"byteStart\":22,\"line\":30}],\"startLine\":27},{\"content\":\" fn a_last_line_without_a_newline() {\\n assert_eq!(count(\\\"a\\\\nb\\\").lines, 2);\\n }\",\"highlights\":\"string:66,6;keyword:4,2;number:81,1;function:7,29,13,10\",\"ranges\":[{\"byteEnd\":24,\"byteStart\":19,\"line\":35}],\"startLine\":34}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"Rust\",\"path\":\"src/lib.rs\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}},{\"chunks\":[{\"content\":\"\\ntally reads the whole input before counting. Inputs are small, and one\\npass over a string is simpler than a streaming reader.\",\"highlights\":\"\",\"ranges\":[{\"byteEnd\":40,\"byteStart\":35,\"line\":3}],\"startLine\":2},{\"content\":\" input[File or stdin] --> read[Read it whole]\\n read --> count{Count}\\n count --> lines\\n count --> words\\n count --> bytes\\n lines & words & bytes --> row[One row, lined up]\",\"highlights\":\"attr:8,13,11,13,112,17\",\"ranges\":[{\"byteEnd\":16,\"byteStart\":11,\"line\":9},{\"byteEnd\":22,\"byteStart\":17,\"line\":9},{\"byteEnd\":7,\"byteStart\":2,\"line\":10},{\"byteEnd\":7,\"byteStart\":2,\"line\":11},{\"byteEnd\":7,\"byteStart\":2,\"line\":12}],\"startLine\":8}],\"commit\":\"4c4f3483836ee19f1f64f7976d1bc44ebc9db642\",\"language\":\"Markdown\",\"path\":\"docs/DESIGN.md\",\"repo\":{\"createdAt\":\"2026-10-09T16:47:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"forkOf\":\"at://did:plc:abe24xewga4qdbzjcxmuzdt3/dev.knotless.repo/tally\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"handle\":\"bob.test\"}}},{\"chunks\":[{\"content\":\"use std::io::Read;\\nuse tally::count;\\n\",\"highlights\":\"keyword:0,3,16,3;type:13,4\",\"ranges\":[{\"byteEnd\":16,\"byteStart\":11,\"line\":2}],\"startLine\":1},{\"content\":\" };\\n let c = count(&text);\\n let name = path.as_deref().unwrap_or(\\\"-\\\");\",\"highlights\":\"string:74,3;keyword:11,3,23,3;function:19,5,29,8,3,9\",\"ranges\":[{\"byteEnd\":17,\"byteStart\":12,\"line\":14}],\"startLine\":13}],\"commit\":\"4c4f3483836ee19f1f64f7976d1bc44ebc9db642\",\"language\":\"Rust\",\"path\":\"src/main.rs\",\"repo\":{\"createdAt\":\"2026-10-09T16:47:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"forkOf\":\"at://did:plc:abe24xewga4qdbzjcxmuzdt3/dev.knotless.repo/tally\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"handle\":\"bob.test\"}}},{\"chunks\":[{\"content\":\"use std::io::Read;\\nuse tally::count;\\n\",\"highlights\":\"keyword:0,3,16,3;type:13,4\",\"ranges\":[{\"byteEnd\":16,\"byteStart\":11,\"line\":2}],\"startLine\":1},{\"content\":\" };\\n let c = count(&text);\\n let name = path.as_deref().unwrap_or(\\\"-\\\");\",\"highlights\":\"string:74,3;keyword:11,3,23,3;function:19,5,29,8,3,9\",\"ranges\":[{\"byteEnd\":17,\"byteStart\":12,\"line\":14}],\"startLine\":13}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"Rust\",\"path\":\"src/main.rs\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}},{\"chunks\":[{\"content\":\"//! Splitting text into the things tally counts.\\n\",\"highlights\":\"comment:0,49\",\"ranges\":[{\"byteEnd\":46,\"byteStart\":41,\"line\":1}],\"startLine\":1},{\"content\":\"pub fn words(line: &str) -> usize {\\n line.split_whitespace().count()\\n}\",\"highlights\":\"keyword:0,3,1,2;function:7,5,33,16,3,5;type:20,3,5,5\",\"ranges\":[{\"byteEnd\":33,\"byteStart\":28,\"line\":5}],\"startLine\":4},{\"content\":\"\\n/// The lines of `text`. A last line without a newline still counts.\\npub fn lines(text: &str) -> impl Iterator<Item = &str> {\",\"highlights\":\"comment:1,69;keyword:70,3,1,2,22,4;type:90,3,10,8,1,4,4,3\",\"ranges\":[{\"byteEnd\":66,\"byteStart\":61,\"line\":8}],\"startLine\":7}],\"commit\":\"4c4f3483836ee19f1f64f7976d1bc44ebc9db642\",\"language\":\"Rust\",\"path\":\"src/parse.rs\",\"repo\":{\"createdAt\":\"2026-10-09T16:47:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"forkOf\":\"at://did:plc:abe24xewga4qdbzjcxmuzdt3/dev.knotless.repo/tally\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"handle\":\"bob.test\"}}},{\"chunks\":[{\"content\":\"//! Splitting text into the things tally counts.\\n\",\"highlights\":\"comment:0,49\",\"ranges\":[{\"byteEnd\":46,\"byteStart\":41,\"line\":1}],\"startLine\":1},{\"content\":\"pub fn words(line: &str) -> usize {\\n line.split_whitespace().count()\\n}\",\"highlights\":\"keyword:0,3,1,2;function:7,5,33,16,3,5;type:20,3,5,5\",\"ranges\":[{\"byteEnd\":33,\"byteStart\":28,\"line\":5}],\"startLine\":4},{\"content\":\"\\n/// The lines of `text`. A last line without a newline still counts, and\\n/// empty input has none.\",\"highlights\":\"comment:1,98\",\"ranges\":[{\"byteEnd\":66,\"byteStart\":61,\"line\":8}],\"startLine\":7},{\"content\":\" fn empty_input_has_no_lines() {\\n assert_eq!(super::lines(\\\"\\\").count(), 0);\\n }\",\"highlights\":\"string:68,2;keyword:4,2,49,5;number:81,1;function:7,24,13,10\",\"ranges\":[{\"byteEnd\":41,\"byteStart\":36,\"line\":18}],\"startLine\":17}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"Rust\",\"path\":\"src/parse.rs\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}},{\"chunks\":[{\"content\":\"\\ntally reads the whole input before counting. Inputs are small, and one\\npass over a string is simpler than a streaming reader.\",\"highlights\":\"\",\"ranges\":[{\"byteEnd\":40,\"byteStart\":35,\"line\":3}],\"startLine\":2},{\"content\":\" input[File or stdin] --> read[Read it whole]\\n read --> count{Count}\\n count --> lines\\n count --> words\\n count --> bytes\\n lines & words & bytes --> row[One row, lined up]\",\"highlights\":\"attr:8,13,11,13,112,17\",\"ranges\":[{\"byteEnd\":16,\"byteStart\":11,\"line\":9},{\"byteEnd\":22,\"byteStart\":17,\"line\":9},{\"byteEnd\":7,\"byteStart\":2,\"line\":10},{\"byteEnd\":7,\"byteStart\":2,\"line\":11},{\"byteEnd\":7,\"byteStart\":2,\"line\":12}],\"startLine\":8}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"Markdown\",\"path\":\"docs/DESIGN.md\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}},{\"chunks\":[{\"content\":\"\\nCounts lines, words and bytes, like `wc`, with output that lines up.\\n\",\"highlights\":\"string:37,4\",\"ranges\":[{\"byteEnd\":5,\"byteStart\":0,\"line\":3}],\"startLine\":2}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"Markdown\",\"path\":\"README.md\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}},{\"chunks\":[{\"content\":\"\\nCounts lines, words and bytes, like `wc`, with output that lines up.\\n\",\"highlights\":\"string:37,4\",\"ranges\":[{\"byteEnd\":5,\"byteStart\":0,\"line\":3}],\"startLine\":2}],\"commit\":\"4c4f3483836ee19f1f64f7976d1bc44ebc9db642\",\"language\":\"Markdown\",\"path\":\"README.md\",\"repo\":{\"createdAt\":\"2026-10-09T16:47:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"forkOf\":\"at://did:plc:abe24xewga4qdbzjcxmuzdt3/dev.knotless.repo/tally\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"handle\":\"bob.test\"}}},{\"chunks\":[{\"content\":\"edition = \\\"2024\\\"\\ndescription = \\\"Counts lines, words and bytes\\\"\\nlicense = \\\"MIT\\\"\",\"highlights\":\"string:10,6,15,31,11,5;type:0,7,10,11,35,7\",\"ranges\":[{\"byteEnd\":20,\"byteStart\":15,\"line\":5}],\"startLine\":4}],\"commit\":\"4c4f3483836ee19f1f64f7976d1bc44ebc9db642\",\"language\":\"TOML\",\"path\":\"Cargo.toml\",\"repo\":{\"createdAt\":\"2026-10-09T16:47:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"forkOf\":\"at://did:plc:abe24xewga4qdbzjcxmuzdt3/dev.knotless.repo/tally\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:55j6e4wxzlzndxm4nov2lz7x\",\"handle\":\"bob.test\"}}},{\"chunks\":[{\"content\":\"edition = \\\"2024\\\"\\ndescription = \\\"Counts lines, words and bytes\\\"\\nlicense = \\\"MIT\\\"\",\"highlights\":\"string:10,6,15,31,11,5;type:0,7,10,11,35,7\",\"ranges\":[{\"byteEnd\":20,\"byteStart\":15,\"line\":5}],\"startLine\":4}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"TOML\",\"path\":\"Cargo.toml\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}}],\"matchCount\":57}"
},
"/xrpc/dev.knotless.searchCode?q=count&repo=at%3A%2F%2Falice.test%2Fdev.knotless.repo%2Ftally": {
"status": 200,
"headers": {
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"fileCount\":6,\"files\":[{\"chunks\":[{\"content\":\"//! Counting, kept apart from reading so it can be tested.\\n\",\"highlights\":\"comment:0,59\",\"ranges\":[{\"byteEnd\":9,\"byteStart\":4,\"line\":1}],\"startLine\":1},{\"content\":\"#[derive(Debug, Default, PartialEq)]\\npub struct Counts {\\n pub lines: usize,\",\"highlights\":\"keyword:37,3,1,6,14,3;type:9,5,2,7,2,9,14,6,18,5;attr:0,9,5,2,7,2,9,2\",\"ranges\":[{\"byteEnd\":16,\"byteStart\":11,\"line\":7}],\"startLine\":6},{\"content\":\"\\n/// Counts `text` in one pass.\\npub fn count(text: &str) -> Counts {\\n let mut counts = Counts { bytes: text.len(), ..Counts::default() };\\n for line in parse::lines(text) {\\n counts.lines += 1;\\n counts.words += parse::words(line);\\n }\\n counts\\n}\",\"highlights\":\"comment:1,31;keyword:32,3,1,2,35,3,1,3,65,3,6,2;number:202,1;function:39,5,67,3,14,7,29,5,67,5;type:52,3,5,6,24,6,24,6\",\"ranges\":[{\"byteEnd\":9,\"byteStart\":4,\"line\":13},{\"byteEnd\":12,\"byteStart\":7,\"line\":14},{\"byteEnd\":33,\"byteStart\":28,\"line\":14},{\"byteEnd\":17,\"byteStart\":12,\"line\":15},{\"byteEnd\":26,\"byteStart\":21,\"line\":15},{\"byteEnd\":56,\"byteStart\":51,\"line\":15},{\"byteEnd\":13,\"byteStart\":8,\"line\":17},{\"byteEnd\":13,\"byteStart\":8,\"line\":18},{\"byteEnd\":9,\"byteStart\":4,\"line\":20}],\"startLine\":12},{\"content\":\" #[test]\\n fn counts() {\\n let c = count(\\\"one two\\\\nthree\\\\n\\\");\\n assert_eq!(c, Counts { lines: 2, words: 3, bytes: 14 });\\n }\",\"highlights\":\"string:52,18;keyword:16,2,20,3;number:111,1,9,1,9,2;function:19,6,21,5,30,10;type:95,6;attr:4,7\",\"ranges\":[{\"byteEnd\":12,\"byteStart\":7,\"line\":28},{\"byteEnd\":21,\"byteStart\":16,\"line\":29},{\"byteEnd\":27,\"byteStart\":22,\"line\":30}],\"startLine\":27},{\"content\":\" fn a_last_line_without_a_newline() {\\n assert_eq!(count(\\\"a\\\\nb\\\").lines, 2);\\n }\",\"highlights\":\"string:66,6;keyword:4,2;number:81,1;function:7,29,13,10\",\"ranges\":[{\"byteEnd\":24,\"byteStart\":19,\"line\":35}],\"startLine\":34}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"Rust\",\"path\":\"src/lib.rs\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}},{\"chunks\":[{\"content\":\"use std::io::Read;\\nuse tally::count;\\n\",\"highlights\":\"keyword:0,3,16,3;type:13,4\",\"ranges\":[{\"byteEnd\":16,\"byteStart\":11,\"line\":2}],\"startLine\":1},{\"content\":\" };\\n let c = count(&text);\\n let name = path.as_deref().unwrap_or(\\\"-\\\");\",\"highlights\":\"string:74,3;keyword:11,3,23,3;function:19,5,29,8,3,9\",\"ranges\":[{\"byteEnd\":17,\"byteStart\":12,\"line\":14}],\"startLine\":13}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"Rust\",\"path\":\"src/main.rs\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}},{\"chunks\":[{\"content\":\"\\ntally reads the whole input before counting. Inputs are small, and one\\npass over a string is simpler than a streaming reader.\",\"highlights\":\"\",\"ranges\":[{\"byteEnd\":40,\"byteStart\":35,\"line\":3}],\"startLine\":2},{\"content\":\" input[File or stdin] --> read[Read it whole]\\n read --> count{Count}\\n count --> lines\\n count --> words\\n count --> bytes\\n lines & words & bytes --> row[One row, lined up]\",\"highlights\":\"attr:8,13,11,13,112,17\",\"ranges\":[{\"byteEnd\":16,\"byteStart\":11,\"line\":9},{\"byteEnd\":22,\"byteStart\":17,\"line\":9},{\"byteEnd\":7,\"byteStart\":2,\"line\":10},{\"byteEnd\":7,\"byteStart\":2,\"line\":11},{\"byteEnd\":7,\"byteStart\":2,\"line\":12}],\"startLine\":8}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"Markdown\",\"path\":\"docs/DESIGN.md\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}},{\"chunks\":[{\"content\":\"//! Splitting text into the things tally counts.\\n\",\"highlights\":\"comment:0,49\",\"ranges\":[{\"byteEnd\":46,\"byteStart\":41,\"line\":1}],\"startLine\":1},{\"content\":\"pub fn words(line: &str) -> usize {\\n line.split_whitespace().count()\\n}\",\"highlights\":\"keyword:0,3,1,2;function:7,5,33,16,3,5;type:20,3,5,5\",\"ranges\":[{\"byteEnd\":33,\"byteStart\":28,\"line\":5}],\"startLine\":4},{\"content\":\"\\n/// The lines of `text`. A last line without a newline still counts, and\\n/// empty input has none.\",\"highlights\":\"comment:1,98\",\"ranges\":[{\"byteEnd\":66,\"byteStart\":61,\"line\":8}],\"startLine\":7},{\"content\":\" fn empty_input_has_no_lines() {\\n assert_eq!(super::lines(\\\"\\\").count(), 0);\\n }\",\"highlights\":\"string:68,2;keyword:4,2,49,5;number:81,1;function:7,24,13,10\",\"ranges\":[{\"byteEnd\":41,\"byteStart\":36,\"line\":18}],\"startLine\":17}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"Rust\",\"path\":\"src/parse.rs\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}},{\"chunks\":[{\"content\":\"\\nCounts lines, words and bytes, like `wc`, with output that lines up.\\n\",\"highlights\":\"string:37,4\",\"ranges\":[{\"byteEnd\":5,\"byteStart\":0,\"line\":3}],\"startLine\":2}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"Markdown\",\"path\":\"README.md\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}},{\"chunks\":[{\"content\":\"edition = \\\"2024\\\"\\ndescription = \\\"Counts lines, words and bytes\\\"\\nlicense = \\\"MIT\\\"\",\"highlights\":\"string:10,6,15,31,11,5;type:0,7,10,11,35,7\",\"ranges\":[{\"byteEnd\":20,\"byteStart\":15,\"line\":5}],\"startLine\":4}],\"commit\":\"aae0fe0316b38d65d0c88c17c026786348223d7d\",\"language\":\"TOML\",\"path\":\"Cargo.toml\",\"repo\":{\"createdAt\":\"2026-10-09T10:00:00.000Z\",\"description\":\"Counts lines, words and bytes, with output that lines up\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"tally\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}}],\"matchCount\":29}"
},
"/xrpc/dev.knotless.searchCode?q=t&limit=1": {
"status": 200,
"headers": {
"content-type": "application/json",
"cache-control": "no-cache"
},
"body": "{\"fileCount\":10,\"files\":[{\"chunks\":[{\"content\":\"# notes\\n\\nThings worth remembering.\",\"highlights\":\"function:0,8\",\"ranges\":[{\"byteEnd\":5,\"byteStart\":4,\"line\":1},{\"byteEnd\":1,\"byteStart\":0,\"line\":3},{\"byteEnd\":11,\"byteStart\":10,\"line\":3}],\"startLine\":1}],\"commit\":\"661c67a96bf920012bc578c6c6895e4103128dfe\",\"language\":\"Markdown\",\"path\":\"README.md\",\"repo\":{\"createdAt\":\"2026-10-09T11:51:00.000Z\",\"description\":\"Scratch notes 📝\",\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"name\":\"notes\",\"owner\":{\"did\":\"did:plc:abe24xewga4qdbzjcxmuzdt3\",\"handle\":\"alice.test\"}}}],\"matchCount\":271}"
}
}
}
harness/shots/fixtures.mjs+5−1
harness/shots/world.mjs+20−0
lexicons/dev/knotless/searchCode.json+2−1
web/src/lib/atoms/Count.svelte+3−2
web/src/lib/organisms/CodeResults.svelte+3−2
web/src/lib/highlight.ts+33−0
web/src/routes/search/+page.svelte+3−2
web/src/routes/search/+page.ts+22−7
web/src/app.css+1−0