johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! `dev.knotless.searchCode`: the code of public repositories, as Zoekt
//! finds it ([`crate::code`]).
use super::*;
use serde_json::{Value, json};
/// How many files a search shows, if not asked, and at most.
const FILES: usize = 20;
const FILES_MAX: usize = 50;
/// How deep the pages of a search go: each page asks Zoekt for all of
/// them, as it cuts off files it scores alike as they come.
const DEEPEST: usize = 200;
#[derive(Deserialize)]
pub struct SearchParams {
q: String,
repo: Option<String>,
limit: Option<usize>,
cursor: Option<String>,
}
/// A file found, in the repository the index lists it in.
#[derive(Serialize)]
struct FileView {
repo: Value,
path: String,
#[serde(skip_serializing_if = "String::is_empty")]
language: String,
#[serde(skip_serializing_if = "String::is_empty")]
commit: String,
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. 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>,
) -> Result<Response> {
let (Some(code), Some(index)) = (app.code.as_deref(), app.index.as_deref()) else {
return Err(Shown(
StatusCode::NOT_IMPLEMENTED,
"this server searches no code".into(),
));
};
let mut within = None;
if let Some(uri) = &p.repo {
let repo = repo::found(&app, uri).await?;
within = Some((repo.did, repo.key));
}
let within = within
.as_ref()
.map(|(did, key)| (did.as_str(), key.as_str()));
let limit = p.limit.unwrap_or(FILES).clamp(1, FILES_MAX);
let skip = repo::skipped(p.cursor.as_deref())?;
let upto = (skip + limit).min(DEEPEST);
// The languages are secondary to what was found: a query Zoekt cannot
// count them by lists none, rather than failing the search. The first
// page alone lists them, as the pages after add only files.
let faceted = async {
match skip {
0 => code.faceted(&p.q, within).await.unwrap_or_default(),
_ => Vec::new(),
}
};
let (found, faceted) = tokio::join!(code.search(&p.q, within, DEEPEST), faceted);
let found = found?;
let more = upto < DEEPEST && found.file_count > upto as u64;
let listed = |h: crate::code::Hit| Some((index.listed(&h.did, &h.key).ok().flatten()?, h));
let pages_before = found.hits.into_iter().take(upto).skip(skip);
let shown: Vec<_> = pages_before.filter_map(listed).collect();
let owners = shown.iter().map(|(r, _)| actor(&app, &r.did));
let owners = futures::future::join_all(owners).await;
let files: Vec<FileView> = shown
.into_iter()
.zip(owners)
.map(|((r, hit), owner)| {
let mut repo = json!(r);
repo["owner"] = json!(owner);
FileView {
repo,
path: hit.path,
language: hit.language,
commit: hit.commit,
chunks: hit.chunks,
}
})
.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();
let mut answered = json!({
"files": files,
"fileCount": found.file_count,
"matchCount": found.match_count,
"languages": languages,
});
if more {
answered["cursor"] = json!(upto.to_string());
}
Ok(answer(answered))
}