@optique/core 1.4.0-dev.2758 → 1.4.0-dev.2892

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.
@@ -0,0 +1,73 @@
1
+
2
+ //#region src/internal/joined-file-suggestion.ts
3
+ const joinedFiles = /* @__PURE__ */ new WeakMap();
4
+ /**
5
+ * Keeps a joined file fallback's origin available to the Bash encoder.
6
+ * Other shells retain the existing literal representation.
7
+ * @param prefix The option name and delimiter.
8
+ * @param file The original file suggestion.
9
+ * @param value The value prefix being completed.
10
+ * @returns The literal fallback with private file metadata.
11
+ * @internal
12
+ */
13
+ function joinedFileSuggestion(prefix, file, value) {
14
+ const literal = {
15
+ kind: "literal",
16
+ text: prefix + (file.pattern ?? ""),
17
+ description: file.description
18
+ };
19
+ joinedFiles.set(literal, {
20
+ files: [{
21
+ prefix,
22
+ file: file.pattern === value && !/[?*]/.test(value) && !(file.prefix != null && file.prefix !== "" && value.startsWith(file.prefix)) ? {
23
+ ...file,
24
+ pattern: void 0
25
+ } : file
26
+ }],
27
+ literal: false
28
+ });
29
+ return literal;
30
+ }
31
+ /**
32
+ * Retrieves the originating file suggestion for a joined literal fallback.
33
+ * @param suggestion The suggestion to inspect.
34
+ * @returns Its private file metadata, if present.
35
+ * @internal
36
+ */
37
+ function getJoinedFileSuggestion(suggestion) {
38
+ return joinedFiles.get(suggestion);
39
+ }
40
+ /**
41
+ * Preserves file origins when equal literal fallbacks are deduplicated.
42
+ * @param target The retained suggestion.
43
+ * @param source The duplicate suggestion.
44
+ * @internal
45
+ */
46
+ function mergeJoinedFileSuggestions(target, source) {
47
+ const a = joinedFiles.get(target);
48
+ const b = joinedFiles.get(source);
49
+ if (a === void 0 && b === void 0) return;
50
+ const files = [...a?.files ?? [], ...b?.files ?? []];
51
+ const seen = /* @__PURE__ */ new Set();
52
+ joinedFiles.set(target, {
53
+ literal: (a?.literal ?? true) || (b?.literal ?? true),
54
+ files: files.filter(({ prefix, file }) => {
55
+ const key = JSON.stringify([
56
+ prefix,
57
+ file.type,
58
+ file.pattern,
59
+ file.prefix,
60
+ file.includeHidden === true,
61
+ file.extensions?.toSorted()
62
+ ]);
63
+ if (seen.has(key)) return false;
64
+ seen.add(key);
65
+ return true;
66
+ })
67
+ });
68
+ }
69
+
70
+ //#endregion
71
+ exports.getJoinedFileSuggestion = getJoinedFileSuggestion;
72
+ exports.joinedFileSuggestion = joinedFileSuggestion;
73
+ exports.mergeJoinedFileSuggestions = mergeJoinedFileSuggestions;
@@ -0,0 +1,70 @@
1
+ //#region src/internal/joined-file-suggestion.ts
2
+ const joinedFiles = /* @__PURE__ */ new WeakMap();
3
+ /**
4
+ * Keeps a joined file fallback's origin available to the Bash encoder.
5
+ * Other shells retain the existing literal representation.
6
+ * @param prefix The option name and delimiter.
7
+ * @param file The original file suggestion.
8
+ * @param value The value prefix being completed.
9
+ * @returns The literal fallback with private file metadata.
10
+ * @internal
11
+ */
12
+ function joinedFileSuggestion(prefix, file, value) {
13
+ const literal = {
14
+ kind: "literal",
15
+ text: prefix + (file.pattern ?? ""),
16
+ description: file.description
17
+ };
18
+ joinedFiles.set(literal, {
19
+ files: [{
20
+ prefix,
21
+ file: file.pattern === value && !/[?*]/.test(value) && !(file.prefix != null && file.prefix !== "" && value.startsWith(file.prefix)) ? {
22
+ ...file,
23
+ pattern: void 0
24
+ } : file
25
+ }],
26
+ literal: false
27
+ });
28
+ return literal;
29
+ }
30
+ /**
31
+ * Retrieves the originating file suggestion for a joined literal fallback.
32
+ * @param suggestion The suggestion to inspect.
33
+ * @returns Its private file metadata, if present.
34
+ * @internal
35
+ */
36
+ function getJoinedFileSuggestion(suggestion) {
37
+ return joinedFiles.get(suggestion);
38
+ }
39
+ /**
40
+ * Preserves file origins when equal literal fallbacks are deduplicated.
41
+ * @param target The retained suggestion.
42
+ * @param source The duplicate suggestion.
43
+ * @internal
44
+ */
45
+ function mergeJoinedFileSuggestions(target, source) {
46
+ const a = joinedFiles.get(target);
47
+ const b = joinedFiles.get(source);
48
+ if (a === void 0 && b === void 0) return;
49
+ const files = [...a?.files ?? [], ...b?.files ?? []];
50
+ const seen = /* @__PURE__ */ new Set();
51
+ joinedFiles.set(target, {
52
+ literal: (a?.literal ?? true) || (b?.literal ?? true),
53
+ files: files.filter(({ prefix, file }) => {
54
+ const key = JSON.stringify([
55
+ prefix,
56
+ file.type,
57
+ file.pattern,
58
+ file.prefix,
59
+ file.includeHidden === true,
60
+ file.extensions?.toSorted()
61
+ ]);
62
+ if (seen.has(key)) return false;
63
+ seen.add(key);
64
+ return true;
65
+ })
66
+ });
67
+ }
68
+
69
+ //#endregion
70
+ export { getJoinedFileSuggestion, joinedFileSuggestion, mergeJoinedFileSuggestions };
@@ -758,6 +758,14 @@ type Suggestion = {
758
758
  * The current prefix/pattern for fallback when native completion is unavailable.
759
759
  */
760
760
  readonly pattern?: string;
761
+ /**
762
+ * Literal text at the start of `pattern` that is preserved on insertion
763
+ * but excluded from native filesystem matching, such as `output=`.
764
+ * Shell adapters ignore it if it does not lead both the pattern and
765
+ * the current word. Regenerate installed scripts to use this field.
766
+ * @since 1.1.13
767
+ */
768
+ readonly prefix?: string;
761
769
  /**
762
770
  * The type of file system entries to complete.
763
771
  */
@@ -758,6 +758,14 @@ type Suggestion = {
758
758
  * The current prefix/pattern for fallback when native completion is unavailable.
759
759
  */
760
760
  readonly pattern?: string;
761
+ /**
762
+ * Literal text at the start of `pattern` that is preserved on insertion
763
+ * but excluded from native filesystem matching, such as `output=`.
764
+ * Shell adapters ignore it if it does not lead both the pattern and
765
+ * the current word. Regenerate installed scripts to use this field.
766
+ * @since 1.1.13
767
+ */
768
+ readonly prefix?: string;
761
769
  /**
762
770
  * The type of file system entries to complete.
763
771
  */
@@ -4,6 +4,7 @@ const require_mode_dispatch = require('./internal/mode-dispatch.cjs');
4
4
  const require_dependency_runtime = require('./dependency-runtime.cjs');
5
5
  const require_internal_parser = require('./internal/parser.cjs');
6
6
  const require_annotation_state = require('./annotation-state.cjs');
7
+ const require_joined_file_suggestion = require('./internal/joined-file-suggestion.cjs');
7
8
  const require_empty_input = require('./internal/empty-input.cjs');
8
9
  const require_execution_context = require('./execution-context.cjs');
9
10
  const require_short_option = require('./short-option.cjs');
@@ -1580,6 +1581,7 @@ function multiple(parser, options = {}) {
1580
1581
  "file",
1581
1582
  suggestion.type,
1582
1583
  suggestion.pattern ?? "",
1584
+ suggestion.prefix ?? "",
1583
1585
  suggestion.includeHidden === true,
1584
1586
  suggestion.extensions == null ? "" : suggestion.extensions.toSorted().join("\0"),
1585
1587
  description
@@ -1587,14 +1589,16 @@ function multiple(parser, options = {}) {
1587
1589
  };
1588
1590
  return require_mode_dispatch.dispatchIterableByMode(parser.mode, function* () {
1589
1591
  const selectedValues = collectSelectedValuesSync();
1590
- const emitted = /* @__PURE__ */ new Set();
1592
+ const emitted = /* @__PURE__ */ new Map();
1591
1593
  const yieldUnique = function* (suggestions) {
1592
1594
  for (const s of suggestions) {
1593
1595
  const key = suggestionKey(s);
1594
- if (shouldInclude(selectedValues, s) && !emitted.has(key)) {
1595
- emitted.add(key);
1596
+ if (!shouldInclude(selectedValues, s)) continue;
1597
+ const existing = emitted.get(key);
1598
+ if (existing === void 0) {
1599
+ emitted.set(key, s);
1596
1600
  yield s;
1597
- }
1601
+ } else require_joined_file_suggestion.mergeJoinedFileSuggestions(existing, s);
1598
1602
  }
1599
1603
  };
1600
1604
  let shouldTryFallback = false;
@@ -1607,14 +1611,16 @@ function multiple(parser, options = {}) {
1607
1611
  if (shouldTryFallback) yield* yieldUnique(syncParser.suggest(require_execution_context.withChildContext(context, itemIndex, suggestFallbackState), prefix));
1608
1612
  }, async function* () {
1609
1613
  const selectedValues = await collectSelectedValuesAsync();
1610
- const emitted = /* @__PURE__ */ new Set();
1614
+ const emitted = /* @__PURE__ */ new Map();
1611
1615
  const yieldUnique = async function* (suggestions) {
1612
1616
  for await (const s of suggestions) {
1613
1617
  const key = suggestionKey(s);
1614
- if (shouldInclude(selectedValues, s) && !emitted.has(key)) {
1615
- emitted.add(key);
1618
+ if (!shouldInclude(selectedValues, s)) continue;
1619
+ const existing = emitted.get(key);
1620
+ if (existing === void 0) {
1621
+ emitted.set(key, s);
1616
1622
  yield s;
1617
- }
1623
+ } else require_joined_file_suggestion.mergeJoinedFileSuggestions(existing, s);
1618
1624
  }
1619
1625
  };
1620
1626
  let shouldTryFallback = false;
package/dist/modifiers.js CHANGED
@@ -4,6 +4,7 @@ import { dispatchByMode, dispatchIterableByMode, mapModeValue, wrapForMode } fro
4
4
  import { defineForwardedEffectfulSchedulingNodes } from "./dependency-runtime.js";
5
5
  import { defineInheritedAnnotationParser, defineParseLanes, defineSourceBindingOnlyAnnotationCompletionParser, getOwnParseLanes, unmatchedNonCliDependencySourceStateMarker } from "./internal/parser.js";
6
6
  import { getDelegatedAnnotationState, hasDelegatedAnnotationCarrier, isAnnotationWrappedInitialState, normalizeDelegatedAnnotationState, normalizeNestedDelegatedAnnotationState } from "./annotation-state.js";
7
+ import { mergeJoinedFileSuggestions } from "./internal/joined-file-suggestion.js";
7
8
  import { acceptsEmptyInput, defineEmptyInputFacts, getEmptyInputFacts, multipleFacts, optionalLikeFacts, resolveEmptyStepState } from "./internal/empty-input.js";
8
9
  import { mergeChildExec, withChildContext, withChildExecPath } from "./execution-context.js";
9
10
  import { combinedOptionScope, repeatedOptionScope, scopeParser } from "./short-option.js";
@@ -1580,6 +1581,7 @@ function multiple(parser, options = {}) {
1580
1581
  "file",
1581
1582
  suggestion.type,
1582
1583
  suggestion.pattern ?? "",
1584
+ suggestion.prefix ?? "",
1583
1585
  suggestion.includeHidden === true,
1584
1586
  suggestion.extensions == null ? "" : suggestion.extensions.toSorted().join("\0"),
1585
1587
  description
@@ -1587,14 +1589,16 @@ function multiple(parser, options = {}) {
1587
1589
  };
1588
1590
  return dispatchIterableByMode(parser.mode, function* () {
1589
1591
  const selectedValues = collectSelectedValuesSync();
1590
- const emitted = /* @__PURE__ */ new Set();
1592
+ const emitted = /* @__PURE__ */ new Map();
1591
1593
  const yieldUnique = function* (suggestions) {
1592
1594
  for (const s of suggestions) {
1593
1595
  const key = suggestionKey(s);
1594
- if (shouldInclude(selectedValues, s) && !emitted.has(key)) {
1595
- emitted.add(key);
1596
+ if (!shouldInclude(selectedValues, s)) continue;
1597
+ const existing = emitted.get(key);
1598
+ if (existing === void 0) {
1599
+ emitted.set(key, s);
1596
1600
  yield s;
1597
- }
1601
+ } else mergeJoinedFileSuggestions(existing, s);
1598
1602
  }
1599
1603
  };
1600
1604
  let shouldTryFallback = false;
@@ -1607,14 +1611,16 @@ function multiple(parser, options = {}) {
1607
1611
  if (shouldTryFallback) yield* yieldUnique(syncParser.suggest(withChildContext(context, itemIndex, suggestFallbackState), prefix));
1608
1612
  }, async function* () {
1609
1613
  const selectedValues = await collectSelectedValuesAsync();
1610
- const emitted = /* @__PURE__ */ new Set();
1614
+ const emitted = /* @__PURE__ */ new Map();
1611
1615
  const yieldUnique = async function* (suggestions) {
1612
1616
  for await (const s of suggestions) {
1613
1617
  const key = suggestionKey(s);
1614
- if (shouldInclude(selectedValues, s) && !emitted.has(key)) {
1615
- emitted.add(key);
1618
+ if (!shouldInclude(selectedValues, s)) continue;
1619
+ const existing = emitted.get(key);
1620
+ if (existing === void 0) {
1621
+ emitted.set(key, s);
1616
1622
  yield s;
1617
- }
1623
+ } else mergeJoinedFileSuggestions(existing, s);
1618
1624
  }
1619
1625
  };
1620
1626
  let shouldTryFallback = false;
@@ -7,6 +7,7 @@ const require_mode_dispatch = require('./internal/mode-dispatch.cjs');
7
7
  const require_internal_dependency = require('./internal/dependency.cjs');
8
8
  const require_dependency_runtime = require('./dependency-runtime.cjs');
9
9
  const require_annotation_state = require('./annotation-state.cjs');
10
+ const require_joined_file_suggestion = require('./internal/joined-file-suggestion.cjs');
10
11
  const require_empty_input = require('./internal/empty-input.cjs');
11
12
  const require_execution_context = require('./execution-context.cjs');
12
13
  const require_usage_internals = require('./usage-internals.cjs');
@@ -287,6 +288,10 @@ function* getSuggestionsWithDependency(valueParser, prefix, dependencyRegistry,
287
288
  }
288
289
  yield* valueParser.suggest(prefix);
289
290
  }
291
+ /** Returns the joined prefix accepted by option parsing. */
292
+ function joinedOptionPrefix(names, input) {
293
+ return names.filter((name) => name.startsWith("--") || name.startsWith("/") || name.startsWith("-") && name.length > 2).map((name) => name + (name.startsWith("/") ? ":" : "=")).find((prefix) => input.startsWith(prefix));
294
+ }
290
295
  /**
291
296
  * Internal sync helper for option suggest functionality.
292
297
  * @internal
@@ -307,28 +312,23 @@ function* suggestOptionSync(optionNames$1, valueParser, hidden, description, con
307
312
  return;
308
313
  }
309
314
  }
310
- const equalsIndex = prefix.indexOf("=");
311
- if (equalsIndex >= 0 && !waitingForValue) {
312
- const optionPart = prefix.slice(0, equalsIndex);
313
- const valuePart = prefix.slice(equalsIndex + 1);
315
+ const joinedPrefix = joinedOptionPrefix(optionNames$1, prefix);
316
+ if (joinedPrefix !== void 0 && !waitingForValue) {
317
+ const optionPart = joinedPrefix.slice(0, -1);
318
+ const valuePart = prefix.slice(joinedPrefix.length);
314
319
  if (optionNames$1.includes(optionPart) && !/^-[^-]$/u.test(optionPart)) {
315
320
  if (valueParser && valueParser.suggest) {
316
321
  const valueSuggestions = getSuggestionsWithDependency(valueParser, valuePart, context.dependencyRegistry, context.exec);
317
322
  for (const suggestion of valueSuggestions) if (suggestion.kind === "literal") yield {
318
323
  kind: "literal",
319
- text: `${optionPart}=${suggestion.text}`,
320
- description: suggestion.description
321
- };
322
- else yield {
323
- kind: "literal",
324
- text: `${optionPart}=${suggestion.pattern || ""}`,
324
+ text: `${joinedPrefix}${suggestion.text}`,
325
325
  description: suggestion.description
326
326
  };
327
+ else yield require_joined_file_suggestion.joinedFileSuggestion(joinedPrefix, suggestion, valuePart);
327
328
  }
328
329
  }
329
330
  } else {
330
- const expectingValue = context.buffer.length > 0 && optionNames$1.includes(context.buffer[context.buffer.length - 1]);
331
- if (!expectingValue && (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+"))) {
331
+ if (!waitingForValue && (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+"))) {
332
332
  for (const optionName$1 of optionNames$1) if (optionName$1.startsWith(prefix)) {
333
333
  if (prefix === "-" && !/^-[^-]$/u.test(optionName$1)) continue;
334
334
  yield {
@@ -406,28 +406,23 @@ async function* suggestOptionAsync(optionNames$1, valueParser, hidden, descripti
406
406
  return;
407
407
  }
408
408
  }
409
- const equalsIndex = prefix.indexOf("=");
410
- if (equalsIndex >= 0 && !waitingForValue) {
411
- const optionPart = prefix.slice(0, equalsIndex);
412
- const valuePart = prefix.slice(equalsIndex + 1);
409
+ const joinedPrefix = joinedOptionPrefix(optionNames$1, prefix);
410
+ if (joinedPrefix !== void 0 && !waitingForValue) {
411
+ const optionPart = joinedPrefix.slice(0, -1);
412
+ const valuePart = prefix.slice(joinedPrefix.length);
413
413
  if (optionNames$1.includes(optionPart) && !/^-[^-]$/u.test(optionPart)) {
414
414
  if (valueParser && valueParser.suggest) {
415
415
  const valueSuggestions = getSuggestionsWithDependencyAsync(valueParser, valuePart, context.dependencyRegistry, context.exec);
416
416
  for await (const suggestion of valueSuggestions) if (suggestion.kind === "literal") yield {
417
417
  kind: "literal",
418
- text: `${optionPart}=${suggestion.text}`,
419
- description: suggestion.description
420
- };
421
- else yield {
422
- kind: "literal",
423
- text: `${optionPart}=${suggestion.pattern || ""}`,
418
+ text: `${joinedPrefix}${suggestion.text}`,
424
419
  description: suggestion.description
425
420
  };
421
+ else yield require_joined_file_suggestion.joinedFileSuggestion(joinedPrefix, suggestion, valuePart);
426
422
  }
427
423
  }
428
424
  } else {
429
- const expectingValue = context.buffer.length > 0 && optionNames$1.includes(context.buffer[context.buffer.length - 1]);
430
- if (!expectingValue && (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+"))) {
425
+ if (!waitingForValue && (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+"))) {
431
426
  for (const optionName$1 of optionNames$1) if (optionName$1.startsWith(prefix)) {
432
427
  if (prefix === "-" && !/^-[^-]$/u.test(optionName$1)) continue;
433
428
  yield {
@@ -7,6 +7,7 @@ import { dispatchByMode, dispatchIterableByMode, wrapForMode } from "./internal/
7
7
  import { getDefaultValuesFunction, getDependencyIds, getSnapshottedDefaultDependencyValues, isDerivedValueParser, suggestWithDependency } from "./internal/dependency.js";
8
8
  import { effectfulSchedulingNodesKey, exclusiveSourceScopeKey, includeSourceFailureChain, recordDerivedRawInput, replayDerivedParser, replayDerivedParserAsync, sourceCollectionExpansionKey } from "./dependency-runtime.js";
9
9
  import { getWrappedChildParseState, getWrappedChildState, isAnnotationWrappedInitialState, normalizeInjectedAnnotationState } from "./annotation-state.js";
10
+ import { joinedFileSuggestion } from "./internal/joined-file-suggestion.js";
10
11
  import { UNCHANGED, defineEmptyInputFacts, failingStepFacts } from "./internal/empty-input.js";
11
12
  import { mergeChildExec, withChildContext, withChildExecPath } from "./execution-context.js";
12
13
  import { extractLeadingCommandNames } from "./usage-internals.js";
@@ -287,6 +288,10 @@ function* getSuggestionsWithDependency(valueParser, prefix, dependencyRegistry,
287
288
  }
288
289
  yield* valueParser.suggest(prefix);
289
290
  }
291
+ /** Returns the joined prefix accepted by option parsing. */
292
+ function joinedOptionPrefix(names, input) {
293
+ return names.filter((name) => name.startsWith("--") || name.startsWith("/") || name.startsWith("-") && name.length > 2).map((name) => name + (name.startsWith("/") ? ":" : "=")).find((prefix) => input.startsWith(prefix));
294
+ }
290
295
  /**
291
296
  * Internal sync helper for option suggest functionality.
292
297
  * @internal
@@ -307,28 +312,23 @@ function* suggestOptionSync(optionNames$1, valueParser, hidden, description, con
307
312
  return;
308
313
  }
309
314
  }
310
- const equalsIndex = prefix.indexOf("=");
311
- if (equalsIndex >= 0 && !waitingForValue) {
312
- const optionPart = prefix.slice(0, equalsIndex);
313
- const valuePart = prefix.slice(equalsIndex + 1);
315
+ const joinedPrefix = joinedOptionPrefix(optionNames$1, prefix);
316
+ if (joinedPrefix !== void 0 && !waitingForValue) {
317
+ const optionPart = joinedPrefix.slice(0, -1);
318
+ const valuePart = prefix.slice(joinedPrefix.length);
314
319
  if (optionNames$1.includes(optionPart) && !/^-[^-]$/u.test(optionPart)) {
315
320
  if (valueParser && valueParser.suggest) {
316
321
  const valueSuggestions = getSuggestionsWithDependency(valueParser, valuePart, context.dependencyRegistry, context.exec);
317
322
  for (const suggestion of valueSuggestions) if (suggestion.kind === "literal") yield {
318
323
  kind: "literal",
319
- text: `${optionPart}=${suggestion.text}`,
320
- description: suggestion.description
321
- };
322
- else yield {
323
- kind: "literal",
324
- text: `${optionPart}=${suggestion.pattern || ""}`,
324
+ text: `${joinedPrefix}${suggestion.text}`,
325
325
  description: suggestion.description
326
326
  };
327
+ else yield joinedFileSuggestion(joinedPrefix, suggestion, valuePart);
327
328
  }
328
329
  }
329
330
  } else {
330
- const expectingValue = context.buffer.length > 0 && optionNames$1.includes(context.buffer[context.buffer.length - 1]);
331
- if (!expectingValue && (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+"))) {
331
+ if (!waitingForValue && (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+"))) {
332
332
  for (const optionName$1 of optionNames$1) if (optionName$1.startsWith(prefix)) {
333
333
  if (prefix === "-" && !/^-[^-]$/u.test(optionName$1)) continue;
334
334
  yield {
@@ -406,28 +406,23 @@ async function* suggestOptionAsync(optionNames$1, valueParser, hidden, descripti
406
406
  return;
407
407
  }
408
408
  }
409
- const equalsIndex = prefix.indexOf("=");
410
- if (equalsIndex >= 0 && !waitingForValue) {
411
- const optionPart = prefix.slice(0, equalsIndex);
412
- const valuePart = prefix.slice(equalsIndex + 1);
409
+ const joinedPrefix = joinedOptionPrefix(optionNames$1, prefix);
410
+ if (joinedPrefix !== void 0 && !waitingForValue) {
411
+ const optionPart = joinedPrefix.slice(0, -1);
412
+ const valuePart = prefix.slice(joinedPrefix.length);
413
413
  if (optionNames$1.includes(optionPart) && !/^-[^-]$/u.test(optionPart)) {
414
414
  if (valueParser && valueParser.suggest) {
415
415
  const valueSuggestions = getSuggestionsWithDependencyAsync(valueParser, valuePart, context.dependencyRegistry, context.exec);
416
416
  for await (const suggestion of valueSuggestions) if (suggestion.kind === "literal") yield {
417
417
  kind: "literal",
418
- text: `${optionPart}=${suggestion.text}`,
419
- description: suggestion.description
420
- };
421
- else yield {
422
- kind: "literal",
423
- text: `${optionPart}=${suggestion.pattern || ""}`,
418
+ text: `${joinedPrefix}${suggestion.text}`,
424
419
  description: suggestion.description
425
420
  };
421
+ else yield joinedFileSuggestion(joinedPrefix, suggestion, valuePart);
426
422
  }
427
423
  }
428
424
  } else {
429
- const expectingValue = context.buffer.length > 0 && optionNames$1.includes(context.buffer[context.buffer.length - 1]);
430
- if (!expectingValue && (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+"))) {
425
+ if (!waitingForValue && (prefix.startsWith("--") || prefix.startsWith("-") || prefix.startsWith("/") || prefix.startsWith("+"))) {
431
426
  for (const optionName$1 of optionNames$1) if (optionName$1.startsWith(prefix)) {
432
427
  if (prefix === "-" && !/^-[^-]$/u.test(optionName$1)) continue;
433
428
  yield {
@@ -286,9 +286,14 @@ function attachedValuePrefix(context, prefix, optionNames) {
286
286
  }
287
287
  /** Preserves the prefix when embedding value suggestions in one token. @internal */
288
288
  function prefixSuggestion(head, suggestion) {
289
+ if (suggestion.kind === "file") return {
290
+ ...suggestion,
291
+ pattern: head + (suggestion.pattern ?? ""),
292
+ prefix: head + (suggestion.prefix ?? "")
293
+ };
289
294
  return {
290
295
  kind: "literal",
291
- text: head + (suggestion.kind === "literal" ? suggestion.text : suggestion.pattern ?? ""),
296
+ text: head + suggestion.text,
292
297
  ...suggestion.description != null ? { description: suggestion.description } : {}
293
298
  };
294
299
  }
@@ -286,9 +286,14 @@ function attachedValuePrefix(context, prefix, optionNames) {
286
286
  }
287
287
  /** Preserves the prefix when embedding value suggestions in one token. @internal */
288
288
  function prefixSuggestion(head, suggestion) {
289
+ if (suggestion.kind === "file") return {
290
+ ...suggestion,
291
+ pattern: head + (suggestion.pattern ?? ""),
292
+ prefix: head + (suggestion.prefix ?? "")
293
+ };
289
294
  return {
290
295
  kind: "literal",
291
- text: head + (suggestion.kind === "literal" ? suggestion.text : suggestion.pattern ?? ""),
296
+ text: head + suggestion.text,
292
297
  ...suggestion.description != null ? { description: suggestion.description } : {}
293
298
  };
294
299
  }
@@ -1,5 +1,6 @@
1
1
  const require_usage = require('./usage.cjs');
2
2
  const require_message = require('./message.cjs');
3
+ const require_joined_file_suggestion = require('./internal/joined-file-suggestion.cjs');
3
4
 
4
5
  //#region src/suggestion.ts
5
6
  /**
@@ -322,22 +323,27 @@ function appendValueHint(base, input, candidates, options) {
322
323
  *
323
324
  * For literal suggestions, the text itself is used as the key.
324
325
  * For file suggestions, a composite key is created from the type,
325
- * extensions, and pattern.
326
+ * extensions, pattern, and insertion prefix.
326
327
  *
327
328
  * @param suggestion The suggestion to create a key for
328
329
  * @returns A string key that uniquely identifies this suggestion
329
330
  * @internal
330
331
  */
331
332
  function getSuggestionKey(suggestion) {
332
- if (suggestion.kind === "literal") return suggestion.text;
333
- return `__FILE__:${suggestion.type}:${suggestion.extensions?.toSorted().join(",") ?? ""}:${suggestion.pattern ?? ""}`;
333
+ if (suggestion.kind === "literal") return `literal:${suggestion.text}`;
334
+ return `file:${JSON.stringify([
335
+ suggestion.type,
336
+ suggestion.extensions?.toSorted().join(",") ?? "",
337
+ suggestion.pattern ?? "",
338
+ suggestion.prefix ?? ""
339
+ ])}`;
334
340
  }
335
341
  /**
336
342
  * Removes duplicate suggestions from an array while preserving order.
337
343
  *
338
344
  * Suggestions are considered duplicates if they have the same key:
339
345
  * - Literal suggestions: same text
340
- * - File suggestions: same type, extensions, and pattern
346
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
341
347
  *
342
348
  * The first occurrence of each unique suggestion is kept. For file
343
349
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -370,7 +376,8 @@ function deduplicateSuggestions(suggestions) {
370
376
  if (existing == null) {
371
377
  entries.set(key, suggestion);
372
378
  order.push(key);
373
- } else if (suggestion.kind === "file" && existing.kind === "file" && suggestion.includeHidden === true && existing.includeHidden !== true) entries.set(key, {
379
+ } else if (suggestion.kind === "literal" && existing.kind === "literal") require_joined_file_suggestion.mergeJoinedFileSuggestions(existing, suggestion);
380
+ else if (suggestion.kind === "file" && existing.kind === "file" && suggestion.includeHidden === true && existing.includeHidden !== true) entries.set(key, {
374
381
  ...existing,
375
382
  includeHidden: true
376
383
  });
@@ -202,7 +202,7 @@ declare function appendValueHint(base: Message, input: string, candidates: reado
202
202
  *
203
203
  * Suggestions are considered duplicates if they have the same key:
204
204
  * - Literal suggestions: same text
205
- * - File suggestions: same type, extensions, and pattern
205
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
206
206
  *
207
207
  * The first occurrence of each unique suggestion is kept. For file
208
208
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -202,7 +202,7 @@ declare function appendValueHint(base: Message, input: string, candidates: reado
202
202
  *
203
203
  * Suggestions are considered duplicates if they have the same key:
204
204
  * - Literal suggestions: same text
205
- * - File suggestions: same type, extensions, and pattern
205
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
206
206
  *
207
207
  * The first occurrence of each unique suggestion is kept. For file
208
208
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -1,5 +1,6 @@
1
1
  import { extractCommandNames, extractOptionNames, isSuggestionHidden } from "./usage.js";
2
2
  import { lineBreak, message, optionName, text, value } from "./message.js";
3
+ import { mergeJoinedFileSuggestions } from "./internal/joined-file-suggestion.js";
3
4
 
4
5
  //#region src/suggestion.ts
5
6
  /**
@@ -322,22 +323,27 @@ function appendValueHint(base, input, candidates, options) {
322
323
  *
323
324
  * For literal suggestions, the text itself is used as the key.
324
325
  * For file suggestions, a composite key is created from the type,
325
- * extensions, and pattern.
326
+ * extensions, pattern, and insertion prefix.
326
327
  *
327
328
  * @param suggestion The suggestion to create a key for
328
329
  * @returns A string key that uniquely identifies this suggestion
329
330
  * @internal
330
331
  */
331
332
  function getSuggestionKey(suggestion) {
332
- if (suggestion.kind === "literal") return suggestion.text;
333
- return `__FILE__:${suggestion.type}:${suggestion.extensions?.toSorted().join(",") ?? ""}:${suggestion.pattern ?? ""}`;
333
+ if (suggestion.kind === "literal") return `literal:${suggestion.text}`;
334
+ return `file:${JSON.stringify([
335
+ suggestion.type,
336
+ suggestion.extensions?.toSorted().join(",") ?? "",
337
+ suggestion.pattern ?? "",
338
+ suggestion.prefix ?? ""
339
+ ])}`;
334
340
  }
335
341
  /**
336
342
  * Removes duplicate suggestions from an array while preserving order.
337
343
  *
338
344
  * Suggestions are considered duplicates if they have the same key:
339
345
  * - Literal suggestions: same text
340
- * - File suggestions: same type, extensions, and pattern
346
+ * - File suggestions: same type, extensions, pattern, and insertion prefix
341
347
  *
342
348
  * The first occurrence of each unique suggestion is kept. For file
343
349
  * suggestions, `includeHidden` is merged across duplicates: if any
@@ -370,7 +376,8 @@ function deduplicateSuggestions(suggestions) {
370
376
  if (existing == null) {
371
377
  entries.set(key, suggestion);
372
378
  order.push(key);
373
- } else if (suggestion.kind === "file" && existing.kind === "file" && suggestion.includeHidden === true && existing.includeHidden !== true) entries.set(key, {
379
+ } else if (suggestion.kind === "literal" && existing.kind === "literal") mergeJoinedFileSuggestions(existing, suggestion);
380
+ else if (suggestion.kind === "file" && existing.kind === "file" && suggestion.includeHidden === true && existing.includeHidden !== true) entries.set(key, {
374
381
  ...existing,
375
382
  includeHidden: true
376
383
  });
@@ -1089,7 +1089,8 @@ function prefixKeyValueSuggestion(suggestion, prefix, suffix = "") {
1089
1089
  const text$1 = `${prefix}${textValue}${suffix}`;
1090
1090
  if (suggestion.kind === "file" && suffix === "") return {
1091
1091
  ...suggestion,
1092
- pattern: text$1
1092
+ pattern: text$1,
1093
+ prefix: `${prefix}${suggestion.prefix ?? ""}`
1093
1094
  };
1094
1095
  return suggestion.description == null ? {
1095
1096
  kind: "literal",
@@ -1089,7 +1089,8 @@ function prefixKeyValueSuggestion(suggestion, prefix, suffix = "") {
1089
1089
  const text$1 = `${prefix}${textValue}${suffix}`;
1090
1090
  if (suggestion.kind === "file" && suffix === "") return {
1091
1091
  ...suggestion,
1092
- pattern: text$1
1092
+ pattern: text$1,
1093
+ prefix: `${prefix}${suggestion.prefix ?? ""}`
1093
1094
  };
1094
1095
  return suggestion.description == null ? {
1095
1096
  kind: "literal",