Skip to content

Commit cdb92c5

Browse files
committed
feat: add -input= option to set initial prompt value
Add support for the -input= option to pre-fill the picker's prompt with an initial query. This is useful for starting searches with a predefined term. Usage: :Fall -input="search term" file The -input= option must appear before the source name. Options placed after the source name are treated as source arguments. Changes: - Add parseArgs() and extractOption() utilities in util/args.ts - Update picker:command to extract and process -input= option - Use <q-args> instead of <f-args> to support quoted arguments - Add comprehensive tests (20 test cases) - Update documentation with usage examples and constraints
1 parent abf9c78 commit cdb92c5

File tree

6 files changed

+387
-9
lines changed

6 files changed

+387
-9
lines changed

deno.jsonc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
".coverage/**",
55
".worktrees/**"
66
],
7+
"lint": {
8+
"rules": {
9+
"exclude": ["no-import-prefix"]
10+
}
11+
},
712
"tasks": {
813
"check": "deno check ./**/*.ts",
914
"test": "deno test -A --parallel --shuffle --doc",

denops/fall/main/picker.ts

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Denops, Entrypoint } from "jsr:@denops/std@^7.3.2";
22
import { ensurePromise } from "jsr:@core/asyncutil@^1.2.0/ensure-promise";
3-
import { assert, ensure, is } from "jsr:@core/unknownutil@^4.3.0";
3+
import { assert, is } from "jsr:@core/unknownutil@^4.3.0";
44
import type { Detail } from "jsr:@vim-fall/core@^0.3.0/item";
55

66
import type { PickerParams } from "../custom.ts";
@@ -12,6 +12,7 @@ import {
1212
loadUserCustom,
1313
} from "../custom.ts";
1414
import { isOptions, isPickerParams, isStringArray } from "../util/predicate.ts";
15+
import { extractOption, parseArgs } from "../util/args.ts";
1516
import { action as buildActionSource } from "../extension/source/action.ts";
1617
import { Picker, type PickerContext } from "../picker.ts";
1718
import type { SubmatchContext } from "./submatch.ts";
@@ -34,6 +35,32 @@ const SESSION_EXCLUDE_SOURCES = [
3435
"@session",
3536
];
3637

