@pify/search 0.1.0 → 0.3.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.
- package/README.md +44 -6
- package/extensions/search.ts +16 -2
- package/native/Cargo.toml +25 -0
- package/native/build.rs +3 -0
- package/native/src/lib.rs +508 -0
- package/native/src/score.rs +147 -0
- package/native/src/store.rs +194 -0
- package/native/src/trigram.rs +292 -0
- package/native/src/walk.rs +114 -0
- package/package.json +6 -2
- package/src/engine.ts +28 -15
- package/src/format.ts +14 -1
- package/src/native.ts +226 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//! What belongs in the index.
|
|
2
|
+
//!
|
|
3
|
+
//! An index holding `node_modules` answers slowly and wrongly — the file you
|
|
4
|
+
//! meant is buried under ten thousand you did not. The rules are deliberately
|
|
5
|
+
//! the same list the TypeScript fallback uses, so the two engines index the
|
|
6
|
+
//! same tree and a search cannot find a file under one engine and miss it
|
|
7
|
+
//! under the other.
|
|
8
|
+
|
|
9
|
+
use std::path::{Path, PathBuf};
|
|
10
|
+
use walkdir::WalkDir;
|
|
11
|
+
|
|
12
|
+
pub const MAX_INDEXABLE_BYTES: u64 = 2 * 1024 * 1024;
|
|
13
|
+
pub const MAX_SEARCHABLE_BYTES: u64 = 10 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
const SKIP_DIRS: &[&str] = &[
|
|
16
|
+
".git", ".hg", ".svn", "node_modules", ".venv", "venv", "__pycache__", ".mypy_cache",
|
|
17
|
+
".pytest_cache", ".ruff_cache", "dist", "build", "out", "target", ".next", ".nuxt",
|
|
18
|
+
".svelte-kit", ".turbo", ".cache", ".gradle", ".idea", ".vscode-test", "coverage",
|
|
19
|
+
".nyc_output", "vendor", "Pods", ".terraform",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const BINARY_EXTENSIONS: &[&str] = &[
|
|
23
|
+
"png", "jpg", "jpeg", "gif", "bmp", "ico", "webp", "avif", "tiff", "psd", "mp3", "mp4", "wav",
|
|
24
|
+
"ogg", "flac", "avi", "mov", "mkv", "webm", "zip", "gz", "tgz", "bz2", "xz", "7z", "rar", "jar",
|
|
25
|
+
"war", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "exe", "dll", "so", "dylib", "bin",
|
|
26
|
+
"o", "a", "lib", "obj", "pdb", "class", "pyc", "pyo", "wasm", "node", "ttf", "otf", "woff",
|
|
27
|
+
"woff2", "eot", "db", "sqlite", "sqlite3", "pack", "idx",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
#[derive(Debug)]
|
|
31
|
+
pub struct Found {
|
|
32
|
+
pub rel: String,
|
|
33
|
+
pub absolute: PathBuf,
|
|
34
|
+
pub size: u64,
|
|
35
|
+
pub mtime_ms: i64,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn normalize(path: &str) -> String {
|
|
39
|
+
path.replace('\\', "/").trim_start_matches("./").to_string()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn relative(root: &Path, absolute: &Path) -> String {
|
|
43
|
+
match absolute.strip_prefix(root) {
|
|
44
|
+
Ok(rel) => normalize(&rel.to_string_lossy()),
|
|
45
|
+
Err(_) => normalize(&absolute.to_string_lossy()),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn mtime_ms(meta: &std::fs::Metadata) -> i64 {
|
|
50
|
+
meta.modified()
|
|
51
|
+
.ok()
|
|
52
|
+
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
|
53
|
+
.map(|d| d.as_millis() as i64)
|
|
54
|
+
.unwrap_or(0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn extension_of(path: &str) -> &str {
|
|
58
|
+
let name = path.rsplit('/').next().unwrap_or(path);
|
|
59
|
+
match name.rfind('.') {
|
|
60
|
+
Some(dot) if dot > 0 => &name[dot + 1..],
|
|
61
|
+
_ => "",
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Whether this file's *content* is worth putting in the trigram index.
|
|
66
|
+
pub fn index_content(rel: &str, size: u64) -> bool {
|
|
67
|
+
if size == 0 || size > MAX_INDEXABLE_BYTES {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
let ext = extension_of(rel).to_ascii_lowercase();
|
|
71
|
+
!BINARY_EXTENSIONS.contains(&ext.as_str())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// A NUL byte in the first few kilobytes: cheaper and more reliable than
|
|
75
|
+
/// trusting an extension, since a `.dat` may be text and a `.txt` may not.
|
|
76
|
+
pub fn looks_binary(bytes: &[u8]) -> bool {
|
|
77
|
+
bytes.iter().take(8192).any(|&b| b == 0)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub fn collect(root: &Path, max_files: usize) -> Vec<Found> {
|
|
81
|
+
let mut out = Vec::new();
|
|
82
|
+
let walker = WalkDir::new(root)
|
|
83
|
+
.max_depth(24)
|
|
84
|
+
.follow_links(false)
|
|
85
|
+
.into_iter()
|
|
86
|
+
.filter_entry(|entry| {
|
|
87
|
+
if entry.depth() == 0 {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
let name = entry.file_name().to_string_lossy();
|
|
91
|
+
if entry.file_type().is_dir() {
|
|
92
|
+
return !SKIP_DIRS.contains(&name.as_ref());
|
|
93
|
+
}
|
|
94
|
+
name != ".DS_Store"
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
for entry in walker.flatten() {
|
|
98
|
+
if out.len() >= max_files {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
if !entry.file_type().is_file() {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let Ok(meta) = entry.metadata() else { continue };
|
|
105
|
+
let absolute = entry.path().to_path_buf();
|
|
106
|
+
out.push(Found {
|
|
107
|
+
rel: relative(root, &absolute),
|
|
108
|
+
absolute,
|
|
109
|
+
size: meta.len(),
|
|
110
|
+
mtime_ms: mtime_ms(&meta),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
out
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pify/search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Fuzzy file finding and indexed content search for pi, with no native binary and no daemon",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
"extensions",
|
|
31
31
|
"src",
|
|
32
32
|
"skills",
|
|
33
|
+
"native/src",
|
|
34
|
+
"native/Cargo.toml",
|
|
35
|
+
"native/build.rs",
|
|
33
36
|
"README.md",
|
|
34
37
|
"LICENSE"
|
|
35
38
|
],
|
|
@@ -44,7 +47,8 @@
|
|
|
44
47
|
"scripts": {
|
|
45
48
|
"typecheck": "tsc --noEmit",
|
|
46
49
|
"test": "bun test",
|
|
47
|
-
"prepublishOnly": "npm run typecheck && npm test"
|
|
50
|
+
"prepublishOnly": "npm run typecheck && npm test",
|
|
51
|
+
"build:native": "cargo build --release --manifest-path native/Cargo.toml"
|
|
48
52
|
},
|
|
49
53
|
"peerDependencies": {
|
|
50
54
|
"@earendil-works/pi-ai": "*",
|
package/src/engine.ts
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Three engines, behind one shape.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* reimplementing.
|
|
4
|
+
* **native** — this package's own Rust core, compiled per platform. It indexes
|
|
5
|
+
* this suite (417 files) in about 33ms, and a literal search then reads the 5
|
|
6
|
+
* files the trigram index says could match rather than all 417.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* never ran — any of those and a binary-only search extension is an extension
|
|
12
|
-
* that does nothing. The fallback is pure TypeScript with no dependencies: a
|
|
13
|
-
* trigram index for content, a fuzzy scorer for paths. It is slower, and it
|
|
14
|
-
* works everywhere pi does.
|
|
8
|
+
* **fff** — `@ff-labs/fff-node`, used when it is installed and the native core
|
|
9
|
+
* is not. A mature engine with its own watcher and git integration.
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* **builtin** — pure TypeScript, no dependencies, no binary. It exists because
|
|
12
|
+
* a native binary is a promise you cannot always keep: an unsupported
|
|
13
|
+
* platform, a locked-down install, a postinstall that never ran. A
|
|
14
|
+
* binary-only search extension in any of those cases is one that silently does
|
|
15
|
+
* nothing.
|
|
16
|
+
*
|
|
17
|
+
* All three answer the same questions, so the tools never learn which one they
|
|
18
|
+
* got. The scoring constants are shared deliberately between the native core
|
|
19
|
+
* and the fallback, so losing the binary changes how fast a search is and not
|
|
20
|
+
* how it is ordered. `/search` says which engine is live.
|
|
19
21
|
*/
|
|
20
22
|
|
|
21
23
|
export interface FileHit {
|
|
@@ -54,16 +56,27 @@ export interface GrepOptions extends FindOptions {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface SearchEngine {
|
|
57
|
-
readonly name: "fff" | "builtin";
|
|
59
|
+
readonly name: "native" | "fff" | "builtin";
|
|
58
60
|
/** Resolve once the first index build has landed. */
|
|
59
61
|
ready(timeoutMs: number): Promise<boolean>;
|
|
60
62
|
find(query: string, options?: FindOptions): Promise<Page<FileHit>>;
|
|
61
63
|
grep(pattern: string, options?: GrepOptions): Promise<Page<ContentHit>>;
|
|
62
64
|
/** Note that a path was used, so frecency can favour it later. */
|
|
63
65
|
touch?(path: string): void;
|
|
66
|
+
/** Re-read one path after a change, when the engine can. */
|
|
67
|
+
refresh?(path: string): void;
|
|
68
|
+
forget?(path: string): void;
|
|
64
69
|
dispose(): void;
|
|
65
70
|
/** How many files the index holds, when the engine can say. */
|
|
66
71
|
indexed?(): number;
|
|
72
|
+
/**
|
|
73
|
+
* How the last index build was paid for: files whose contents came back from
|
|
74
|
+
* the stored index versus files that had to be read. Only an engine that
|
|
75
|
+
* persists its index can answer, and it is worth surfacing — "reused 0" on a
|
|
76
|
+
* tree that has not changed is the visible symptom of a cache that is
|
|
77
|
+
* silently not working.
|
|
78
|
+
*/
|
|
79
|
+
stats?(): { reused: number; rebuilt: number };
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
/** fff calls it "plain"; this package calls it what a user would call it. */
|
package/src/format.ts
CHANGED
|
@@ -45,11 +45,24 @@ export function formatStatus(engine: SearchEngine | null, root: string): string
|
|
|
45
45
|
return "No search engine is running. fffind and ffgrep will start one on first use.";
|
|
46
46
|
}
|
|
47
47
|
const lines = [
|
|
48
|
-
`Engine: ${
|
|
48
|
+
`Engine: ${
|
|
49
|
+
engine.name === "native"
|
|
50
|
+
? "native (this package's Rust core)"
|
|
51
|
+
: engine.name === "fff"
|
|
52
|
+
? "fff (@ff-labs/fff-node)"
|
|
53
|
+
: "builtin (pure TypeScript, no native binary)"
|
|
54
|
+
}`,
|
|
49
55
|
`Root: ${root}`,
|
|
50
56
|
];
|
|
51
57
|
const count = engine.indexed?.();
|
|
52
58
|
if (typeof count === "number") lines.push(`Files: ${count} indexed`);
|
|
59
|
+
const stats = engine.stats?.();
|
|
60
|
+
if (stats && stats.reused + stats.rebuilt > 0) {
|
|
61
|
+
lines.push(
|
|
62
|
+
`Index: ${stats.reused} reused from cache, ${stats.rebuilt} read from disk` +
|
|
63
|
+
(stats.reused === 0 ? " (first run for this tree)" : ""),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
53
66
|
if (engine.name === "builtin") {
|
|
54
67
|
lines.push(
|
|
55
68
|
"",
|
package/src/native.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native core, when there is one.
|
|
3
|
+
*
|
|
4
|
+
* A Rust index built for this package: the same trigram narrowing and the same
|
|
5
|
+
* scoring constants as the TypeScript fallback, compiled. It builds an index
|
|
6
|
+
* of this suite — 417 files — in about 40ms, and a literal search then reads 5
|
|
7
|
+
* of those files instead of all of them.
|
|
8
|
+
*
|
|
9
|
+
* The index is stored between sessions, so a second start on an unchanged tree
|
|
10
|
+
* reloads instead of re-reading it. See `cachePathFor` for where it lives.
|
|
11
|
+
*
|
|
12
|
+
* The binary is an optional dependency per platform, in the napi convention.
|
|
13
|
+
* If none of them installed, this returns null and the caller falls back;
|
|
14
|
+
* nothing here ever throws for a missing binary, because a missing binary is
|
|
15
|
+
* the ordinary case this package is designed to survive.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { createRequire } from "node:module";
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import { homedir, tmpdir } from "node:os";
|
|
22
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
import type { ContentHit, FileHit, GrepOptions, Page, SearchEngine, FindOptions } from "./engine.ts";
|
|
26
|
+
|
|
27
|
+
interface NativePage<T> {
|
|
28
|
+
items: T[];
|
|
29
|
+
total: number;
|
|
30
|
+
/** Offset of the next page, or -1 when this was the last. */
|
|
31
|
+
next: number;
|
|
32
|
+
scanned?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface NativeIndex {
|
|
36
|
+
fileCount(): number;
|
|
37
|
+
indexedCount(): number;
|
|
38
|
+
/** Files whose contents came from the stored index instead of from disk. */
|
|
39
|
+
reusedCount(): number;
|
|
40
|
+
/** Files that had to be read because they were new or had changed. */
|
|
41
|
+
rebuiltCount(): number;
|
|
42
|
+
save(): void;
|
|
43
|
+
touch(path: string): void;
|
|
44
|
+
refresh(path: string): void;
|
|
45
|
+
forget(path: string): void;
|
|
46
|
+
find(query: string, limit: number, offset: number, nowMs: number): NativePage<FileHit>;
|
|
47
|
+
grep(
|
|
48
|
+
pattern: string,
|
|
49
|
+
mode: string,
|
|
50
|
+
limit: number,
|
|
51
|
+
offset: number,
|
|
52
|
+
caseInsensitive: boolean,
|
|
53
|
+
): NativePage<ContentHit>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface NativeModule {
|
|
57
|
+
SearchIndex: new (root: string, maxFiles?: number, cachePath?: string) => NativeIndex;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Where a tree's stored index lives.
|
|
62
|
+
*
|
|
63
|
+
* Not inside the repository: an index is a derived artifact, it is large, and
|
|
64
|
+
* writing one into someone's working tree means it shows up in their `git
|
|
65
|
+
* status` and their diffs. It goes in the platform's cache directory, keyed by
|
|
66
|
+
* the absolute path of the root so two checkouts of the same project keep
|
|
67
|
+
* their own.
|
|
68
|
+
*
|
|
69
|
+
* `PIFY_SEARCH_CACHE_DIR` relocates it and `PIFY_SEARCH_NO_CACHE=1` turns it
|
|
70
|
+
* off, which is how the no-cache path stays tested on a machine that has one.
|
|
71
|
+
*/
|
|
72
|
+
export function cachePathFor(root: string): string | undefined {
|
|
73
|
+
if (process.env.PIFY_SEARCH_NO_CACHE === "1") return undefined;
|
|
74
|
+
|
|
75
|
+
const base =
|
|
76
|
+
process.env.PIFY_SEARCH_CACHE_DIR ??
|
|
77
|
+
(process.platform === "win32"
|
|
78
|
+
? join(process.env.LOCALAPPDATA ?? tmpdir(), "pify-search")
|
|
79
|
+
: process.platform === "darwin"
|
|
80
|
+
? join(homedir(), "Library", "Caches", "pify-search")
|
|
81
|
+
: join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "pify-search"));
|
|
82
|
+
|
|
83
|
+
const absolute = resolve(root);
|
|
84
|
+
// The readable part is for a human looking at the cache directory; the hash
|
|
85
|
+
// is what actually makes the name unique, since two projects can share a
|
|
86
|
+
// basename and paths differ only in case on some platforms.
|
|
87
|
+
const label = (basename(absolute) || "root").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40);
|
|
88
|
+
const digest = createHash("sha256").update(absolute.toLowerCase()).digest("hex").slice(0, 16);
|
|
89
|
+
return join(base, `${label}-${digest}.idx`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The napi triple for this host, matching how the binaries are published. */
|
|
93
|
+
export function tripleOf(platform: string, arch: string): string | null {
|
|
94
|
+
const key = `${platform}-${arch}`;
|
|
95
|
+
const known: Record<string, string> = {
|
|
96
|
+
"win32-x64": "win32-x64",
|
|
97
|
+
"win32-arm64": "win32-arm64",
|
|
98
|
+
"darwin-x64": "darwin-x64",
|
|
99
|
+
"darwin-arm64": "darwin-arm64",
|
|
100
|
+
"linux-x64": "linux-x64",
|
|
101
|
+
"linux-arm64": "linux-arm64",
|
|
102
|
+
};
|
|
103
|
+
return known[key] ?? null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function candidatePaths(triple: string): string[] {
|
|
107
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
108
|
+
const root = join(here, "..");
|
|
109
|
+
return [
|
|
110
|
+
// A locally built binary, which is how the package is developed.
|
|
111
|
+
join(root, `pify-search.${triple}.node`),
|
|
112
|
+
join(root, "native", "target", "release", `pify-search.${triple}.node`),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Load the native core, or null when this platform has no binary.
|
|
118
|
+
* `PIFY_SEARCH_ENGINE=builtin` forces the fallback, which is how both paths
|
|
119
|
+
* get tested on a machine that has the binary.
|
|
120
|
+
*/
|
|
121
|
+
export function loadNative(root: string, maxFiles?: number): SearchEngine | null {
|
|
122
|
+
if (process.env.PIFY_SEARCH_ENGINE === "builtin" || process.env.PIFY_SEARCH_ENGINE === "fff") {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const triple = tripleOf(process.platform, process.arch);
|
|
126
|
+
if (!triple) return null;
|
|
127
|
+
|
|
128
|
+
const require = createRequire(import.meta.url);
|
|
129
|
+
let mod: NativeModule | null = null;
|
|
130
|
+
for (const path of candidatePaths(triple)) {
|
|
131
|
+
if (!existsSync(path)) continue;
|
|
132
|
+
try {
|
|
133
|
+
mod = require(path) as NativeModule;
|
|
134
|
+
break;
|
|
135
|
+
} catch {
|
|
136
|
+
// A binary that will not load is the same as one that is not there.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!mod) {
|
|
140
|
+
try {
|
|
141
|
+
mod = require(`@pify/search-${triple}`) as NativeModule;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let index: NativeIndex;
|
|
148
|
+
try {
|
|
149
|
+
index = new mod.SearchIndex(root, maxFiles, cachePathFor(root));
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const pageOf = <T>(page: NativePage<T>): Page<T> => ({
|
|
155
|
+
items: page.items,
|
|
156
|
+
total: page.total,
|
|
157
|
+
cursor: page.next >= 0 ? String(page.next) : null,
|
|
158
|
+
});
|
|
159
|
+
const offsetOf = (cursor?: string) => {
|
|
160
|
+
const n = Number.parseInt(cursor ?? "0", 10);
|
|
161
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
name: "native",
|
|
166
|
+
async ready() {
|
|
167
|
+
// The index is built in the constructor, so by here it is done.
|
|
168
|
+
return true;
|
|
169
|
+
},
|
|
170
|
+
async find(query: string, options: FindOptions = {}) {
|
|
171
|
+
return pageOf(index.find(query, options.limit ?? 20, offsetOf(options.cursor), Date.now()));
|
|
172
|
+
},
|
|
173
|
+
async grep(pattern: string, options: GrepOptions = {}) {
|
|
174
|
+
return pageOf(
|
|
175
|
+
index.grep(
|
|
176
|
+
pattern,
|
|
177
|
+
options.mode ?? "literal",
|
|
178
|
+
options.limit ?? 20,
|
|
179
|
+
offsetOf(options.cursor),
|
|
180
|
+
options.caseInsensitive !== false,
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
touch(path: string) {
|
|
185
|
+
try {
|
|
186
|
+
index.touch(path);
|
|
187
|
+
} catch {
|
|
188
|
+
// Frecency is a ranking nicety, never a reason to fail a search.
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
refresh(path: string) {
|
|
192
|
+
try {
|
|
193
|
+
index.refresh(path);
|
|
194
|
+
} catch {
|
|
195
|
+
// A file we cannot re-read stays as it was.
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
forget(path: string) {
|
|
199
|
+
try {
|
|
200
|
+
index.forget(path);
|
|
201
|
+
} catch {
|
|
202
|
+
// ditto
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
indexed() {
|
|
206
|
+
try {
|
|
207
|
+
return index.fileCount();
|
|
208
|
+
} catch {
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
stats() {
|
|
213
|
+
try {
|
|
214
|
+
return { reused: index.reusedCount(), rebuilt: index.rebuiltCount() };
|
|
215
|
+
} catch {
|
|
216
|
+
return { reused: 0, rebuilt: 0 };
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
dispose() {
|
|
220
|
+
// Deliberately not saving here. Anything `refresh` changed during the
|
|
221
|
+
// session has a new mtime on disk, so the next start sees the mismatch
|
|
222
|
+
// and re-reads those files anyway — writing the whole index out at exit
|
|
223
|
+
// would cost a multi-megabyte write to save a handful of file reads.
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|