@lapis-notes/search 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,8 @@
8
8
  searchQueryLanguageSupport,
9
9
  Notice,
10
10
  type App,
11
+ type Plugin,
12
+ type SearchResultScope,
11
13
  type EditorPosition,
12
14
  type TFile,
13
15
  TextFileView,
@@ -24,6 +26,7 @@
24
26
  import * as Popover from "@lapismd/design-core/shadcn/popover";
25
27
  import { ScrollArea } from "@lapismd/design-core/shadcn/scroll-area";
26
28
  import * as Sidebar from "@lapismd/design-core/shadcn/sidebar";
29
+ import { Skeleton } from "@lapismd/design-core/shadcn/skeleton";
27
30
  import { Switch } from "@lapismd/design-core/shadcn/switch";
28
31
  import ChevronDown from "@lucide/svelte/icons/chevron-down";
29
32
  import ChevronRight from "@lucide/svelte/icons/chevron-right";
@@ -57,8 +60,18 @@
57
60
  type SearchResult,
58
61
  } from "./search-result-model";
59
62
  import { SearchViewType } from "./search-view-type";
63
+ import { observeRecoveredSearchSettings } from "./search-settings-observer";
64
+
65
+ import {
66
+ SearchProviderController,
67
+ searchProviderFields,
68
+ type ProviderResults,
69
+ } from "./search-provider-controller";
70
+ import { groupProviderResults } from "./search-provider-groups";
71
+ import SearchHistoryVersions from "./search-history-versions.svelte";
60
72
 
