@ff-labs/pi-fff 0.6.5-nightly.ca7bf03 → 0.7.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/package.json +1 -1
  2. package/src/index.ts +97 -90
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ff-labs/pi-fff",
3
3
  "public": true,
4
- "version": "0.6.5-nightly.ca7bf03",
4
+ "version": "0.7.0",
5
5
  "description": "pi extension: FFF-powered fuzzy file and content search",
6
6
  "type": "module",
7
7
  "license": "MIT",
package/src/index.ts CHANGED
@@ -279,76 +279,11 @@ function createFffMentionProvider(
279
279
  };
280
280
  }
281
281
 
282
- // Simple editor wrapper that injects FFF @-mention autocomplete alongside base provider
283
- class FffEditor extends CustomEditor {
284
- private baseProvider: AutocompleteProvider | undefined;
285
- private getMentionItems: (
286
- query: string,
287
- signal: AbortSignal,
288
- ) => Promise<AutocompleteItem[]>;
289
-
290
- constructor(
291
- tui: any,
292
- theme: any,
293
- keybindings: any,
294
- getMentionItems: (
295
- query: string,
296
- signal: AbortSignal,
297
- ) => Promise<AutocompleteItem[]>,
298
- ) {
299
- super(tui, theme, keybindings);
300
- this.getMentionItems = getMentionItems;
301
- }
302
-
303
- override setAutocompleteProvider(provider: AutocompleteProvider): void {
304
- this.baseProvider = provider;
305
- // Create composite provider that handles @-mentions and falls back to base
306
- const mentionProvider = createFffMentionProvider(this.getMentionItems);
307
- const compositeProvider: AutocompleteProvider = {
308
- getSuggestions: async (lines, cursorLine, cursorCol, options) => {
309
- // Try @-mention first
310
- const mentionResult = await mentionProvider.getSuggestions(
311
- lines,
312
- cursorLine,
313
- cursorCol,
314
- options,
315
- );
316
- if (mentionResult) return mentionResult;
317
- // Fall back to base provider
318
- return (
319
- this.baseProvider?.getSuggestions(
320
- lines,
321
- cursorLine,
322
- cursorCol,
323
- options,
324
- ) ?? null
325
- );
326
- },
327
- applyCompletion: (lines, cursorLine, cursorCol, item, prefix) => {
328
- // Let mention provider handle @ completions, base provider for others
329
- if (prefix?.startsWith("@")) {
330
- return mentionProvider.applyCompletion!(
331
- lines,
332
- cursorLine,
333
- cursorCol,
334
- item,
335
- prefix,
336
- );
337
- }
338
- return (
339
- this.baseProvider?.applyCompletion?.(
340
- lines,
341
- cursorLine,
342
- cursorCol,
343
- item,
344
- prefix,
345
- ) ?? { lines, cursorLine, cursorCol }
346
- );
347
- },
348
- };
349
- super.setAutocompleteProvider(compositeProvider);
350
- }
351
- }
282
+ // FffEditor is defined inside fffExtension() so it can capture `getMentionItems`
283
+ // via closure rather than via a 4th constructor parameter. This makes the class
284
+ // safe to subclass via `new SubClass(tui, theme, keybindings)` -- the pattern
285
+ // pi-vim and pi-image-attachments use to compose editors. See:
286
+ // https://github.com/badlogic/pi-mono/issues/3935
352
287
 
353
288
  // ---------------------------------------------------------------------------
354
289
  // Extension