38+
/**
39+
* Create initial picker context with the specified query.
40+
*
41+
* All fields except query are initialized to their default values:
42+
* - Empty selection, collections, and filtered items
43+
* - Cursor and offset at 0
44+
* - All component indices at 0 (except previewerIndex which is undefined)
45+
*
46+
* @param query - Initial query string for the picker prompt
47+
* @returns PickerContext with default values
48+
*/
49+
function createInitialContext(query: string): PickerContext<Detail> {
50+
return {
51+
query,
52+
selection: new Set(),
53+
collectedItems: [],
54+
filteredItems: [],
55+
cursor: 0,
56+
offset: 0,
57+
matcherIndex: 0,
58+
sorterIndex: 0,
59+
rendererIndex: 0,
60+
previewerIndex: undefined,
61+
};
62+
}
63+
3764
export const main: Entrypoint = (denops) => {
3865
denops.dispatcher = {
3966
...denops.dispatcher,
@@ -43,12 +70,36 @@ export const main: Entrypoint = (denops) => {
4370
assert(options, isOptions);
4471
return startPicker(denops, args, itemPickerParams, options);
4572
},
46-
"picker:command": withHandleError(denops, async (args) => {
73+
"picker:command": withHandleError(denops, async (cmdline) => {
4774
await loadUserCustom(denops);
48-
// Split the command arguments
49-
const [name, ...sourceArgs] = ensure(args, isStringArray);
5075

51-
// Load user custom
76+
// Parse command line arguments
77+
// cmdline is string from denops#request('fall', 'picker:command', [a:args])
78+
assert(cmdline, is.String);
79+
const allArgs = parseArgs(cmdline);
80+
81+
// Find the first non-option argument (source name)
82+
const sourceIndex = allArgs.findIndex((arg) => !arg.startsWith("-"));
83+
if (sourceIndex === -1) {
84+
throw new ExpectedError(
85+
`Picker name is required. Available item pickers are: ${
86+
listPickerNames().join(", ")
87+
}`,
88+
);
89+
}
90+
91+
// Extract -input= option only from arguments before the source name
92+
const beforeSourceArgs = allArgs.slice(0, sourceIndex);
93+
const afterSourceArgs = allArgs.slice(sourceIndex);
94+
const [inputValues] = extractOption(beforeSourceArgs, "-input=");
95+
const initialQuery = inputValues.at(-1);
96+
// Note: Currently only -input= is supported. Other options before
97+
// the source name are silently ignored for future extensibility.
98+
99+
// Get source name and its arguments
100+
const [name, ...sourceArgs] = afterSourceArgs;
101+
102+
// Load picker params
52103
const itemPickerParams = getPickerParams(name);
53104
if (!itemPickerParams) {
54105
throw new ExpectedError(
@@ -57,11 +108,17 @@ export const main: Entrypoint = (denops) => {
57108
}`,
58109
);
59110
}
111+
112+
// Create context with initial query if specified
113+
const context = initialQuery !== undefined
114+
? createInitialContext(initialQuery)
115+
: undefined;
116+
60117
await startPicker(
61118
denops,
62119
sourceArgs,
63120
itemPickerParams,
64-
{ signal: denops.interrupted },
121+
{ signal: denops.interrupted, context },
65122
);
66123
}),
67124
"picker:command:complete": withHandleError(

denops/fall/util/args.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Utility functions for parsing and processing command-line arguments.
3+
* @module
4+
*/
5+
6+
/**
7+
* Parse command line arguments respecting quotes and escapes.
8+
*
9+
* This function properly handles:
10+
* - Double quotes (`"`)
11+
* - Single quotes (`'`)
12+
* - Escape sequences (`\`)
13+
* - Nested quotes of different types
14+
*
15+
* Note on edge cases:
16+
* - Unclosed quotes: treated as part of the argument value
17+
* - Trailing backslash: ignored (escape with no following character)
18+
* - Empty string or only spaces: returns empty array
19+
*
20+
* @param cmdline - The command line string to parse
21+
* @returns Array of parsed arguments
22+
*
23+
* @example
24+
* ```ts
25+
* parseArgs('file -input="Hello world"')
26+
* // => ['file', '-input=Hello world']
27+
*
28+
* parseArgs("file -input='test'")
29+
* // => ['file', '-input=test']
30+
*
31+
* parseArgs('file -input="He said \\"hello\\""')
32+
* // => ['file', '-input=He said "hello"']
33+
*
34+
* parseArgs('file -input="unclosed')
35+
* // => ['file', '-input=unclosed'] (unclosed quote)
36+
* ```
37+
*/
38+
export function parseArgs(cmdline: string): string[] {
39+
const args: string[] = [];
40+
let current = "";
41+
let inQuote: string | null = null;
42+
let escaped = false;
43+
44+
for (const char of cmdline) {
45+
if (escaped) {
46+
current += char;
47+
escaped = false;
48+
} else if (char === "\\") {
49+
escaped = true;
50+
} else if (char === '"' || char === "'") {
51+
if (inQuote === char) {
52+
inQuote = null;
53+
} else if (inQuote === null) {
54+
inQuote = char;
55+
} else {
56+
current += char;
57+
}
58+
} else if (char === " " && inQuote === null) {
59+
if (current) {
60+
args.push(current);
61+
current = "";
62+
}
63+
} else {
64+
current += char;
65+
}
66+
}
67+
68+
if (current) {
69+
args.push(current);
70+
}
71+
72+
return args;
73+
}
74+
75+
/**
76+
* Extract option arguments from argument list.
77+
*
78+
* All arguments with the matching prefix are extracted and returned as an array.
79+
* The caller can decide whether to use the first, last, or all values.
80+
*
81+
* @param args - Array of arguments to search
82+
* @param prefix - Option prefix to extract (e.g., '-input=')
83+
* @returns Tuple of [array of extracted values, remaining arguments]
84+
*
85+
* @example
86+
* ```ts
87+
* extractOption(['-input=Hello', 'file', '/path'], '-input=')
88+
* // => [['Hello'], ['file', '/path']]
89+
*
90+
* extractOption(['file', '/path'], '-input=')
91+
* // => [[], ['file', '/path']]
92+
*
93+
* extractOption(['-input=first', 'file', '-input=second'], '-input=')
94+
* // => [['first', 'second'], ['file']]
95+
* ```
96+
*/
97+
export function extractOption(
98+
args: readonly string[],
99+
prefix: string,
100+
): [string[], string[]] {
101+
const values: string[] = [];
102+
const remaining: string[] = [];
103+
104+
for (const arg of args) {
105+
if (arg.startsWith(prefix)) {
106+
values.push(arg.slice(prefix.length));
107+
} else {
108+
remaining.push(arg);
109+
}
110+
}
111+
112+
return [values, remaining];
113+
}

0 commit comments

Comments
 (0)