61
73
  type SearchPanelPlugin = {
74
+ readonly state: Plugin["state"];
62
75
  searchManager: SearchManager;
63
76
  getSettings(): SearchPluginSettings;
64
77
  updateSettings(patch: SearchPluginSettingsPatch): Promise<void>;
@@ -81,9 +94,13 @@
81
94
  let {
82
95
  app,
83
96
  initialQuery = "",
97
+ initialScope = "files",
98
+ onStateChange,
84
99
  }: {
85
100
  app: App;
86
101
  initialQuery?: string;
102
+ initialScope?: SearchResultScope;
103
+ onStateChange?: () => void;
87
104
  } = $props();
88
105
 
89
106
  const plugin = untrack(() => {
@@ -94,7 +111,97 @@
94
111
  return registered;
95
112
  });
96
113
 
97
- let query = $state("");
114
+ let query = $state(untrack(() => initialQuery));
115
+ let scope = $state<SearchResultScope>(untrack(() => initialScope));
116
+ let previousViewState = untrack(() => JSON.stringify({ query, scope }));
117
+ $effect(() => {
118
+ const next = JSON.stringify({ query, scope });
119
+ if (next === previousViewState) return;
120
+ previousViewState = next;
121
+ untrack(() => onStateChange?.());
122
+ });
123
+ export function getSearchState() {
124
+ return { query, scope };
125
+ }
126
+ let providerRows = $state<ProviderResults[]>([]);
127
+ const providerController = untrack(
128
+ () =>
129
+ new SearchProviderController(
130
+ app.searchResultProviders,
131
+ (error) =>
132
+ new Notice(
133
+ `History search failed: ${error instanceof Error ? error.message : String(error)}`,
134
+ undefined,
135
+ app,
136
+ ),
137
+ ),
138
+ );
139
+ const reportProviderError = (error: unknown) => {
140
+ new Notice(
141
+ `History search failed: ${error instanceof Error ? error.message : String(error)}`,
142
+ undefined,
143
+ app,
144
+ );
145
+ };
146
+ const historyGroups = $derived(groupProviderResults(providerRows));
147
+ const historyCount = $derived(
148
+ historyGroups.reduce((sum, group) => sum + group.versions.length, 0),
149
+ );
150
+ const historyPending = $derived(
151
+ providerRows.some(
152
+ (row) => row.busy || row.next || row.status?.complete === false,
153
+ ),
154
+ );
155
+ const parsedQuery = $derived(parseSearchQueryAst(query));
156
+ const currentUnsupported = $derived(
157
+ searchProviderFields(parsedQuery.expression).some((field) =>
158
+ ["author", "after", "before", "commit"].includes(field),
159
+ ),
160
+ );
161
+ const displayGroups = $derived.by(() => {
162
+ const groups = new Map(
163
+ historyGroups.map((history) => [
164
+ history.id,
165
+ {
166
+ id: history.id,
167
+ history,
168
+ current: undefined as SearchResult | undefined,
169
+ },
170
+ ]),
171
+ );
172
+ for (const current of filteredResults) {
173
+ const existing = groups.get(current.file.path);
174
+ if (existing) existing.current = current;
175
+ else
176
+ groups.set(current.file.path, {
177
+ id: current.file.path,
178
+ current,
179
+ history: undefined!,
180
+ });
181
+ }
182
+ const mode = settings.view.sortMode;
183
+ return [...groups.values()].sort((a, b) => {
184
+ if (mode.startsWith("filename"))
185
+ return (
186
+ (mode.endsWith("desc") ? -1 : 1) *
187
+ (a.current?.file.name ?? a.history?.title ?? a.id).localeCompare(
188
+ b.current?.file.name ?? b.history?.title ?? b.id,
189
+ undefined,
190
+ { numeric: true, sensitivity: "base" },
191
+ ) || a.id.localeCompare(b.id)
192
+ );
193
+ const time = (item: typeof a) =>
194
+ item.current
195
+ ? mode.startsWith("created")
196
+ ? item.current.file.stat.ctime
197
+ : item.current.file.stat.mtime
198
+ : (item.history?.versions[0]?.hit.timestamp ?? 0);
199
+ return (
200
+ (mode.endsWith("desc") ? -1 : 1) * (time(a) - time(b)) ||
201
+ a.id.localeCompare(b.id)
202
+ );
203
+ });
204
+ });
98
205
  let settings = $state(untrack(() => plugin.getSettings()));
99
206
  let results = $state<SearchResult[]>([]);
100
207
  let resultCount = $state(0);
@@ -108,9 +215,32 @@
108
215
  let metadataFacetError = $state<string | null>(null);
109
216
  let metadataFacetGeneration = 0;
110
217
  let searchRevision = 0;
218
+ const resultSummary = $derived(
219
+ searching
220
+ ? "Searching…"
221
+ : scope === "files"
222
+ ? `${resultCount} result${resultCount === 1 ? "" : "s"}`
223
+ : `${scope === "history" ? 0 : resultCount} current · ${historyCount} saved${historyPending ? " loaded" : ""}`,
224
+ );
225
+ const providerSearchKey = $derived(
226
+ JSON.stringify({
227
+ text: query,
228
+ caseSensitive: settings.view.matchCase,
229
+ mode: retrievalModeForQuery(),
230
+ limit: settings.query.resultLimit,
231
+ enabled: scope !== "files",
232
+ }),
233
+ );
234
+ $effect(() => {
235
+ const { enabled, ...input } = JSON.parse(providerSearchKey);
236
+ const timer = window.setTimeout(
237
+ () => providerController.search(input, enabled),
238
+ 250,
239
+ );
240
+ return () => window.clearTimeout(timer);
241
+ });
111
242
  const databaseRefreshDelayMs = 75;
112
243
 
113
- const parsedQuery = $derived(parseSearchQueryAst(query));
114
244
  const diagnostic = $derived(
115
245
  query.trim() ? (parsedQuery.diagnostics[0]?.message ?? null) : null,
116
246
  );
@@ -124,6 +254,7 @@
124
254
  const searchRequestKey = $derived(
125
255
  JSON.stringify([
126
256
  query,
257
+ scope,
127
258
  settings.view.matchCase,
128
259
  settings.view.showMoreContext,
129
260
  settings.view.semanticSearchInStructuredQueries,
@@ -144,6 +275,7 @@
144
275
  : "Matching filenames, paths, tags, metadata, and content.";
145
276
  });
146
277
  const filteredResults = $derived.by(() => {
278
+ if (scope === "history" || currentUnsupported) return [];
147
279
  const facet = settings.view.resultFacet;
148
280
  const matching = results.filter((result) => {
149
281
  if (facet === "all") return true;
@@ -163,11 +295,41 @@
163
295
  .map((file) => file.name)
164
296
  .sort()
165
297
  .slice(0, 100);
166
- return createVaultSearchFilterSyntax({
298
+ const syntax = createVaultSearchFilterSyntax({
167
299
  fileNames: names,
168
300
  paths,
169
301
  tags: metadataTags,
170
302
  });
303
+ if (scope === "files") return syntax;
304
+ const fields =
305
+ scope === "history"
306
+ ? (syntax.fields?.filter((field) =>
307
+ ["file", "path", "content"].includes(field.name),
308
+ ) ?? [])
309
+ : (syntax.fields ?? []);
310
+ return {
311
+ ...syntax,
312
+ title: "Search syntax",
313
+ fields: [
314
+ ...fields,
315
+ { name: "author", description: "Checkpoint author", operators: [":"] },
316
+ {
317
+ name: "after",
318
+ description: "Checkpoint date on or after (UTC)",
319
+ operators: [":"],
320
+ },
321
+ {
322
+ name: "before",
323
+ description: "Checkpoint date on or before (UTC)",
324
+ operators: [":"],
325
+ },
326
+ {
327
+ name: "commit",
328
+ description: "Checkpoint revision",
329
+ operators: [":"],
330
+ },
331
+ ],
332
+ };
171
333
  });
172
334
 
173
335
  async function refreshMetadataFacets(): Promise<void> {
@@ -193,6 +355,10 @@
193
355
  }
194
356
  }
195
357
 
358
+ export function setSearchScope(next: SearchResultScope): void {
359
+ scope = next;
360
+ }
361
+
196
362
  export function setSearchQuery(next: string): void {
197
363
  query = next;
198
364
  }
@@ -212,13 +378,28 @@
212
378
  );
213
379
  }
214
380
 
381
+ let openQuery = "";
382
+ $effect(() => {
383
+ if (openQuery !== query) {
384
+ openQuery = query;
385
+ resultOpenState = {};
386
+ }
387
+ });
215
388
  function refreshOpenState(items: SearchResult[], identity: string): void {
216
389
  if (identity === resultIdentity) return;
217
390
  resultIdentity = identity;
218
391
  const defaultOpen = !settings.view.collapseResults;
219
- resultOpenState = Object.fromEntries(
220
- items.map((item) => [item.file.path, defaultOpen]),
221
- );
392
+ const previous = openQuery === query ? resultOpenState : {};
393
+ openQuery = query;
394
+ resultOpenState = {
395
+ ...previous,
396
+ ...Object.fromEntries(
397
+ items.map((item) => [
398
+ item.file.path,
399
+ previous[item.file.path] ?? defaultOpen,
400
+ ]),
401
+ ),
402
+ };
222
403
  }
223
404
 
224
405
  function setResultOpen(path: string, open: boolean): void {
@@ -251,13 +432,19 @@
251
432
  if (patch.view?.collapseResults !== undefined) {
252
433
  const defaultOpen = !settings.view.collapseResults;
253
434
  resultOpenState = Object.fromEntries(
254
- results.map((result) => [result.file.path, defaultOpen]),
435
+ displayGroups.map((group) => [group.id, defaultOpen]),
255
436
  );
256
437
  }
257
438
  }
258
439
 
259
440
  async function executeSearch(term: string, revision: number): Promise<void> {
260
- if (!term.trim() || diagnostic) {
441
+ if (plugin.state !== "enabled") return;
442
+ if (
443
+ !term.trim() ||
444
+ diagnostic ||
445
+ scope === "history" ||
446
+ currentUnsupported
447
+ ) {
261
448
  results = [];
262
449
  resultCount = 0;
263
450
  searching = false;
@@ -272,11 +459,11 @@
272
459
  snippetLength: snippetLength(),
273
460
  mode: retrievalModeForQuery(),
274
461
  });
275
- if (revision !== searchRevision) return;
462
+ if (revision !== searchRevision || plugin.state !== "enabled") return;
276
463
  const nextResults = response.hits
277
464
  .map((hit) => searchResultFromHit(app, hit))
278
465
  .filter((result): result is SearchResult => result !== null);
279
- resultCount = response.count;
466
+ resultCount = nextResults.length;
280
467
  const identity = `${term}\u0000${nextResults
281
468
  .map(
282
469
  (result) =>
@@ -285,7 +472,14 @@
285
472
  .join("\u0000")}`;
286
473
  if (identity !== resultIdentity) results = nextResults;
287
474
  refreshOpenState(nextResults, identity);
288
- void rememberSearch(term);
475
+ await rememberSearch(term);
476
+ } catch (error) {
477
+ if (revision === searchRevision && plugin.state === "enabled")
478
+ new Notice(
479
+ error instanceof Error ? error.message : String(error),
480
+ undefined,
481
+ app,
482
+ );
289
483
  } finally {
290
484
  if (revision === searchRevision) searching = false;
291
485
  }
@@ -303,6 +497,7 @@
303
497
  });
304
498
 
305
499
  async function refreshIndex(): Promise<void> {
500
+ providerController.refresh();
306
501
  if (indexing) return;
307
502
  indexing = true;
308
503
  try {
@@ -319,10 +514,18 @@
319
514
  result.file.path,
320
515
  ...result.matches.map((match) => ` ${match.key}: ${match.text}`),
321
516
  ]);
517
+ lines.push(
518
+ ...providerRows.flatMap((row) =>
519
+ row.hits.flatMap((hit) => [
520
+ hit.path ?? hit.title,
521
+ ...hit.matches.map((match) => ` ${match.field}: ${match.text}`),
522
+ ]),
523
+ ),
524
+ );
322
525
  const text = lines.join("\n");
323
526
  try {
324
527
  await navigator.clipboard.writeText(text);
325
- new Notice("Search results copied");
528
+ new Notice("Search results copied", undefined, app);
326
529
  } catch {
327
530
  const textarea = document.createElement("textarea");
328
531
  textarea.value = text;
@@ -334,6 +537,8 @@
334
537
  textarea.remove();
335
538
  new Notice(
336
539
  copied ? "Search results copied" : "Unable to copy search results",
540
+ undefined,
541
+ app,
337
542
  );
338
543
  }
339
544
  }
