Skip to content

Commit b60acd1

Browse files
committed
feat: rewrite submatch feature
1 parent 36862c3 commit b60acd1

File tree

5 files changed

+181
-39
lines changed

5 files changed

+181
-39
lines changed

denops/fall/config.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -210,23 +210,20 @@ export function getGlobalConfig(): Readonly<GlobalConfig> {
210210
* Get action picker params.
211211
*/
212212
export function getActionPickerParams(): Readonly<
213-
ActionPickerParams & GlobalConfig
213+
ActionPickerParams
214214
> {
215-
return {
216-
...getGlobalConfig(),
217-
...actionPickerParams,
218-
};
215+
return actionPickerParams;
219216
}
220217

221218
/**
222219
* Get item picker params.
223220
*/
224221
export function getItemPickerParams(
225222
name: string,
226-
): Readonly<ItemPickerParams & GlobalConfig> | undefined {
223+
): Readonly<ItemPickerParams> | undefined {
227224
const params = itemPickerParamsMap.get(name);
228225
if (params) {
229-
return { ...getGlobalConfig(), ...params };
226+
return params;
230227
}
231228
return undefined;
232229
}

denops/fall/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import "./lib/polyfill.ts";
22

33
import type { Entrypoint } from "jsr:@denops/std@^7.3.2";
4+
45
import { main as mainConfig } from "./main/config.ts";
56
import { main as mainEvent } from "./main/event.ts";
67
import { main as mainPicker } from "./main/picker.ts";
8+
import { main as mainSubmatch } from "./main/submatch.ts";
79

810
export const main: Entrypoint = async (denops) => {
911
await mainConfig(denops);
1012
await mainEvent(denops);
1113
await mainPicker(denops);
14+
await mainSubmatch(denops);
1215
};

denops/fall/main/picker.ts

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,35 @@ import { ensurePromise } from "jsr:@core/asyncutil@^1.2.0/ensure-promise";
33
import { assert, ensure, is } from "jsr:@core/unknownutil@^4.3.0";
44
import type { DetailUnit } from "jsr:@vim-fall/core@^0.2.1/item";
55

6-
import type { GlobalConfig, ItemPickerParams } from "../config.ts";
6+
import type { ItemPickerParams } from "../config.ts";
77
import {
88
getActionPickerParams,
99
getGlobalConfig,
1010
getItemPickerParams,
1111
listItemPickerNames,
1212
loadUserConfig,
1313
} from "../config.ts";
14-
import { isOptions, isParams, isStringArray } from "../util/predicate.ts";
14+
import {
15+
isGlobalConfig,
16+
isItemPickerParams,
17+
isOptions,
18+
isStringArray,
19+
} from "../util/predicate.ts";
1520
import { action as buildActionSource } from "../extension/source/action.ts";
1621
import { Picker } from "../picker.ts";
22+
import type { SubmatchContext } from "./submatch.ts";
1723

1824
let zindex = 50;
1925

2026
export const main: Entrypoint = (denops) => {
2127
denops.dispatcher = {
2228
...denops.dispatcher,
29+
"picker": (args, itemPickerParams, options) => {
30+
assert(args, isStringArray);
31+
assert(itemPickerParams, isItemPickerParams);
32+
assert(options, isOptions);
33+
return startPicker(denops, args, itemPickerParams, options);
34+
},
2335
"picker:command": async (args) => {
2436
await loadUserConfig(denops);
2537
// Split the command arguments
@@ -43,33 +55,50 @@ export const main: Entrypoint = (denops) => {
4355
assert(cursorpos, is.Number);
4456
return listItemPickerNames().filter((name) => name.startsWith(arglead));
4557
},
46-
// _screen is not used
47-
"picker:start": async (args, _screen, params, options) => {
48-
await loadUserConfig(denops);
49-
assert(args, isStringArray);
50-
assert(params, isParams);
58+
// TODO: Remove this API prior to release v1.0.0
59+
// DEPRECATED: Use "submatch:start" instead
60+
"picker:start": async (_args, _screen, params, options) => {
61+
console.warn(
62+
"The 'picker:start' is deprecated. Use 'submatch:start' instead.",
63+
"It is probably caused by '@vim-fall/std/builtin/action/submatch' action.",
64+
);
65+
assert(
66+
params,
67+
is.IntersectionOf([
68+
isGlobalConfig,
69+
isItemPickerParams,
70+
]),
71+
);
5172
assert(options, isOptions);
52-
return await startPicker(denops, args, params, options);
73+
return await startPicker(denops, [], params, options);
5374
},
5475
};
5576
};
5677

5778
async function startPicker(
5879
denops: Denops,
5980
args: string[],
60-
params: ItemPickerParams<DetailUnit, string> & GlobalConfig,
81+
itemPickerParams: ItemPickerParams<DetailUnit, string>,
6182
{ signal }: { signal?: AbortSignal } = {},
6283
): Promise<void | true> {
6384
await using stack = new AsyncDisposableStack();
64-
const itemPicker = stack.use(new Picker({ ...params, zindex }));
85+
const globalConfig = getGlobalConfig();
86+
const itemPicker = stack.use(
87+
new Picker({
88+
...globalConfig,
89+
...itemPickerParams,
90+
zindex,
91+
}),
92+
);
6593
zindex += Picker.ZINDEX_ALLOCATION;
6694
stack.defer(() => {
6795
zindex -= Picker.ZINDEX_ALLOCATION;
6896
});
6997
const actionPicker = stack.use(
7098
new Picker({
7199
name: "@action",
72-
source: buildActionSource(params.actions),
100+
source: buildActionSource(itemPickerParams.actions),
101+
...globalConfig,
73102
...getActionPickerParams(),
74103
zindex,
75104
}),
@@ -120,27 +149,31 @@ async function startPicker(
120149
actionName = resultItem.action;
121150
} else {
122151
// Default action
123-
actionName = params.defaultAction;
152+
actionName = itemPickerParams.defaultAction;
124153
}
125154

126155
// Execute the action
127-
const action = params.actions[actionName];
156+
const action = itemPickerParams.actions[actionName];
128157
if (!action) {
129158
throw new Error(`Action "${actionName}" is not found`);
130159
}
131160
const actionParams = {
132-
// for 'submatch' action
161+
// TODO: Drop the following attributes prior to release v1.0.0
162+
// Attributes used before @vim-fall/[email protected]
133163
_submatchContext: {
134-
globalConfig: getGlobalConfig(),
135-
pickerParams: params,
136-
// not used
137-
screen: {
138-
width: 0,
139-
height: 0,
164+
globalConfig,
165+
pickerParams: {
166+
...globalConfig,
167+
...itemPickerParams,
140168
},
169+
screen: { width: 0, height: 0 },
170+
},
171+
// Secret attribute for @vim-fall/std/builtin/action/submatch
172+
_submatch: {
173+
itemPickerParams,
141174
},
142175
...resultItem,
143-
};
176+
} as const satisfies SubmatchContext & { _submatchContext: unknown };
144177
if (await ensurePromise(action.invoke(denops, actionParams, { signal }))) {
145178
// Picker should not be closed
146179
continue;

denops/fall/main/submatch.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { Denops, Entrypoint } from "jsr:@denops/std@^7.3.2";
2+
import { as, assert, is, type Predicate } from "jsr:@core/unknownutil@^4.3.0";
3+
import type {
4+
Coordinator,
5+
Detail,
6+
InvokeParams,
7+
Matcher,
8+
Previewer,
9+
Renderer,
10+
Sorter,
11+
Theme,
12+
} from "jsr:@vim-fall/core@^0.2.1";
13+
14+
import type { ItemPickerParams } from "../config.ts";
15+
import {
16+
isAction,
17+
isCoordinator,
18+
isItemPickerParams,
19+
isMatcher,
20+
isOptions,
21+
isPreviewer,
22+
isRenderer,
23+
isSorter,
24+
isTheme,
25+
} from "../util/predicate.ts";
26+
import { list as buildListSource } from "../extension/source/list.ts";
27+
28+
export type SubmatchContext = InvokeParams<Detail> & {
29+
readonly _submatch: {
30+
readonly itemPickerParams: ItemPickerParams<Detail, string>;
31+
};
32+
};
33+
34+
type SubmatchParams = {
35+
readonly matchers: readonly [Matcher<Detail>, ...Matcher<Detail>[]];
36+
readonly actions?: ItemPickerParams<Detail, string>["actions"];
37+
readonly defaultAction?: string;
38+
readonly sorters?: readonly Sorter<Detail>[] | null;
39+
readonly renderers?: readonly Renderer<Detail>[] | null;
40+
readonly previewers?: readonly Previewer<Detail>[] | null;
41+
readonly coordinator?: Coordinator | null;
42+
readonly theme?: Theme | null;
43+
};
44+
45+
export const main: Entrypoint = (denops) => {
46+
denops.dispatcher = {
47+
...denops.dispatcher,
48+
"submatch": async (context, params, options) => {
49+
assert(context, isSubmatchContext);
50+
assert(params, isSubmatchParams);
51+
assert(options, isOptions);
52+
return await submatchStart(denops, context, params, options);
53+
},
54+
};
55+
};
56+
57+
async function submatchStart(
58+
denops: Denops,
59+
context: SubmatchContext,
60+
params: SubmatchParams,
61+
options: { signal?: AbortSignal } = {},
62+
): Promise<void | true> {
63+
const itemPickerParams = {
64+
...context._submatch.itemPickerParams,
65+
source: buildListSource(context.selectedItems ?? context.filteredItems),
66+
};
67+
if (params.actions) {
68+
itemPickerParams.actions = params.actions;
69+
}
70+
if (params.defaultAction) {
71+
itemPickerParams.defaultAction = params.defaultAction;
72+
}
73+
if (params.sorters) {
74+
itemPickerParams.sorters = params.sorters;
75+
}
76+
if (params.renderers) {
77+
itemPickerParams.renderers = params.renderers;
78+
}
79+
if (params.previewers) {
80+
itemPickerParams.previewers = params.previewers;
81+
}
82+
if (params.coordinator) {
83+
itemPickerParams.coordinator = params.coordinator;
84+
}
85+
if (params.theme) {
86+
itemPickerParams.theme = params.theme;
87+
}
88+
const result = await denops.dispatch(
89+
denops.name,
90+
"picker",
91+
[],
92+
itemPickerParams,
93+
options,
94+
);
95+
if (result === true) {
96+
return true;
97+
}
98+
}
99+
100+
const isSubmatchContext = is.ObjectOf({
101+
item: as.Optional(is.Any),
102+
selectedItems: as.Optional(is.ArrayOf(is.Any)),
103+
filteredItems: is.ArrayOf(is.Any),
104+
_submatch: is.ObjectOf({
105+
itemPickerParams: isItemPickerParams,
106+
}),
107+
}) satisfies Predicate<SubmatchContext>;
108+
109+
const isSubmatchParams = is.ObjectOf({
110+
matchers: is.ArrayOf(isMatcher) as Predicate<
111+
[Matcher<Detail>, ...Matcher<Detail>[]]
112+
>,
113+
actions: as.Optional(is.RecordOf(isAction, is.String)),
114+
defaultAction: as.Optional(is.String),
115+
sorters: as.Optional(is.ArrayOf(isSorter)),
116+
renderers: as.Optional(is.ArrayOf(isRenderer)),
117+
previewers: as.Optional(is.ArrayOf(isPreviewer)),
118+
coordinator: as.Optional(isCoordinator),
119+
theme: as.Optional(isTheme),
120+
}) satisfies Predicate<SubmatchParams>;

denops/fall/util/predicate.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import type {
77
Matcher,
88
Previewer,
99
Renderer,
10-
Size,
1110
Sorter,
1211
Source,
1312
Theme,
@@ -21,11 +20,6 @@ export const isAbortSignal = is.InstanceOf(AbortSignal) as Predicate<
2120
AbortSignal
2221
>;
2322

24-
export const isScreen = is.ObjectOf({
25-
width: is.Number,
26-
height: is.Number,
27-
}) satisfies Predicate<Size>;
28-
2923
export const isTheme = is.ObjectOf({
3024
border: is.UniformTupleOf(8, is.String),
3125
divider: is.UniformTupleOf(6, is.String),
@@ -93,11 +87,6 @@ export const isItemPickerParams = is.ObjectOf({
9387
theme: as.Optional(isTheme),
9488
}) satisfies Predicate<ItemPickerParams<Detail, string>>;
9589

96-
export const isParams = is.IntersectionOf([
97-
isGlobalConfig,
98-
isItemPickerParams,
99-
]);
100-
10190
export const isOptions = is.ObjectOf({
10291
signal: as.Optional(isAbortSignal),
10392
});

0 commit comments

Comments
 (0)