@@ -357,6 +292,11 @@ class FffEditor extends CustomEditor {
357
292
  export default function fffExtension(pi: ExtensionAPI) {
358
293
  let finder: FileFinder | null = null;
359
294
  let finderCwd: string | null = null;
295
+ // Concurrent ensureFinder() callers share the same in-flight promise so
296
+ // FileFinder.create() (which takes native DB locks) runs at most once per
297
+ // base path at a time — otherwise parallel tool calls would race and
298
+ // deadlock at the native layer (issue #403).
299
+ let finderPromise: Promise<FileFinder> | null = null;
360
300
  let activeCwd = process.cwd();
361
301
 
362
302
  // Mode resolution: flag > env > default
@@ -389,28 +329,37 @@ export default function fffExtension(pi: ExtensionAPI) {
389
329
  return currentMode !== "tools-only";
390
330
  }
391
331
 
392
- async function ensureFinder(cwd: string): Promise<FileFinder> {
393
- if (finder && !finder.isDestroyed && finderCwd === cwd) return finder;
394
- if (finder && !finder.isDestroyed) {
395
- finder.destroy();
396
- finder = null;
397
- finderCwd = null;
398
- }
332
+ function ensureFinder(cwd: string): Promise<FileFinder> {
333
+ if (finder && !finder.isDestroyed && finderCwd === cwd)
334
+ return Promise.resolve(finder);
335
+ if (finderPromise) return finderPromise;
399
336
 
400
- const result = FileFinder.create({
401
- basePath: cwd,
402
- frecencyDbPath,
403
- historyDbPath,
404
- aiMode: true,
405
- });
337
+ finderPromise = (async () => {
338
+ if (finder && !finder.isDestroyed) {
339
+ finder.destroy();
340
+ finder = null;
341
+ finderCwd = null;
342
+ }
343
+
344
+ const result = FileFinder.create({
345
+ basePath: cwd,
346
+ frecencyDbPath,
347
+ historyDbPath,
348
+ aiMode: true,
349
+ });
406
350
 
407
- if (!result.ok)
408
- throw new Error(`Failed to create FFF file finder: ${result.error}`);
351
+ if (!result.ok)
352
+ throw new Error(`Failed to create FFF file finder: ${result.error}`);
409
353
 
410
- finder = result.value;
411
- finderCwd = cwd;
412
- await finder.waitForScan(15000);
413
- return finder;
354
+ finder = result.value;
355
+ finderCwd = cwd;
356
+ await finder.waitForScan(15000);
357
+ return finder;
358
+ })().finally(() => {
359
+ finderPromise = null;
360
+ });
361
+
362
+ return finderPromise;
414
363
  }
415
364
 
416
365
  function destroyFinder() {
@@ -450,6 +399,64 @@ export default function fffExtension(pi: ExtensionAPI) {
450
399
  });
451
400
  }
452
401
 
402
+ // Editor wrapper that injects FFF @-mention autocomplete alongside base provider.
403
+ // Defined inside fffExtension() so the class methods capture `getMentionItems`
404
+ // via closure. Subclasses constructed as `new Sub(tui, theme, keybindings)` by
405
+ // composability wrappers (pi-vim, pi-image-attachments) still get a working
406
+ // mention provider because the closure binding is preserved across subclassing.
407
+ class FffEditor extends CustomEditor {
408
+ private baseProvider: AutocompleteProvider | undefined;
409
+
410
+ override setAutocompleteProvider(provider: AutocompleteProvider): void {
411
+ this.baseProvider = provider;
412
+ // Create composite provider that handles @-mentions and falls back to base
413
+ const mentionProvider = createFffMentionProvider(getMentionItems);
414
+ const compositeProvider: AutocompleteProvider = {
415
+ getSuggestions: async (lines, cursorLine, cursorCol, options) => {
416
+ // Try @-mention first
417
+ const mentionResult = await mentionProvider.getSuggestions(
418
+ lines,
419
+ cursorLine,
420
+ cursorCol,
421
+ options,
422
+ );
423
+ if (mentionResult) return mentionResult;
424
+ // Fall back to base provider
425
+ return (
426
+ this.baseProvider?.getSuggestions(
427
+ lines,
428
+ cursorLine,
429
+ cursorCol,
430
+ options,
431
+ ) ?? null
432
+ );
433
+ },
434
+ applyCompletion: (lines, cursorLine, cursorCol, item, prefix) => {
435
+ // Let mention provider handle @ completions, base provider for others
436
+ if (prefix?.startsWith("@")) {
437
+ return mentionProvider.applyCompletion!(
438
+ lines,
439
+ cursorLine,
440
+ cursorCol,
441
+ item,
442
+ prefix,
443
+ );
444
+ }
445
+ return (
446
+ this.baseProvider?.applyCompletion?.(
447
+ lines,
448
+ cursorLine,
449
+ cursorCol,
450
+ item,
451
+ prefix,
452
+ ) ?? { lines, cursorLine, cursorCol }
453
+ );
454
+ },
455
+ };
456
+ super.setAutocompleteProvider(compositeProvider);
457
+ }
458
+ }
459
+
453
460
  function applyEditorMode(ctx: {
454
461
  ui: {
455
462
  setEditorComponent: (
@@ -462,7 +469,7 @@ export default function fffExtension(pi: ExtensionAPI) {
462
469
  } else {
463
470
  ctx.ui.setEditorComponent(
464
471
  (tui: any, theme: any, keybindings: any) =>
465
- new FffEditor(tui, theme, keybindings, getMentionItems),
472
+ new FffEditor(tui, theme, keybindings),
466
473
  );
467
474
  }
468
475
  }