diff --git a/frontend/src/api/snippets.ts b/frontend/src/api/snippets.ts index 6b400dd..fa7ef03 100644 --- a/frontend/src/api/snippets.ts +++ b/frontend/src/api/snippets.ts @@ -80,13 +80,26 @@ export interface SnippetInput { force?: boolean; } +/** `repo`/`path`/`symbol` are the reverse lookup — "what already lives here?" + * They must all match the same recorded location; `path` also matches anything + * beneath it. Combinable with `q`. */ export async function listSnippets( - params: { q?: string; tag?: string; projectId?: number | null } = {}, + params: { + q?: string; + tag?: string; + projectId?: number | null; + repo?: string; + path?: string; + symbol?: string; + } = {}, ): Promise<{ snippets: SnippetListItem[]; total: number }> { const qs = new URLSearchParams(); if (params.q) qs.set("q", params.q); if (params.tag) qs.set("tag", params.tag); if (params.projectId) qs.set("project_id", String(params.projectId)); + if (params.repo) qs.set("repo", params.repo); + if (params.path) qs.set("path", params.path); + if (params.symbol) qs.set("symbol", params.symbol); const query = qs.toString(); return apiGet(`/api/snippets${query ? `?${query}` : ""}`); } diff --git a/frontend/src/views/SnippetListView.vue b/frontend/src/views/SnippetListView.vue index c5849cb..8fbed78 100644 --- a/frontend/src/views/SnippetListView.vue +++ b/frontend/src/views/SnippetListView.vue @@ -13,6 +13,27 @@ const error = ref(null); const search = ref(""); let searchTimer: ReturnType | undefined; +// Reverse lookup — "what already lives here?" All three must match the same +// recorded location; path also matches anything beneath it. +const showLocationFilter = ref(false); +const locRepo = ref(""); +const locPath = ref(""); +const locSymbol = ref(""); +const locationActive = computed( + () => !!(locRepo.value.trim() || locPath.value.trim() || locSymbol.value.trim()), +); +function clearLocation() { + locRepo.value = ""; + locPath.value = ""; + locSymbol.value = ""; + loadSnippets(); +} +function toggleLocationFilter() { + showLocationFilter.value = !showLocationFilter.value; + // Collapsing while filtered would hide why the list is short. + if (!showLocationFilter.value && locationActive.value) clearLocation(); +} + // Multi-select → merge const selectMode = ref(false); const selectedIds = ref>(new Set()); @@ -70,7 +91,12 @@ async function loadSnippets() { loading.value = true; error.value = null; try { - const data = await listSnippets({ q: search.value.trim() || undefined }); + const data = await listSnippets({ + q: search.value.trim() || undefined, + repo: locRepo.value.trim() || undefined, + path: locPath.value.trim() || undefined, + symbol: locSymbol.value.trim() || undefined, + }); snippets.value = data.snippets; } catch { error.value = "Couldn't load your snippets."; @@ -84,6 +110,9 @@ function onSearchInput() { clearTimeout(searchTimer); searchTimer = setTimeout(loadSnippets, 300); } +// Same debounce for the location fields — typing a path shouldn't fire a query +// per keystroke. +const onLocationInput = onSearchInput; onMounted(loadSnippets); @@ -127,6 +156,48 @@ function languageOf(tags: string[]): string { @input="onSearchInput" @keydown.enter="loadSnippets" /> + + + + +
+ + + +
@@ -138,15 +209,24 @@ function languageOf(tags: string[]): string {
❭_

- {{ search.trim() ? "No snippets match your search" : "No snippets kept yet" }} + {{ locationActive + ? "Nothing kept at that location yet" + : search.trim() + ? "No snippets match your search" + : "No snippets kept yet" }}

- {{ search.trim() - ? "Try a different term, or clear the search." - : "Record a reusable function or component and it will be offered back to you later." }} + {{ locationActive + ? "No recorded snippet lives there — so whatever you're about to write is new. Widen the path, or clear the filter." + : search.trim() + ? "Try a different term, or clear the search." + : "Record a reusable function or component and it will be offered back to you later." }}

+