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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
136 lines · 4.8 KB Raw History
//! `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))
}