<script lang="ts">
import type { Did } from "@atcute/lexicons";
import { untrack } from "svelte";
import { resolve } from "$app/paths";
import { getAuth } from "#lib/auth.svelte.ts";
import { loginWithReturn } from "#lib/auth/guards.ts";
import Button from "#lib/components/ui/Button.svelte";
import EmptyState from "#lib/components/ui/EmptyState.svelte";
import ErrorBox from "#lib/components/ui/Error.svelte";
import Spinner from "#lib/components/ui/Spinner.svelte";
import Card from "#lib/components/ui/Card.svelte";
import SearchBar from "#lib/components/search/SearchBar.svelte";
import SearchLayout from "#lib/components/search/SearchLayout.svelte";
import QueryToken from "#lib/components/search/QueryToken.svelte";
import CodeResultCard from "#lib/components/search/CodeResultCard.svelte";
import FileMatch from "#lib/components/search/FileMatch.svelte";
import { searchUrl } from "#lib/components/search/SearchTabs.svelte";
import { matchCount } from "#lib/components/search/chunks.ts";
import { createCodeSearch } from "#lib/components/search/codeSearch.svelte.ts";
import { plural } from "#lib/format.ts";
export interface RepoScope {
ownerHandle: string;
repoName: string;
repoDid?: string;
}
interface Props {
query: string;
repo?: RepoScope;
}
let { query, repo }: Props = $props();
const auth = getAuth();
const search = createCodeSearch({
query: () => query,
repo: () => repo?.repoDid as Did | undefined
});
// eslint-disable-next-line svelte/prefer-writable-derived -- draft is two-way bound to search input
let draft = $state(untrack(() => query));
$effect(() => {
draft = query;
});
const signedIn = $derived(Boolean(auth.currentDid));
const loginHref = $derived(
resolve(
loginWithReturn(
repo
? (`/${repo.ownerHandle}/${repo.repoName}/search${query ? `?q=${encodeURIComponent(query)}` : ""}` as "/")
: (searchUrl("code", query) as "/")
) as "/login"
)
);
const matches = $derived(
search.results.reduce((n, r) => n + matchCount(r.chunks ?? []) + (r.ranges?.length ?? 0), 0)
);
</script>
<svelte:head>
{#if repo}
<title>Search · {repo.ownerHandle}/{repo.repoName} · Tangled</title>
{:else}
<title>Code Search · Tangled</title>
{/if}
</svelte:head>
{#snippet resultsList()}
{#if !signedIn}
<EmptyState message="Code search needs an account.">
<a href={loginHref} class="underline">Sign in</a>
</EmptyState>
{:else if repo && !repo.repoDid}
<EmptyState message="This repository has no DID to search by." />
{:else if !query}
{#if repo}
<EmptyState
message="Enter a search term to find files or code in this repository."
messageDirection="vertical"
class="px-md text-center md:px-4xl"
>
<span>
Narrow it with <QueryToken>lang:go</QueryToken>,
<QueryToken>file:readme</QueryToken>, or
<QueryToken>case:yes</QueryToken>.
</span>
</EmptyState>
{:else}
<EmptyState message="Enter a query to search code." />
{/if}
{:else if search.loading}
<Card
padding="none"
border={false}
class="flex items-center justify-center p-4xl text-foreground-subtle edge-ring-inset"
role="status"
aria-label="Searching code"
>
<Spinner class="size-6" />
</Card>
{:else if search.error}
<ErrorBox label={search.error} />
{:else}
<div class="grid grid-cols-1 gap-sm">
{#each search.results as result (`${result.repo.did}/${result.commit}/${result.path}`)}
{#if repo}
<FileMatch {result} />
{:else}
<CodeResultCard {result} />
{/if}
{:else}
<EmptyState message="No results found." />
{/each}
</div>
{#if search.moreError}
<ErrorBox label={search.moreError} />
{/if}
{#if search.cursor}
<Button
onclick={search.loadMore}
loading={search.loadingMore}
disabled={search.loadingMore}
class="w-full"
>
Load more
</Button>
{/if}
{/if}
{/snippet}
{#snippet matchSummary()}
{#if matches > 0}
<div
class="border-t border-border-default py-xs typography-paragraph-small text-foreground-muted"
>
Found {matches}
{plural(matches, "match", "matches")} in {search.results.length}
{plural(search.results.length, "file", "files")}
</div>
{/if}
{/snippet}
{#if repo}
<Card class="flex flex-col gap-sm">
<SearchBar type="code" bind:query={draft} placeholder="Find files or code..." />
{@render resultsList()}
</Card>
{:else}
<SearchLayout title="Code Search" query={draft} active="code" aside={matchSummary}>
{#snippet bar()}
<SearchBar type="code" bind:query={draft} placeholder="Search code..." mono />
{/snippet}
{@render resultsList()}
</SearchLayout>
{/if}