~ 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 ranks the languages it finds, each filled with its color

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:web/code-search-languages
crates/knotless-serve/src/web/search.rs+42−2
@@ -27,10 +27,20 @@ struct FileView {
chunks: Vec<crate::code::Chunk>,
}
/// A language of the files found, and how many there are.
#[derive(Serialize)]
struct LanguageCount {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
color: Option<&'static str>,
files: u64,
}
/// `dev.knotless.searchCode`: what `q` finds in the default branches of
/// the public repositories this server indexes, or of the one `repo`
/// names; none of a repository the index no longer lists, or has taken
/// down.
/// down. With them, the languages of all it finds, its `lang:` filters
/// left out, so each can be picked.
pub async fn search_code(
State(app): State<Arc<App>>,
Query(p): Query<SearchParams>,
@@ -50,7 +60,11 @@ pub async fn search_code(
.as_ref()
.map(|(did, key)| (did.as_str(), key.as_str()));
let limit = p.limit.unwrap_or(FILES).clamp(1, FILES_MAX);
let found = code.search(&p.q, within, limit).await?;
let (found, faceted) =
tokio::join!(code.search(&p.q, within, limit), code.faceted(&p.q, within));
// The languages are secondary to what was found: a query Zoekt cannot
// count them by lists none, rather than failing the search.
let (found, faceted) = (found?, faceted.unwrap_or_default());
let listed = |h: crate::code::Hit| Some((index.listed(&h.did, &h.key).ok().flatten()?, h));
let shown: Vec<_> = found.hits.into_iter().filter_map(listed).collect();
let owners = shown.iter().map(|(r, _)| actor(&app, &r.did));
@@ -70,9 +84,35 @@ pub async fn search_code(
}
})
.collect();
// Each repository asked after once, however many of its files count.
let mut shown = std::collections::HashMap::new();
let mut counted = std::collections::BTreeMap::<String, u64>::new();
for hit in faceted.into_iter().filter(|h| !h.language.is_empty()) {
let repo = (hit.did, hit.key);
let listed =
|(did, key): &(String, String)| index.listed(did, key).ok().flatten().is_some();
let listed = *shown.entry(repo.clone()).or_insert_with(|| listed(&repo));
*counted.entry(hit.language).or_default() += u64::from(listed);
}
// Most files first, then by name.
let mut ranked: Vec<(std::cmp::Reverse<u64>, String)> = counted
.into_iter()
.filter(|(_, n)| *n > 0)
.map(|(name, n)| (std::cmp::Reverse(n), name))
.collect();
ranked.sort();
let languages: Vec<_> = ranked
.into_iter()
.map(|(std::cmp::Reverse(files), name)| LanguageCount {
color: crate::languages::color(&name),
name,
files,
})
.collect();
Ok(answer(json!({
"files": files,
"fileCount": found.file_count,
"matchCount": found.match_count,
"languages": languages,
})))
}
crates/knotless-serve/src/code.rs+106−24
@@ -33,6 +33,9 @@ use std::time::Duration;
/// How long a search may take in Zoekt.
const WALL: Duration = Duration::from_secs(10);
/// The most files counted by language for a search.
const FACETED: usize = 1000;
/// How long `zoekt-webserver` has to start answering.
const START: Duration = Duration::from_secs(30);
@@ -268,19 +271,47 @@ impl Code {
repo: Option<(&str, &str)>,
limit: usize,
) -> Result<Results, AppError> {
let q = match repo {
Some((did, key)) => format!("repo:^{}$ ({q})", escape(&format!("{did}/{key}"))),
None => q.to_string(),
};
let asked = json!({
"Q": q,
"Opts": {
"ChunkMatches": true,
"NumContextLines": 1,
"MaxDocDisplayCount": limit,
"MaxWallTime": WALL.as_nanos() as u64,
}
let opts = json!({
"ChunkMatches": true,
"NumContextLines": 1,
"MaxDocDisplayCount": limit,
"MaxWallTime": WALL.as_nanos() as u64,
});
let found = self.ask(within(q, repo), opts).await?;
let hits = found
.files
.unwrap_or_default()
.into_iter()
.filter_map(hit)
.collect();
Ok(Results {
hits,
file_count: found.file_count,
match_count: found.match_count,
})
}
/// The files `q` finds in any language, [`FACETED`] at most, with no
/// lines: what its `lang:` filters leave out counted too, so each
/// language can be picked from them.
pub async fn faceted(&self, q: &str, repo: Option<(&str, &str)>) -> Result<Vec<Hit>, AppError> {
let opts = json!({
"MaxDocDisplayCount": FACETED,
"MaxWallTime": WALL.as_nanos() as u64,
});
let q = format!("type:file ({})", unlanguaged(q));
let found = self.ask(within(&q, repo), opts).await?;
let files = found.files.unwrap_or_default().into_iter();
let bare = files.map(|f| ZFile {
chunk_matches: None,
..f
});
Ok(bare.filter_map(hit).collect())
}
/// What Zoekt answers `q` with `opts`.
async fn ask(&self, q: String, opts: serde_json::Value) -> Result<ZResult, AppError> {
let asked = json!({ "Q": q, "Opts": opts });
let url = format!("{}/api/search", self.url);
let res = self.http.post(url).json(&asked).send().await;
let res = res
@@ -295,18 +326,7 @@ impl Code {
if let Some(error) = reply.error {
return Err(AppError(status, format!("code search: {error}")));
}
let found = reply.result.unwrap_or_default();
let hits = found
.files
.unwrap_or_default()
.into_iter()
.filter_map(hit)
.collect();
Ok(Results {
hits,
file_count: found.file_count,
match_count: found.match_count,
})
Ok(reply.result.unwrap_or_default())
}
}
@@ -372,6 +392,52 @@ fn chunk(c: ZChunk, path: &str) -> Chunk {
}
}
/// `q` kept to `did`'s repository with `key`, if one is named.
fn within(q: &str, repo: Option<(&str, &str)>) -> String {
match repo {
Some((did, key)) => format!("repo:^{}$ ({q})", escape(&format!("{did}/{key}"))),
None => q.to_string(),
}
}
/// `q` without its `lang:` and `-lang:` filters, its words joined by
/// single spaces, or `q` itself when nothing else is left of it. An
/// operator joining a filter to the rest goes with it, and a `)` it ends
/// with stays, so what is left still reads as a query.
fn unlanguaged(q: &str) -> String {
let operator = |w: &str| matches!(w, "or" | "and" | "not");
let mut kept: Vec<&str> = Vec::new();
// A filter was just taken out with no operator before it: one after
// it goes instead.
let mut after = false;
let mut words = q.split_whitespace();
while let Some(word) = words.next() {
let filter = word.strip_prefix('-').unwrap_or(word).strip_prefix("lang:");
let Some(name) = filter else {
if !(after && operator(word)) {
kept.push(word);
}
after = false;
continue;
};
// A quoted name runs on to the word its quote closes in.
let mut end = name;
if name.starts_with('"') && !name[1..].contains('"') {
end = words.by_ref().find(|w| w.contains('"')).unwrap_or("");
}
let closing = end.len() - end.trim_end_matches(')').len();
after = !kept.last().is_some_and(|w| operator(w));
if !after {
kept.pop();
}
kept.extend(std::iter::repeat_n(")", closing));
}
if kept.is_empty() {
return q.to_string();
}
kept.join(" ")
}
/// The prefix of the shards of `did`'s repository with `key`: `did/key`,
/// each byte but letters, digits, `.` and `-` written `_` and its hex, so
/// that it is a file name Zoekt keeps as it is, and no two are one.
@@ -458,6 +524,22 @@ pub(crate) fn dead(dir: &Path) -> Code {
mod tests {
use super::*;
#[test]
fn languages_are_left_out_to_count_them() {
assert_eq!(unlanguaged("count lang:Rust"), "count");
assert_eq!(unlanguaged("lang:\"Vim Script\" a -lang:toml b"), "a b");
assert_eq!(unlanguaged("lang:\"Go\" x"), "x");
assert_eq!(unlanguaged("lang:Rust"), "lang:Rust");
// An operator joining a filter goes with it, before or after.
assert_eq!(unlanguaged("foo or lang:Rust"), "foo");
assert_eq!(unlanguaged("lang:Rust or foo"), "foo");
assert_eq!(unlanguaged("a or lang:Rust or b"), "a or b");
// A group the filter closed stays closed.
assert_eq!(unlanguaged("(foo or lang:Rust) bar"), "(foo ) bar");
assert_eq!(unlanguaged("(a or lang:\"Vim Script\") b"), "(a ) b");
assert_eq!(within("a", Some(("d", "k"))), "repo:^d\\/k$ (a)");
}
#[test]
fn prefixes_are_file_names_of_their_own() {
assert_eq!(prefix("did:plc:ab", "3k.x-y"), "did_3aplc_3aab_2f3k.x-y");
crates/knotless-serve/src/languages.rs+24−0
@@ -135,6 +135,23 @@ static KNOWN: LazyLock<Known> = LazyLock::new(|| {
(names, extensions)
});
/// Prose and data, which are never counted, and their colors in linguist:
/// a code search finds them all the same.
const UNCOUNTED: &[(&str, &str)] = &[
("Markdown", "#083fa1"),
("JSON", "#292929"),
("YAML", "#cb171e"),
("TOML", "#9c4221"),
];
/// The color linguist gives the language it names `name`, as Zoekt names
/// languages too, if it is one known here.
pub fn color(name: &str) -> Option<&'static str> {
let counted = KINDS.iter().find(|(n, ..)| *n == name).map(|(_, c, _)| *c);
let uncounted = || UNCOUNTED.iter().find(|(n, _)| *n == name).map(|(_, c)| *c);
counted.or_else(uncounted)
}
/// The language `path` is written in, by its index in `KINDS`, if it is
/// one that is counted.
pub fn kind(path: &str) -> Option<usize> {
@@ -286,6 +303,13 @@ async fn recount(app: &App, did: &str, key: &str) -> Option<()> {
mod tests {
use super::*;
#[test]
fn colors() {
assert_eq!(color("Rust"), Some("#dea584"));
assert_eq!(color("Markdown"), Some("#083fa1"));
assert_eq!(color("Klingon"), None);
}
fn name(path: &str) -> Option<&'static str> {
kind(path).map(|i| KINDS[i].0)
}
crates/knotless-serve/tests/web.rs+9−0
@@ -3488,6 +3488,15 @@ fn searching_code() {
(one["fileCount"].as_u64(), one["matchCount"].as_u64()),
(Some(1), Some(1))
);
// Its languages, counted across all it finds.
assert_eq!(
one["languages"],
serde_json::json!([{"name": "Rust", "color": "#dea584", "files": 1}])
);
// Picking another language finds nothing, but still lists them all.
let other = search("needle_+lang:Markdown");
assert_eq!(other["files"].as_array().unwrap().len(), 0, "{other}");
assert_eq!(other["languages"][0]["files"], 2, "{other}");
let file = &one["files"][0];
assert_eq!(file["repo"]["name"], "needle-one");
assert_eq!(file["repo"]["owner"]["handle"], handle.as_str());
docs/design/serve.md+6−0
@@ -424,6 +424,12 @@ 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.
Beside the files, a search answers how many it found in each language,
so the page can offer each to narrow to. These come from a second Zoekt
query, run at the same time: the same one with its `lang:` filters taken
out, asking only for files (`type:file`), up to 1000 of them. Picking a
language then leaves the others listed.
### Shared steps
The index keeps a directory of what repositories publish for others to
docs/reference/api.md+4−1
@@ -168,7 +168,10 @@ 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, and its
`highlights?`, encoded as `getFile`'s `ranges` are. `fileCount` and
`highlights?`, encoded as `getFile`'s `ranges` are. `languages` counts
the files found in each language, most first, its `name`, `color?` and
`files`: its `lang:` filters left out, so each can be picked, over the
first 1000 files at most. `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.
harness/shots/fixtures.json+2−2
@@ -570,7 +570,7 @@
"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}"
"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\"}}}],\"languages\":[{\"name\":\"Rust\",\"color\":\"#dea584\",\"files\":6},{\"name\":\"Markdown\",\"color\":\"#083fa1\",\"files\":4},{\"name\":\"TOML\",\"color\":\"#9c4221\",\"files\":2}],\"matchCount\":57}"
},
"/xrpc/dev.knotless.searchCode?q=count&repo=at%3A%2F%2Falice.test%2Fdev.knotless.repo%2Ftally": {
"status": 200,
@@ -578,7 +578,7 @@
"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}"
"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\"}}}],\"languages\":[{\"name\":\"Rust\",\"color\":\"#dea584\",\"files\":3},{\"name\":\"Markdown\",\"color\":\"#083fa1\",\"files\":2},{\"name\":\"TOML\",\"color\":\"#9c4221\",\"files\":1}],\"matchCount\":29}"
},
"/xrpc/dev.knotless.searchCode?q=t&limit=1": {
"status": 200,
lexicons/dev/knotless/searchCode.json+12−1
@@ -23,7 +23,8 @@
"properties": {
"files": { "type": "array", "items": { "type": "ref", "ref": "#fileView" } },
"fileCount": { "type": "integer", "minimum": 0, "description": "How many files the query found in all, of which files are the best." },
"matchCount": { "type": "integer", "minimum": 0, "description": "How many matches the query found in all." }
"matchCount": { "type": "integer", "minimum": 0, "description": "How many matches the query found in all." },
"languages": { "type": "array", "items": { "type": "ref", "ref": "#languageCount" }, "description": "The languages of the files found, counting those its lang: filters leave out, most files first: of the first 1000 files at most." }
}
}
}
@@ -40,6 +41,16 @@
"chunks": { "type": "array", "items": { "type": "ref", "ref": "#chunk" }, "description": "Runs of lines holding matches, in order. Empty when only the path matched." }
}
},
"languageCount": {
"type": "object",
"description": "A language files were found in, and how many.",
"required": ["name", "files"],
"properties": {
"name": { "type": "string", "description": "As Zoekt names it, and lang: takes it." },
"color": { "type": "string", "description": "Its color, as linguist has it. Absent for a language with none known here." },
"files": { "type": "integer", "minimum": 1 }
}
},
"chunk": {
"type": "object",
"description": "Lines of a file holding matches, with a line on either side.",
web/src/routes/search/+page.svelte+20−12
@@ -27,27 +27,35 @@
const within = $derived(/(?:^|\s)repo:(\S+?)\/(\S+)(?=\s|$)/.exec(q));
const everywhere = $derived(of('code', q.replace(/(^|\s)repo:\S+/, ' ').trim()));
// The languages of the files found, most first, each to narrow the search to.
const languages = $derived.by(() => {
if (code.kind !== 'found' || /(^|\s)lang:/.test(q)) return [];
const counted = new Map<string, number>();
for (const f of code.answer.files) if (f.language) counted.set(f.language, (counted.get(f.language) ?? 0) + 1);
return [...counted].sort((a, b) => b[1] - a[1]).map(([name]) => name);
});
// The language `lang:` keeps the search to, and the search without it.
const LANG = /(?:^|\s)lang:("[^"]*"|\S+)/;
const picked = $derived(LANG.exec(q)?.[1].replace(/"/g, '').toLowerCase());
const unpicked = $derived(q.replace(LANG, ' ').replace(/\s+/g, ' ').trim());
// The languages of all the search finds, most files first, each to
// narrow it to, or, picked, to widen it again: each row filled behind
// as far as its share of the first's, which fills its own.
const languages = $derived(code.kind === 'found' ? (code.answer.languages ?? []) : []);
const most = $derived(languages[0]?.files ?? 1);
const narrowed = (name: string) => (name.toLowerCase() === picked ? of('code', unpicked) : of('code', `${unpicked} lang:${name.includes(' ') ? `"${name}"` : name}`));
</script>
<Meta {title} />
<div class="search-page results">
<div class="search-side">
<nav class="account-nav search-nav" aria-label="Filter by">
<p class="search-nav-head">Filter by</p>
<a href={of('code')} aria-current={data.type === 'code' ? 'page' : undefined}><Icon name="code" />Code{#if data.counts.code !== undefined}<Count n={data.counts.code} />{/if}</a>
<a href={of('repositories')} aria-current={data.type === 'repositories' ? 'page' : undefined}><Icon name="repo" />Repositories{#if data.counts.repos !== undefined}<Count n={data.counts.repos} more={data.counts.more} />{/if}</a>
{#if languages.length}
<p class="search-nav-head search-langs">Languages</p>
{#each languages as l (l)}<a class="search-langs" href={of('code', `${q} lang:${l.includes(' ') ? `"${l}"` : l}`)}>{l}</a>{/each}
{/if}
</nav>
{#if languages.length}
<nav class="search-langs" aria-labelledby="search-langs-head">
<p class="search-nav-head" id="search-langs-head">Languages</p>
{#each languages as l (l.name)}<a class="search-lang" href={narrowed(l.name)} aria-current={l.name.toLowerCase() === picked ? 'page' : undefined} title={plural(l.files, 'file', 'files')} aria-label="{l.name}, {plural(l.files, 'file', 'files')}" style:--lang={l.color ?? 'var(--faint)'}><i class="search-lang-bar" aria-hidden="true" style:width="max(var(--space-3), {(l.files * 100) / most}%)"></i><span>{l.name}</span></a>{/each}
</nav>
{/if}
</div>
<div class="settings-main">
<h1>{heading}</h1>
{#if data.type === 'code'}
@@ -59,7 +67,7 @@
{#if code.kind === 'off'}<Empty title="Code search is off here">This server searches no code.</Empty>
{:else if code.kind === 'missing'}<Empty title="No repository {code.repo}">Check its owner and name, or <a href={everywhere}>search all of knotless</a>.</Empty>
{:else if code.kind === 'bad'}<Empty title="That search can’t be read">{code.message}</Empty>
{:else if code.kind === 'found'}<Empty title="No code found">{#if within}Nothing in {within[1]}/{within[2]} matches. <a href={everywhere}>Search all of knotless</a> instead.{:else}Only each repository’s default branch is searched. <a href={of('repositories')}>Search repositories</a> instead.{/if}</Empty>
{:else if code.kind === 'found'}<Empty title="No code found">{#if picked && languages.length}No {languages.find((l) => l.name.toLowerCase() === picked)?.name ?? picked} file matches. <a href={of('code', unpicked)}>Search every language</a> instead.{:else if within}Nothing in {within[1]}/{within[2]} matches. <a href={everywhere}>Search all of knotless</a> instead.{:else}Only each repository’s default branch is searched. <a href={of('repositories')}>Search repositories</a> instead.{/if}</Empty>
{:else}<Empty title="Search code">Type what to look for, as a word, "quoted text" or /a regex/. Add repo:owner/name to search one repository.</Empty>{/if}
</div>
{/if}
web/src/routes/search/+page.ts+3−1
@@ -31,7 +31,9 @@ export const load: PageLoad = async (event) => {
// The page is cached as the repositories are.
const quiet = { ...event, setHeaders: () => {} };
// Each kind's count beside it, for a search: the other kind asked too.
const listed = (e: typeof event) => (q || type === 'repositories' ? xrpc<typeof listRepos.main>(e, 'dev.knotless.listRepos', { q, topic }, true) : null);
// Repositories are asked for with the words alone, not a code search's `lang:`.
const words = type === 'code' ? q.replace(/(^|\s)-?lang:("[^"]*"|\S+)/g, ' ').trim() : q;
const listed = (e: typeof event) => (words || type === 'repositories' ? xrpc<typeof listRepos.main>(e, 'dev.knotless.listRepos', { q: words, topic }, true) : null);
if (type === 'code') {
const [found, repos] = await Promise.all([code(event, q), REPO.test(q) ? null : listed(quiet)]);
const counts = { code: found.kind === 'found' ? found.answer.fileCount : undefined, ...reposCounted(repos) };
web/src/app.css+13−2
web/src/tokens.css+5−0