@lapis-notes/search 0.1.0

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE.md +8 -0
  3. package/README.md +20 -0
  4. package/dist/agent-tools.d.ts +1 -0
  5. package/dist/agent-tools.js +1 -0
  6. package/dist/built-in-search-document-providers.d.ts +5 -0
  7. package/dist/built-in-search-document-providers.js +30 -0
  8. package/dist/highlighted-text.svelte +41 -0
  9. package/dist/highlighted-text.svelte.d.ts +11 -0
  10. package/dist/index.d.ts +11 -0
  11. package/dist/index.js +9 -0
  12. package/dist/notes-search-command.d.ts +2 -0
  13. package/dist/notes-search-command.js +8 -0
  14. package/dist/notes-search-tool.d.ts +8 -0
  15. package/dist/notes-search-tool.js +84 -0
  16. package/dist/public-components.d.ts +2 -0
  17. package/dist/public-components.js +2 -0
  18. package/dist/search-manager.d.ts +92 -0
  19. package/dist/search-manager.js +771 -0
  20. package/dist/search-match-context.d.ts +17 -0
  21. package/dist/search-match-context.js +45 -0
  22. package/dist/search-panel.css +460 -0
  23. package/dist/search-panel.svelte +796 -0
  24. package/dist/search-panel.svelte.d.ts +11 -0
  25. package/dist/search-plugin.d.ts +22 -0
  26. package/dist/search-plugin.js +174 -0
  27. package/dist/search-result-model.d.ts +29 -0
  28. package/dist/search-result-model.js +50 -0
  29. package/dist/search-settings-tab.d.ts +12 -0
  30. package/dist/search-settings-tab.js +139 -0
  31. package/dist/search-settings.d.ts +54 -0
  32. package/dist/search-settings.js +107 -0
  33. package/dist/search-sort.d.ts +8 -0
  34. package/dist/search-sort.js +36 -0
  35. package/dist/search-startup.d.ts +3 -0
  36. package/dist/search-startup.js +4 -0
  37. package/dist/search-tool-result.css +70 -0
  38. package/dist/search-tool-result.d.ts +16 -0
  39. package/dist/search-tool-result.js +111 -0
  40. package/dist/search-tool-result.svelte +55 -0
  41. package/dist/search-tool-result.svelte.d.ts +6 -0
  42. package/dist/search-view-type.d.ts +1 -0
  43. package/dist/search-view-type.js +1 -0
  44. package/dist/search-view.d.ts +14 -0
  45. package/dist/search-view.js +48 -0
  46. package/dist/styles.css +2 -0
  47. package/manifest.json +28 -0
  48. package/package.json +80 -0
  49. package/styles.css +1 -0