@@ -419,7 +624,26 @@
419
624
  }
420
625
 
421
626
  onMount(() => {
422
- query = initialQuery;
627
+ const settingsChanged = observeRecoveredSearchSettings(
628
+ app,
629
+ "search",
630
+ (next) => {
631
+ const collapseChanged =
632
+ settings.view.collapseResults !== next.view.collapseResults;
633
+ settings = next;
634
+ if (collapseChanged) {
635
+ resultOpenState = Object.fromEntries(
636
+ displayGroups.map((group) => [
637
+ group.id,
638
+ !next.view.collapseResults,
639
+ ]),
640
+ );
641
+ }
642
+ },
643
+ );
644
+ const unsubscribeProviders = providerController.subscribe(
645
+ (rows) => (providerRows = rows),
646
+ );
423
647
  void refreshMetadataFacets();
424
648
  let databaseRefreshTimer: number | undefined;
425
649
  const databaseChanged = app.appDatabase.subscribeToChanges((change) => {
@@ -440,6 +664,9 @@
440
664
  () => void refreshMetadataFacets(),
441
665
  );
442
666
  return () => {
667
+ settingsChanged();
668
+ unsubscribeProviders();
669
+ providerController.close();
443
670
  searchRevision += 1;
444
671
  metadataFacetGeneration += 1;
445
672
  window.clearTimeout(databaseRefreshTimer);
@@ -586,19 +813,31 @@
586
813
  </p>
587
814
  {/if}
588
815
 
816
+ <div class="search-panel__source">
817
+ <FilterCommandPicker
818
+ label={`${scope === "files" ? "Current files" : scope === "history" ? "History" : "Everything"} search source`}
819
+ value={scope}
820
+ options={[
821
+ { value: "files", label: "Current files" },
822
+ { value: "history", label: "History" },
823
+ { value: "all", label: "Everything" },
824
+ ]}
825
+ onChange={(value: string) => {
826
+ scope = value as SearchResultScope;
827
+ }}
828
+ />
829
+ </div>
589
830
  <div class="search-panel__summary">
590
831
  <Button
591
832
  variant="ghost"
592
833
  size="sm"
593
834
  class="search-panel__summary-control"
594
- aria-label="Copy search results"
595
- disabled={!filteredResults.length}
835
+ aria-label={`Copy search results: ${resultSummary}`}
836
+ disabled={!filteredResults.length && !historyCount}
596
837
  onclick={copySearchResults}
597
838
  >
598
839
  <Copy aria-hidden="true" />
599
- {searching
600
- ? "Searching…"
601
- : `${resultCount} result${resultCount === 1 ? "" : "s"}`}
840
+ {resultSummary}
602
841
  </Button>
603
842
  <Popover.Root>
604
843
  <Popover.Trigger>
@@ -608,8 +847,11 @@
608
847
  variant="ghost"
609
848
  size="sm"
610
849
  class="search-panel__summary-control search-panel__sort"
850
+ aria-label={formatSearchViewSortLabel(settings.view.sortMode)}
611
851
  >
612
- <span>{formatSearchViewSortLabel(settings.view.sortMode)}</span>
852
+ <span class="search-panel__sort-label"
853
+ >{formatSearchViewSortLabel(settings.view.sortMode)}</span
854
+ >
613
855
  <ChevronsUpDown aria-hidden="true" />
614
856
  </Button>
615
857
  {/snippet}
@@ -630,6 +872,35 @@
630
872
  </Popover.Root>
631
873
  </div>
632
874
 
875
+ {#if scope !== "files"}
876
+ {#if !providerRows.length && query.trim()}<p
877
+ class="search-panel__explanation"
878
+ >
879
+ History search is unavailable. Enable Repository History to search
880
+ saved versions.
881
+ </p>{/if}
882
+ {#each providerRows as row (row.provider.id)}
883
+ {#if row.unavailable}<p class="search-panel__explanation">
884
+ {row.provider.label}: {row.unavailable}
885
+ </p>{/if}
886
+ {#if row.status?.omitted}<p class="search-panel__explanation">
887
+ {row.status.omitted} file versions are searchable by path only.
888
+ </p>{/if}
889
+ {#if row.status?.complete === false}<p
890
+ class="search-panel__explanation"
891
+ role="status"
892
+ >
893
+ {row.status.description ??
894
+ `Indexing history · ${row.status.indexed} checkpoints ready`}
895
+ </p>{/if}
896
+ {/each}
897
+ {/if}
898
+ {#if currentUnsupported && scope !== "history"}<p
899
+ class="search-panel__explanation"
900
+ >
901
+ Current files do not support checkpoint author, date or revision
902
+ filters.
903
+ </p>{/if}
633
904
  {#if explanation}
634
905
  <p class="search-panel__explanation">{explanation}</p>
635
906
  {/if}
@@ -639,7 +910,17 @@
639
910
  <Sidebar.Content class="search-panel__results" data-ui-part="content">
640
911
  {#if !query.trim()}
641
912
  <p class="search-panel__empty">Type to search.</p>
642
- {:else if !searching && !diagnostic && filteredResults.length === 0}
913
+ {:else if providerRows.some((row) => row.busy) && displayGroups.length === 0}
914
+ <div
915
+ class="search-panel__loading"
916
+ role="status"
917
+ aria-label="Searching saved history"
918
+ >
919
+ {#each Array(4) as _}<Skeleton
920
+ class="search-panel__loading-title"
921
+ /><Skeleton class="search-panel__loading-snippet" />{/each}
922
+ </div>
923
+ {:else if !searching && !historyPending && !providerRows.some((row) => row.unavailable) && !diagnostic && displayGroups.length === 0}
643
924
  <p class="search-panel__empty">No matches found.</p>
644
925
  {:else}
645
926
  <div class="search-panel__tree-inset">
@@ -648,14 +929,16 @@
648
929
  aria-label="Search results"
649
930
  class="search-panel__tree"
650
931
  >
651
- {#each filteredResults as result (result.file.path)}
932
+ {#each displayGroups as display (display.id)}
933
+ {@const result = display.current}
934
+ {@const path =
935
+ result?.file.path ?? display.history?.path ?? display.id}
652
936
  {@const open =
653
- resultOpenState[result.file.path] ??
654
- !settings.view.collapseResults}
937
+ resultOpenState[display.id] ?? !settings.view.collapseResults}
655
938
  <Sidebar.MenuItem role="none" class="search-panel__tree-item">
656
939
  <Collapsible.Root
657
940
  {open}
658
- onOpenChange={(next) => setResultOpen(result.file.path, next)}
941
+ onOpenChange={(next) => setResultOpen(display.id, next)}
659
942
  class="search-panel__result"
660
943
  >
661
944
  <Collapsible.Trigger
@@ -664,13 +947,15 @@
664
947
  aria-level={1}
665
948
  aria-selected="false"
666
949
  aria-expanded={open}
667
- aria-label={`${result.file.path}, ${result.matches.length} matches`}
950
+ aria-label={result
951
+ ? `${result.file.basename} ${(result?.matches.length ?? 0) + (display.history?.versions.length ?? 0)} · ${path}, ${(result?.matches.length ?? 0) + (display.history?.versions.length ?? 0)} matches`
952
+ : `${display.history?.title ?? path} ${display.history?.versions.length ?? 0} · ${path}, ${display.history?.versions.length ?? 0} saved versions`}
668
953
  >
669
954
  <ChevronRight
670
955
  class="search-panel__disclosure"
671
956
  aria-hidden="true"
672
957
  />
673
- {#if result.file.extension === "canvas"}
958
+ {#if (result?.file.extension ?? display.history?.extension) === "canvas"}
674
959
  <Hash
675
960
  class="search-panel__file-icon"
676
961
  aria-hidden="true"
@@ -683,30 +968,34 @@
683
968
  {/if}
684
969
  <span class="search-panel__file-label">
685
970
  <HighlightedText
686
- text={result.file.basename}
687
- ranges={result.title?.ranges ?? []}
971
+ text={result?.file.basename ??
972
+ display.history?.title ??
973
+ path}
974
+ ranges={result?.title?.ranges ?? []}
688
975
  />
689
976
  </span>
690
977
  <Sidebar.MenuBadge class="search-panel__count-badge">
691
- {result.matches.length}
978
+ {(result?.matches.length ?? 0) +
979
+ (display.history?.versions.length ?? 0)}
692
980
  </Sidebar.MenuBadge>
693
981
  </Collapsible.Trigger>
694
982
  <Collapsible.Content class="search-panel__match-list">
695
983
  <div class="search-panel__match-header">
696
- <span
697
- class="search-panel__match-path"
698
- title={result.file.path}
699
- >
700
- {result.file.path}
984
+ <span class="search-panel__match-path" title={path}>
985
+ {path}
701
986
  </span>
702
987
  <Badge variant="outline" class="search-panel__mode-badge">
703
- {result.hit.retrievalMode}
988
+ {result?.hit.retrievalMode ?? "lexical"}
704
989
  </Badge>
990
+ {#if display.history && result}<Badge variant="outline"
991
+ >Current</Badge
992
+ >{/if}
705
993
  </div>
706
994
  <Sidebar.MenuSub role="group">
707
- {#each result.matches as match, index (`${result.file.path}:${index}`)}
995
+ {#each result?.matches ?? [] as match, index (`${display.id}:${index}`)}
996
+ {@const currentResult = result!}
708
997
  <Sidebar.MenuSubItem role="none">
709
- {@const loadingKey = `${result.file.path}\u0000${match.id}`}
998
+ {@const loadingKey = `${path}\u0000${match.id}`}
710
999
  <div
711
1000
  class="search-panel__match-shell"
712
1001
  role="treeitem"
@@ -718,12 +1007,12 @@
718
1007
  match.context &&
719
1008
  match.context.end < match.context.sourceLength,
720
1009
  )}
721
- onclick={() => openResult(result, match.pos)}
1010
+ onclick={() => openResult(currentResult, match.pos)}
722
1011
  onkeydown={(event) => {
723
1012
  if (event.key !== "Enter" && event.key !== " ")
724
1013
  return;
725
1014
  event.preventDefault();
726
- void openResult(result, match.pos);
1015
+ void openResult(currentResult, match.pos);
727
1016
  }}
728
1017
  >
729
1018
  {#if match.context?.start}
@@ -736,7 +1025,7 @@
736
1025
  onclick={(event) => {
737
1026
  event.stopPropagation();
738
1027
  void expandMatchContext(
739
- result,
1028
+ currentResult,
740
1029
  match,
741
1030
  "before",
742
1031
  );
@@ -771,7 +1060,7 @@
771
1060
  onclick={(event) => {
772
1061
  event.stopPropagation();
773
1062
  void expandMatchContext(
774
- result,
1063
+ currentResult,
775
1064
  match,
776
1065
  "after",
777
1066
  );
@@ -784,6 +1073,10 @@
784
1073
  </Sidebar.MenuSubItem>
785
1074
  {/each}
786
1075
  </Sidebar.MenuSub>
1076
+ {#if display.history}<SearchHistoryVersions
1077
+ versions={display.history.versions}
1078
+ report={reportProviderError}
1079
+ />{/if}
787
1080
  </Collapsible.Content>
788
1081
  </Collapsible.Root>
789
1082
  </Sidebar.MenuItem>
@@ -791,6 +1084,15 @@
791
1084
  </Sidebar.Menu>
792
1085
  </div>
793
1086
  {/if}
1087
+ {#each providerRows.filter((row) => row.next) as row (row.provider.id)}
1088
+ <Button
1089
+ variant="ghost"
1090
+ size="sm"
1091
+ disabled={row.busy}
1092
+ onclick={() => providerController.more(row.provider.id)}
1093
+ >{row.busy ? "Searching history…" : "Search older history"}</Button
1094
+ >
1095
+ {/each}
794
1096
  </Sidebar.Content>
795
1097
  </ScrollArea>
796
1098
  </div>
@@ -1,10 +1,17 @@
1
1
  import "./search-panel.css";
2
- import { type App } from "@lapis-notes/api";
2
+ import { type App, type SearchResultScope } from "@lapis-notes/api";
3
3
  type $$ComponentProps = {
4
4
  app: App;
5
5
  initialQuery?: string;
6
+ initialScope?: SearchResultScope;
7
+ onStateChange?: () => void;
6
8
  };
7
9
  declare const SearchPanel: import("svelte").Component<$$ComponentProps, {
10
+ getSearchState: () => {
11
+ query: string;
12
+ scope: SearchResultScope;
13
+ };
14
+ setSearchScope: (next: SearchResultScope) => void;
8
15
  setSearchQuery: (next: string) => void;
9
16
  }, "">;
10
17
  type SearchPanel = ReturnType<typeof SearchPanel>;
@@ -7,16 +7,22 @@ export declare class SearchPlugin extends Plugin {
7
7
  private startupRefreshPromise;
8
8
  private providerReconcileReason;
9
9
  private providerReconcilePromise;
10
- readonly searchManager: SearchManager;
10
+ private managerStopped;
11
+ private currentSearchManager;
11
12
  constructor(app: App, pluginManifest?: PluginManifest);
13
+ private showNotice;
14
+ get searchManager(): SearchManager;
12
15
  getSettings(): SearchPluginSettings;
13
16
  updateSettings(patch: SearchPluginSettingsPatch): Promise<void>;
17
+ private reconfigureSettings;
14
18
  getSearchStatus(): Promise<SearchRuntimeStatus>;
15
19
  rebuildSemanticSearch(): Promise<SearchRuntimeStatus>;
16
20
  onload(): Promise<void>;
17
21
  refreshIndex(reason?: string): Promise<SearchRuntimeStatus>;
22
+ disable(): Promise<void>;
18
23
  onunload(): Promise<void>;
19
24
  private startupRefresh;
20
25
  private scheduleProviderReconciliation;
21
26
  private openSearchInLeftSidebar;
27
+ private nativeObserverDetached;
22
28
  }