diffwiki-ripgrep 0.14.0 → 0.15.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 (2) hide show
  1. package/dist/bin.js +129 -60
  2. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin.ts
4
- import { definePlugin } from "diffwiki-core/plugin";
4
+ import { definePlugin, ensureTool } from "diffwiki-core/plugin";
5
5
 
6
6
  // src/ripgrep.ts
7
7
  import { spawn } from "child_process";
@@ -22,6 +22,22 @@ var spawnRg = (args) => new Promise((resolve, reject) => {
22
22
  }
23
23
  });
24
24
  });
25
+ var spawnFzf = (args, input) => new Promise((resolve, reject) => {
26
+ const chunks = [];
27
+ const proc = spawn("fzf", args, { stdio: ["pipe", "pipe", "ignore"] });
28
+ proc.stdout.on("data", (chunk) => chunks.push(chunk));
29
+ proc.on("error", reject);
30
+ proc.on("close", (code) => {
31
+ if (code === 0 || code === 1) {
32
+ resolve(Buffer.concat(chunks).toString("utf8"));
33
+ } else {
34
+ reject(new Error(`fzf exited with code ${code}`));
35
+ }
36
+ });
37
+ proc.stdin.on("error", () => {
38
+ });
39
+ proc.stdin.end(input);
40
+ });
25
41
  var readFileLines = (absPath) => new Promise((resolve, reject) => {
26
42
  const lines = [];
27
43
  const stream = createReadStream(absPath, { encoding: "utf8" });
@@ -37,26 +53,61 @@ var readFileLines = (absPath) => new Promise((resolve, reject) => {
37
53
  rl.on("error", reject);
38
54
  stream.on("error", reject);
39
55
  });
40
- async function rgOnPath(spawnFn = spawnRg) {
41
- if (spawnFn !== spawnRg) {
56
+ async function toolOnPath(name, probe) {
57
+ if (probe) {
42
58
  try {
43
- await spawnFn(["--version"]);
59
+ await probe();
44
60
  return true;
45
61
  } catch {
46
62
  return false;
47
63
  }
48
64
  }
49
- return commandExists("rg");
65
+ return commandExists(name);
66
+ }
67
+ async function rgOnPath(spawnFn = spawnRg) {
68
+ return toolOnPath("rg", spawnFn === spawnRg ? null : () => spawnFn(["--version"]));
69
+ }
70
+ async function fzfOnPath(fzfFn = spawnFzf) {
71
+ return toolOnPath("fzf", fzfFn === spawnFzf ? null : () => fzfFn(["--version"], ""));
50
72
  }
51
- async function runRg(spawnFn, pattern, roots, opts) {
73
+ function tokenize(query) {
74
+ return query.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? [];
75
+ }
76
+ function escapeRegex(token) {
77
+ return token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
78
+ }
79
+ async function runRg(spawnFn, query, roots, opts) {
80
+ const tokens = tokenize(query);
81
+ if (tokens.length === 0 || roots.length === 0) return "";
82
+ const pattern = tokens.map(escapeRegex).join("|");
52
83
  const limit = opts.limit > 0 ? opts.limit : 20;
53
- const args = ["--json", "--line-number", "--max-count", String(limit), "--glob", "*.{md,mdx}"];
54
- if (opts.fixedStrings) {
55
- args.push("--fixed-strings", "-i");
56
- }
57
- args.push("--", pattern, ...roots);
84
+ const args = [
85
+ "--line-number",
86
+ "--no-heading",
87
+ "--with-filename",
88
+ "--color",
89
+ "never",
90
+ "--smart-case",
91
+ "--max-count",
92
+ String(limit),
93
+ "--glob",
94
+ "*.{md,mdx}",
95
+ "-e",
96
+ pattern,
97
+ "--",
98
+ ...roots
99
+ ];
58
100
  return spawnFn(args);
59
101
  }
102
+ async function fuzzyRank(fzfFn, candidates, query) {
103
+ if (candidates.trim().length === 0) return "";
104
+ return fzfFn(["--filter", query], candidates);
105
+ }
106
+ function parseRgLine(line) {
107
+ const m = /^(.*?):(\d+):(.*)$/.exec(line);
108
+ if (!m) return null;
109
+ return { path: m[1], lineNumber: Number(m[2]), text: m[3] };
110
+ }
60
111
  async function cheapTitle(absPath, readFn = readFileLines) {
61
112
  let lines;
62
113
  try {
@@ -80,7 +131,9 @@ async function cheapTitle(absPath, readFn = readFileLines) {
80
131
  }
81
132
 
82
133
  // src/bin.ts
83
- var VERSION = "0.3.0";
134
+ var VERSION = "0.4.0";
135
+ var RG_INSTALL = "https://github.com/BurntSushi/ripgrep";
136
+ var FZF_INSTALL = "https://github.com/junegunn/fzf";
84
137
  function asCollections(raw) {
85
138
  if (!Array.isArray(raw)) return [];
86
139
  return raw.filter(
@@ -100,27 +153,19 @@ function matchCollection(filePath, collections) {
100
153
  }
101
154
  return null;
102
155
  }
103
- function parseRgOutput(stdout) {
104
- return stdout.split("\n").filter((line) => line.trim().length > 0).flatMap((line) => {
105
- try {
106
- const obj = JSON.parse(line);
107
- return obj.type === "match" ? [obj] : [];
108
- } catch {
109
- return [];
110
- }
111
- });
112
- }
113
156
  function makePluginDefinition(deps = {}) {
114
157
  const spawnFn = deps.spawnFn ?? spawnRg;
158
+ const fzfFn = deps.fzfFn ?? spawnFzf;
115
159
  const readFn = deps.readFn ?? readFileLines;
160
+ const installFn = deps.installFn ?? ensureTool;
161
+ const RG_SPEC = { command: "rg", mise: "ripgrep", brew: "ripgrep", apt: "ripgrep", manual: RG_INSTALL };
162
+ const FZF_SPEC = { command: "fzf", mise: "fzf", brew: "fzf", apt: "fzf", manual: FZF_INSTALL };
116
163
  const handlers = {
117
164
  async search(params) {
118
165
  const term = params.term ?? "";
119
166
  const collections = asCollections(params.collections);
120
167
  const collectionFilter = typeof params.collection === "string" && params.collection.length > 0 ? params.collection : null;
121
- const type = params.type ?? "text";
122
168
  const limit = params.limit && params.limit > 0 ? params.limit : 20;
123
- const fixedStrings = type !== "regex";
124
169
  let roots;
125
170
  let activeCollections;
126
171
  if (collectionFilter !== null) {
@@ -131,22 +176,35 @@ function makePluginDefinition(deps = {}) {
131
176
  roots = collections.map((c) => c.path);
132
177
  activeCollections = collections;
133
178
  }
134
- if (roots.length === 0 || term.length === 0) {
179
+ if (roots.length === 0 || term.trim().length === 0) {
135
180
  return { hits: [] };
136
181
  }
137
- const stdout = await runRg(spawnFn, term, roots, { fixedStrings, limit });
138
- const events = parseRgOutput(stdout);
182
+ const candidates = await runRg(spawnFn, term, roots, { limit });
183
+ if (candidates.trim().length === 0) {
184
+ return { hits: [] };
185
+ }
186
+ let ranked = candidates;
187
+ let fuzzyRanked = false;
188
+ try {
189
+ const fzfOut = await fuzzyRank(fzfFn, candidates, term);
190
+ if (fzfOut.trim().length > 0) {
191
+ ranked = fzfOut;
192
+ fuzzyRanked = true;
193
+ }
194
+ } catch {
195
+ }
139
196
  const seen = /* @__PURE__ */ new Set();
140
197
  const hits = [];
141
- for (const event of events) {
198
+ for (const line of ranked.split("\n")) {
142
199
  if (hits.length >= limit) break;
143
- const filePath = event.data.path.text;
144
- if (seen.has(filePath)) continue;
145
- seen.add(filePath);
146
- const matched = matchCollection(filePath, activeCollections);
200
+ const parsed = parseRgLine(line);
201
+ if (!parsed) continue;
202
+ if (seen.has(parsed.path)) continue;
203
+ seen.add(parsed.path);
204
+ const matched = matchCollection(parsed.path, activeCollections);
147
205
  if (!matched) continue;
148
- const title = await cheapTitle(filePath, readFn);
149
- const snippet = (event.data.lines.text || "").trim();
206
+ const title = await cheapTitle(parsed.path, readFn);
207
+ const snippet = parsed.text.trim();
150
208
  hits.push({
151
209
  collection: matched.collection,
152
210
  relPath: matched.relPath,
@@ -154,36 +212,53 @@ function makePluginDefinition(deps = {}) {
154
212
  snippet: snippet.length > 0 ? snippet : void 0
155
213
  });
156
214
  }
215
+ if (fuzzyRanked && hits.length > 0) {
216
+ const total = hits.length;
217
+ hits.forEach((hit, i) => {
218
+ hit.score = Number(((total - i) / total).toFixed(3));
219
+ });
220
+ }
157
221
  return { hits };
158
222
  },
159
223
  async index() {
160
224
  return { indexed: 0 };
161
225
  },
162
226
  async setup(_params, log) {
163
- const rgPresent = await rgOnPath(spawnFn);
164
- if (!rgPresent) {
165
- log("error", "ripgrep (rg) not found \u2014 install it: https://github.com/BurntSushi/ripgrep");
166
- return {
167
- ready: false,
168
- steps: [
169
- {
170
- step: "verify-rg",
171
- ok: false,
172
- message: "rg not on PATH \u2014 install ripgrep: https://github.com/BurntSushi/ripgrep"
173
- }
174
- ]
175
- };
227
+ const steps = [];
228
+ const rg = await installFn(RG_SPEC, { install: true, log });
229
+ if (!rg.present) {
230
+ log("error", `ripgrep (rg) unavailable \u2014 ${rg.message}`);
231
+ steps.push({ step: "verify-rg", ok: false, message: rg.message });
232
+ return { ready: false, steps };
176
233
  }
177
- return {
178
- ready: true,
179
- steps: [{ step: "verify-rg", ok: true }]
180
- };
234
+ steps.push({ step: "verify-rg", ok: true, message: rg.installed ? rg.message : void 0 });
235
+ const fzf = await installFn(FZF_SPEC, { install: true, log });
236
+ if (fzf.present) {
237
+ steps.push({ step: "verify-fzf", ok: true, message: fzf.installed ? fzf.message : void 0 });
238
+ } else {
239
+ log("warn", `fzf unavailable \u2014 fuzzy ranking disabled (plain ripgrep results). ${fzf.message}`);
240
+ steps.push({ step: "verify-fzf", ok: false, message: fzf.message });
241
+ }
242
+ return { ready: true, steps };
181
243
  },
182
244
  async onEvent() {
183
245
  return { accepted: true };
184
246
  },
185
247
  async health() {
186
248
  const rgPresent = await rgOnPath(spawnFn);
249
+ const fzfPresent = rgPresent ? await fzfOnPath(fzfFn) : false;
250
+ const diagnostics = [];
251
+ if (rgPresent) {
252
+ diagnostics.push({ level: "ok", message: "ripgrep ready" });
253
+ diagnostics.push(
254
+ fzfPresent ? { level: "ok", message: "fzf ready \u2014 fuzzy ranking enabled" } : {
255
+ level: "warn",
256
+ message: `fzf not found \u2014 using plain ripgrep (no fuzzy ranking); install fzf: ${FZF_INSTALL}`
257
+ }
258
+ );
259
+ } else {
260
+ diagnostics.push({ level: "error", message: `ripgrep (rg) not found \u2014 install it: ${RG_INSTALL}` });
261
+ }
187
262
  return {
188
263
  ready: rgPresent,
189
264
  readiness: {
@@ -194,12 +269,7 @@ function makePluginDefinition(deps = {}) {
194
269
  embeddingsPending: 0,
195
270
  firstSearchCost: "instant"
196
271
  },
197
- diagnostics: rgPresent ? [{ level: "ok", message: "ripgrep ready" }] : [
198
- {
199
- level: "error",
200
- message: "ripgrep (rg) not found \u2014 install it: https://github.com/BurntSushi/ripgrep"
201
- }
202
- ]
272
+ diagnostics
203
273
  };
204
274
  }
205
275
  };
@@ -208,7 +278,7 @@ function makePluginDefinition(deps = {}) {
208
278
  kind: "search",
209
279
  version: VERSION,
210
280
  capabilities: {
211
- searchTypes: ["text", "regex"],
281
+ searchTypes: ["fuzzy"],
212
282
  collectionFilter: true,
213
283
  events: false
214
284
  },
@@ -224,6 +294,5 @@ createPlugin().run();
224
294
  export {
225
295
  createPlugin,
226
296
  makePluginDefinition,
227
- matchCollection,
228
- parseRgOutput
297
+ matchCollection
229
298
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffwiki-ripgrep",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "diffwiki search plugin driving ripgrep (rg) — instant literal/regex search, no index",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "diffwiki-core": "0.14.0"
23
+ "diffwiki-core": "0.15.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^22.0.0"