@@ -0,0 +1,70 @@
1
+ [data-ui-component="search-tool-result"] {
2
+ display: flex;
3
+ width: 100%;
4
+ min-width: 0;
5
+ flex-direction: column;
6
+ gap: 0.375rem;
7
+ }
8
+
9
+ [data-ui-component="search-tool-result"] [data-ui-part="empty"] {
10
+ padding: 0.375rem;
11
+ color: var(--muted-foreground);
12
+ font-size: 0.8125rem;
13
+ }
14
+
15
+ [data-ui-component="search-tool-result"] [data-ui-part="hit"] {
16
+ display: flex;
17
+ min-width: 0;
18
+ flex-direction: column;
19
+ gap: 0.25rem;
20
+ }
21
+
22
+ [data-ui-component="search-tool-result"] [data-ui-part="file"],
23
+ [data-ui-component="search-tool-result"] [data-ui-part="snippet"] {
24
+ box-sizing: border-box;
25
+ width: 100%;
26
+ min-width: 0;
27
+ border: 0;
28
+ border-radius: var(--ui-workspace-radius-small, 0.25rem);
29
+ padding: 0.375rem 0.5rem;
30
+ color: inherit;
31
+ background: transparent;
32
+ font: inherit;
33
+ text-align: start;
34
+ cursor: pointer;
35
+ }
36
+
37
+ [data-ui-component="search-tool-result"] [data-ui-part="file"]:hover,
38
+ [data-ui-component="search-tool-result"] [data-ui-part="file"]:focus-visible,
39
+ [data-ui-component="search-tool-result"] [data-ui-part="snippet"]:hover,
40
+ [data-ui-component="search-tool-result"] [data-ui-part="snippet"]:focus-visible {
41
+ background: var(
42
+ --ui-workspace-explorer-row-hover-background,
43
+ var(--sidebar-accent)
44
+ );
45
+ outline: 2px solid transparent;
46
+ }
47
+
48
+ [data-ui-component="search-tool-result"] [data-ui-part="file-label"] {
49
+ overflow: hidden;
50
+ font-weight: 600;
51
+ text-overflow: ellipsis;
52
+ white-space: nowrap;
53
+ }
54
+
55
+ [data-ui-component="search-tool-result"] [data-ui-part="file-path"] {
56
+ overflow: hidden;
57
+ color: var(--muted-foreground);
58
+ font-size: 0.75rem;
59
+ text-overflow: ellipsis;
60
+ white-space: nowrap;
61
+ }
62
+
63
+ [data-ui-component="search-tool-result"] [data-ui-part="snippet-text"] {
64
+ display: -webkit-box;
65
+ overflow: hidden;
66
+ -webkit-box-orient: vertical;
67
+ -webkit-line-clamp: 3;
68
+ font-size: 0.8125rem;
69
+ line-height: 1.35;
70
+ }
@@ -0,0 +1,16 @@
1
+ import { type App, type EditorPosition } from "@lapis-notes/api";
2
+ import type { AgentResultViewProps } from "@lapis-notes/api";
3
+ export type NotesSearchSnippet = {
4
+ text: string;
5
+ offset: number;
6
+ };
7
+ export type NotesSearchHit = {
8
+ path: string;
9
+ score: number;
10
+ snippets: NotesSearchSnippet[];
11
+ };
12
+ export declare function notesSearchHitsFromOutput(output: unknown): NotesSearchHit[];
13
+ export declare function fileNameFromPath(path: string): string;
14
+ export declare function editorPositionFromOffset(source: string, offset: number): EditorPosition;
15
+ export declare function openNotesSearchHit(app: App, hit: NotesSearchHit, snippet?: NotesSearchSnippet): Promise<void>;
16
+ export declare function searchToolResultProps(props: AgentResultViewProps<App>): NotesSearchHit[];
@@ -0,0 +1,111 @@
1
+ import { FileView, TextFileView, TFile, } from "@lapis-notes/api";
2
+ function asRecord(value) {
3
+ return value && typeof value === "object" && !Array.isArray(value)
4
+ ? value
5
+ : undefined;
6
+ }
7
+ function parseJson(value) {
8
+ if (typeof value !== "string")
9
+ return value;
10
+ try {
11
+ return JSON.parse(value);
12
+ }
13
+ catch {
14
+ return value;
15
+ }
16
+ }
17
+ function normalizeHits(value) {
18
+ if (!Array.isArray(value))
19
+ return [];
20
+ const hits = [];
21
+ for (const entry of value) {
22
+ const record = asRecord(entry);
23
+ if (!record)
24
+ continue;
25
+ const path = record.path;
26
+ if (typeof path !== "string" || !path.trim())
27
+ continue;
28
+ const snippets = Array.isArray(record.snippets)
29
+ ? record.snippets.flatMap((snippet) => {
30
+ const item = asRecord(snippet);
31
+ if (!item || typeof item.text !== "string")
32
+ return [];
33
+ return [
34
+ {
35
+ text: item.text,
36
+ offset: typeof item.offset === "number" && Number.isFinite(item.offset)
37
+ ? Math.max(0, item.offset)
38
+ : 0,
39
+ },
40
+ ];
41
+ })
42
+ : [];
43
+ hits.push({
44
+ path: path.trim(),
45
+ score: typeof record.score === "number" && Number.isFinite(record.score)
46
+ ? record.score
47
+ : 0,
48
+ snippets,
49
+ });
50
+ }
51
+ return hits;
52
+ }
53
+ function resultsFromRecord(record) {
54
+ if (!record)
55
+ return [];
56
+ const direct = normalizeHits(record.results);
57
+ if (direct.length > 0)
58
+ return direct;
59
+ const structured = resultsFromRecord(asRecord(record.structuredContent));
60
+ if (structured.length > 0)
61
+ return structured;
62
+ const content = record.content;
63
+ if (!Array.isArray(content))
64
+ return [];
65
+ for (const item of content) {
66
+ const entry = asRecord(item);
67
+ if (typeof entry?.text !== "string")
68
+ continue;
69
+ const nested = resultsFromRecord(asRecord(parseJson(entry.text)));
70
+ if (nested.length > 0)
71
+ return nested;
72
+ }
73
+ return [];
74
+ }
75
+ export function notesSearchHitsFromOutput(output) {
76
+ return resultsFromRecord(asRecord(parseJson(output)));
77
+ }
78
+ export function fileNameFromPath(path) {
79
+ return path.split("/").at(-1) || path;
80
+ }
81
+ export function editorPositionFromOffset(source, offset) {
82
+ const clamped = Math.max(0, Math.min(offset, source.length));
83
+ const before = source.slice(0, clamped);
84
+ const line = before.split("\n").length - 1;
85
+ const ch = before.length - (before.lastIndexOf("\n") + 1);
86
+ return { line, ch };
87
+ }
88
+ export async function openNotesSearchHit(app, hit, snippet) {
89
+ const file = app.vault.getAbstractFileByPath(hit.path);
90
+ if (!(file instanceof TFile))
91
+ return;
92
+ const selected = app.workspace.getMostRecentLeaf() ?? app.workspace.activeLeaf;
93
+ const leaf = selected && selected.view instanceof FileView
94
+ ? selected
95
+ : app.workspace.getLeaf("tab");
96
+ await leaf.openFile(file);
97
+ app.workspace.activeLeaf = leaf;
98
+ app.workspace.revealLeaf(leaf);
99
+ if (!snippet || !(leaf.view instanceof TextFileView))
100
+ return;
101
+ try {
102
+ const source = await app.vault.cachedRead(file);
103
+ leaf.view.editor.setCursor(editorPositionFromOffset(source, snippet.offset));
104
+ }
105
+ catch {
106
+ // Opening the file is enough when the editor is not ready.
107
+ }
108
+ }
109
+ export function searchToolResultProps(props) {
110
+ return notesSearchHitsFromOutput(props.output);
111
+ }
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ import type { AgentResultViewProps } from "@lapis-notes/api";
3
+ import type { App } from "@lapis-notes/api";
4
+ import {
5
+ fileNameFromPath,
6
+ notesSearchHitsFromOutput,
7
+ openNotesSearchHit,
8
+ type NotesSearchHit,
9
+ type NotesSearchSnippet,
10
+ } from "./search-tool-result";
11
+ import "./search-tool-result.css";
12
+
13
+ let {
14
+ app,
15
+ output,
16
+ }: AgentResultViewProps<App> = $props();
17
+
18
+ const hits = $derived(notesSearchHitsFromOutput(output));
19
+
20
+ async function openHit(
21
+ hit: NotesSearchHit,
22
+ snippet?: NotesSearchSnippet,
23
+ ): Promise<void> {
24
+ await openNotesSearchHit(app, hit, snippet);
25
+ }
26
+ </script>
27
+
28
+ <div data-ui-component="search-tool-result">
29
+ {#if hits.length === 0}
30
+ <p data-ui-part="empty">No matching notes.</p>
31
+ {:else}
32
+ {#each hits as hit (hit.path)}
33
+ <div data-ui-part="hit">
34
+ <button
35
+ type="button"
36
+ data-ui-part="file"
37
+ aria-label={`Open ${fileNameFromPath(hit.path)}`}
38
+ onclick={() => void openHit(hit)}
39
+ >
40
+ <span data-ui-part="file-label">{fileNameFromPath(hit.path)}</span>
41
+ <span data-ui-part="file-path">{hit.path}</span>
42
+ </button>
43
+ {#each hit.snippets as snippet, index (`${hit.path}:${snippet.offset}:${index}`)}
44
+ <button
45
+ type="button"
46
+ data-ui-part="snippet"
47
+ onclick={() => void openHit(hit, snippet)}
48
+ >
49
+ <span data-ui-part="snippet-text">{snippet.text}</span>
50
+ </button>
51
+ {/each}
52
+ </div>
53
+ {/each}
54
+ {/if}
55
+ </div>
@@ -0,0 +1,6 @@
1
+ import type { AgentResultViewProps } from "@lapis-notes/api";
2
+ import type { App } from "@lapis-notes/api";
3
+ import "./search-tool-result.css";
4
+ declare const SearchToolResult: import("svelte").Component<AgentResultViewProps<App>, {}, "">;
5
+ type SearchToolResult = ReturnType<typeof SearchToolResult>;
6
+ export default SearchToolResult;
@@ -0,0 +1 @@
1
+ export declare const SearchViewType = "search";
@@ -0,0 +1 @@
1
+ export const SearchViewType = "search";
@@ -0,0 +1,14 @@
1
+ import { View, type WorkspaceLeaf } from "@lapis-notes/api";
2
+ export { SearchViewType } from "./search-view-type";
3
+ export declare class SearchView extends View {
4
+ private component;
5
+ constructor(leaf: WorkspaceLeaf);
6
+ getViewType(): string;
7
+ getDisplayText(): string;
8
+ getIcon(): string;
9
+ protected onOpen(): Promise<void>;
10
+ protected onClose(): Promise<void>;
11
+ onload(): void;
12
+ setState(state: Record<string, unknown>): Promise<void>;
13
+ onunload(): void;
14
+ }
@@ -0,0 +1,48 @@
1
+ import { View } from "@lapis-notes/api";
2
+ import { mount, unmount } from "svelte";
3
+ import SearchPanel from "./search-panel.svelte";
4
+ import { SearchViewType } from "./search-view-type";
5
+ export { SearchViewType } from "./search-view-type";
6
+ export class SearchView extends View {
7
+ component = null;
8
+ constructor(leaf) {
9
+ super(leaf);
10
+ }
11
+ getViewType() {
12
+ return SearchViewType;
13
+ }
14
+ getDisplayText() {
15
+ return "Search";
16
+ }
17
+ getIcon() {
18
+ return "search";
19
+ }
20
+ onOpen() {
21
+ return Promise.resolve();
22
+ }
23
+ onClose() {
24
+ return Promise.resolve();
25
+ }
26
+ onload() {
27
+ this.component = mount(SearchPanel, {
28
+ target: this.containerEl,
29
+ props: {
30
+ app: this.app,
31
+ initialQuery: typeof this.getState()["query"] === "string"
32
+ ? this.getState()["query"]
33
+ : "",
34
+ },
35
+ });
36
+ }
37
+ async setState(state) {
38
+ await super.setState(state);
39
+ if (typeof state["query"] === "string") {
40
+ this.component?.setSearchQuery?.(state["query"]);
41
+ }
42
+ }
43
+ onunload() {
44
+ if (this.component)
45
+ void unmount(this.component);
46
+ this.component = null;
47
+ }
48
+ }
@@ -0,0 +1,2 @@
1
+ @import "./search-panel.css";
2
+ @import "./search-tool-result.css";
package/manifest.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "id": "search",
3
+ "name": "Search",
4
+ "version": "0.1.0",
5
+ "minAppVersion": "0.1.0",
6
+ "description": "Search files, metadata, tags, and note content.",
7
+ "author": "Lapis Notes",
8
+ "lapis": {
9
+ "manifestVersion": 1,
10
+ "source": "official",
11
+ "locked": false,
12
+ "enabledByDefault": true,
13
+ "runtime": {
14
+ "entries": {
15
+ "workspace": {
16
+ "path": "main.mjs",
17
+ "format": "esm",
18
+ "sharedDependencies": [
19
+ "@lapis-notes/api",
20
+ "@lapis-notes/api/agent-skills",
21
+ "@lapis-notes/api/agent-tools"
22
+ ],
23
+ "requiresReloadOnUpdate": false
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@lapis-notes/search",
3
+ "version": "0.1.0",
4
+ "description": "Lapis Notes vault search plugin",
5
+ "license": "AGPL-3.0-or-later",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "manifest.json",
10
+ "styles.css",
11
+ "README.md",
12
+ "CHANGELOG.md",
13
+ "LICENSE.md",
14
+ "!dist/**/*.test.*",
15
+ "!dist/**/*.spec.*"
16
+ ],
17
+ "sideEffects": [
18
+ "**/*.css"
19
+ ],
20
+ "svelte": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "svelte": "./dist/index.js",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "./agent-tools": {
30
+ "types": "./dist/agent-tools.d.ts",
31
+ "import": "./dist/agent-tools.js",
32
+ "default": "./dist/agent-tools.js"
33
+ },
34
+ "./manifest.json": "./manifest.json",
35
+ "./styles.css": "./styles.css"
36
+ },
37
+ "dependencies": {
38
+ "@lapis-notes/api": "^0.1.0",
39
+ "@lapismd/design-core": "^0.1.0",
40
+ "@lucide/svelte": "^0.523.0"
41
+ },
42
+ "peerDependencies": {
43
+ "@codemirror/language": "^6.11.1",
44
+ "@codemirror/state": "^6.4.1",
45
+ "@codemirror/view": "^6.35.0",
46
+ "svelte": "^5.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@sveltejs/package": "^2.5.0",
50
+ "@sveltejs/vite-plugin-svelte": "^5.1.1",
51
+ "@tsconfig/svelte": "^5.0.5",
52
+ "publint": "^0.3.12",
53
+ "svelte": "^5.38.2",
54
+ "svelte-check": "^4.3.1",
55
+ "typescript": "~5.8.3",
56
+ "vite": "^6.4.2",
57
+ "vitest": "^4.1.8"
58
+ },
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/lapismd/lapis-plugins.git",
62
+ "directory": "packages/search"
63
+ },
64
+ "homepage": "https://github.com/lapismd/lapis-plugins/tree/main/packages/search#readme",
65
+ "bugs": {
66
+ "url": "https://github.com/lapismd/lapis-plugins/issues"
67
+ },
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "scripts": {
72
+ "build": "pnpm run prepack",
73
+ "check": "pnpm check:svelte && pnpm check:types",
74
+ "check:svelte": "svelte-check --tsconfig ./tsconfig.app.json --fail-on-warnings --output human",
75
+ "check:types": "tsc -p tsconfig.node.json --noEmit",
76
+ "test": "vitest run --passWithNoTests",
77
+ "publint": "publint",
78
+ "pack": "pnpm pack --pack-destination ../../.release/npm"
79
+ }
80
+ }
package/styles.css ADDED
@@ -0,0 +1 @@
1
+ @import "./dist/styles.css";