Skip to content

Commit c067c77

Browse files
committed
Add FallResume command to resume previous pickers
1 parent 159a5d1 commit c067c77

5 files changed

Lines changed: 170 additions & 12 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
function! fall#command#FallResume#call(filter) abort
2+
if denops#plugin#wait('fall') isnot# 0
3+
return
4+
endif
5+
let l:laststatus_saved = &laststatus
6+
try
7+
call s:hide()
8+
call fall#internal#mapping#store()
9+
call denops#request('fall', 'picker:resume:command', [a:filter])
10+
finally
11+
call s:show()
12+
call fall#internal#tolerant#call({ -> fall#internal#mapping#restore() })
13+
call fall#internal#tolerant#call({ -> fall#internal#popup#closeall() })
14+
endtry
15+
endfunction
16+
17+
function! fall#command#FallResume#complete(arglead, cmdline, cursorpos) abort
18+
if denops#plugin#wait('fall') isnot# 0
19+
return []
20+
endif
21+
return denops#request('fall', 'picker:resume:command:complete', [a:arglead, a:cmdline, a:cursorpos])
22+
endfunction
23+
24+
function! s:hide() abort
25+
call fall#internal#tolerant#call({ -> fall#internal#msgarea#hide() })
26+
call fall#internal#tolerant#call({ -> fall#internal#cursor#hide() })
27+
endfunction
28+
29+
function! s:show() abort
30+
call fall#internal#tolerant#call({ -> fall#internal#msgarea#show() })
31+
call fall#internal#tolerant#call({ -> fall#internal#cursor#show() })
32+
endfunction

denops/fall/main/picker.ts

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

66
import type { PickerParams } from "../custom.ts";
77
import {
@@ -13,9 +13,14 @@ import {
1313
} from "../custom.ts";
1414
import { isOptions, isPickerParams, isStringArray } from "../util/predicate.ts";
1515
import { action as buildActionSource } from "../extension/source/action.ts";
16-
import { Picker } from "../picker.ts";
16+
import { Picker, type PickerContext } from "../picker.ts";
1717
import type { SubmatchContext } from "./submatch.ts";
1818
import { ExpectedError, withHandleError } from "../error.ts";
19+
import {
20+
listPickerSessions,
21+
loadPickerSession,
22+
savePickerSession,
23+
} from "../session.ts";
1924

2025
let zindex = 50;
2126

@@ -32,6 +37,7 @@ export const main: Entrypoint = (denops) => {
3237
await loadUserCustom(denops);
3338
// Split the command arguments
3439
const [name, ...sourceArgs] = ensure(args, isStringArray);
40+
3541
// Load user custom
3642
const itemPickerParams = getPickerParams(name);
3743
if (!itemPickerParams) {
@@ -58,14 +64,92 @@ export const main: Entrypoint = (denops) => {
5864
return listPickerNames().filter((name) => name.startsWith(arglead));
5965
},
6066
),
67+
"picker:resume:command": withHandleError(
68+
denops,
69+
async (filter) => {
70+
assert(filter, is.UnionOf([is.String, is.Nullish]));
71+
await loadUserCustom(denops);
72+
return await resumePicker(denops, filter ?? "", {
73+
signal: denops.interrupted,
74+
});
75+
},
76+
),
77+
"picker:resume:command:complete": withHandleError(
78+
denops,
79+
async (arglead, cmdline, cursorpos) => {
80+
await loadUserCustom(denops);
81+
assert(arglead, is.String);
82+
assert(cmdline, is.String);
83+
assert(cursorpos, is.Number);
84+
const sessions = listPickerSessions();
85+
if (cmdline.includes("#")) {
86+
// Resume by filter
87+
const [name] = arglead.split("#", 2);
88+
const filteredSessions = name
89+
? sessions.filter((s) => s.name === name)
90+
: sessions;
91+
const candidates = Array.from(
92+
{ length: filteredSessions.length },
93+
(_, i) => {
94+
return `${name}#${i + 1}`;
95+
},
96+
);
97+
return candidates.filter((c) => c.startsWith(arglead));
98+
} else {
99+
// Resume by name
100+
const candidates = sessions.map((s) => s.name);
101+
return candidates.filter((c) => c.startsWith(arglead));
102+
}
103+
},
104+
),
61105
};
62106
};
63107

64-
async function startPicker(
108+
async function resumePicker(
109+
denops: Denops,
110+
filter: string,
111+
{ signal }: {
112+
signal?: AbortSignal;
113+
} = {},
114+
): Promise<void | true> {
115+
// Parse filter ({name}#{indexFromLatest})
116+
const [filterName, filterNumberStr = "1"] = filter.split("#", 2);
117+
const filterNumber = Number(filterNumberStr);
118+
const session = await loadPickerSession({
119+
name: filterName,
120+
number: filterNumber,
121+
});
122+
if (!session) {
123+
throw new ExpectedError(
124+
`Picker session ${filterName}#${filterNumberStr} is not available.`,
125+
);
126+
}
127+
// Load user custom
128+
const pickerParams = getPickerParams(session.name);
129+
if (!pickerParams) {
130+
throw new ExpectedError(
131+
`No item picker "${session.name}" is found. Available item pickers are: ${
132+
listPickerNames().join(", ")
133+
}`,
134+
);
135+
}
136+
const { args, context } = session;
137+
await startPicker(
138+
denops,
139+
args,
140+
pickerParams,
141+
{ signal, context },
142+
);
143+
}
144+
145+
async function startPicker<T extends Detail>(
65146
denops: Denops,
66-
args: string[],
67-
pickerParams: PickerParams<DetailUnit, string>,
68-
{ signal }: { signal?: AbortSignal } = {},
147+
args: readonly string[],
148+
pickerParams: PickerParams<T, string>,
149+
{ signal, context }: {
150+
signal?: AbortSignal;
151+
context?: PickerContext<T>;
152+
} = {},
69153
): Promise<void | true> {
70154
await using stack = new AsyncDisposableStack();
71155
const setting = getSetting();
@@ -74,6 +158,7 @@ async function startPicker(
74158
...setting,
75159
...pickerParams,
76160
zindex,
161+
context,
77162
}),
78163
);
79164
zindex += Picker.ZINDEX_ALLOCATION;
@@ -93,6 +178,13 @@ async function startPicker(
93178
stack.defer(() => {
94179
zindex -= Picker.ZINDEX_ALLOCATION;
95180
});
181+
stack.defer(async () => {
182+
await savePickerSession({
183+
name: pickerParams.name,
184+
args,
185+
context: itemPicker.context,
186+
});
187+
});
96188

97189
stack.use(await itemPicker.open(denops, { signal }));
98190
while (true) {
@@ -155,7 +247,7 @@ async function startPicker(
155247
pickerParams,
156248
},
157249
...resultItem,
158-
} as const satisfies SubmatchContext;
250+
} as const satisfies SubmatchContext<T>;
159251
if (await ensurePromise(action.invoke(denops, actionParams, { signal }))) {
160252
// Picker should not be closed
161253
continue;

denops/fall/main/submatch.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ import {
2626
import { list as buildListSource } from "../extension/source/list.ts";
2727
import { withHandleError } from "../error.ts";
2828

29-
export type SubmatchContext = InvokeParams<Detail> & {
29+
export type SubmatchContext<T extends Detail> = InvokeParams<T> & {
3030
readonly _submatch: {
31-
readonly pickerParams: PickerParams<Detail, string>;
31+
readonly pickerParams: PickerParams<T, string>;
3232
};
3333
};
3434

@@ -55,9 +55,9 @@ export const main: Entrypoint = (denops) => {
5555
};
5656
};
5757

58-
async function submatchStart(
58+
async function submatchStart<T extends Detail>(
5959
denops: Denops,
60-
context: SubmatchContext,
60+
context: SubmatchContext<T>,
6161
params: SubmatchParams,
6262
options: { signal?: AbortSignal } = {},
6363
): Promise<void | true> {
@@ -106,7 +106,8 @@ const isSubmatchContext = is.ObjectOf({
106106
_submatch: is.ObjectOf({
107107
pickerParams: isPickerParams,
108108
}),
109-
}) satisfies Predicate<SubmatchContext>;
109+
// deno-lint-ignore no-explicit-any
110+
}) satisfies Predicate<SubmatchContext<any>>;
110111

111112
const isSubmatchParams = is.ObjectOf({
112113
matchers: is.ArrayOf(isMatcher) as Predicate<

doc/fall.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,37 @@ COMMAND *fall-command*
401401
:Fall {source} [{cmdarg}]
402402
Open picker to filter {source} (defined in "custom.ts" via
403403
|:FallCustom|). {cmdarg} is passed to the source.
404+
>
405+
" Open picker to filter files in the current directory
406+
:Fall file
407+
408+
" Open picker to filter files in the specified directory
409+
:Fall file /path/to/directory
410+
<
411+
412+
*:FallResume*
413+
:FallResume [{filter}]
414+
Resume the previous picker. If {filter} is not specified, it resumes
415+
the latest picker. The {filter} can be a source name and/or a number
416+
indicating the order of the picker. If a source name is specified, it
417+
resumes the latest picker of that source. If a number is specified,
418+
it resumes the nth latest picker. If both a source name and a number
419+
are specified, it resumes the nth latest picker of that source. The
420+
number is preceded by a "#" character. If the number is not specified,
421+
it defaults to 1 (the latest picker).
422+
>
423+
" Resume the latest picker
424+
:FallResume
425+
426+
" Resume the latest picker of the "file" source
427+
:FallResume file
428+
429+
" Resume the 2nd latest picker
430+
:FallResume #2
431+
432+
" Resume the 2nd latest picker of the "file" source
433+
:FallResume file#2
434+
<
404435

405436
*:FallCustom*
406437
:FallCustom

plugin/fall.vim

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ let s:sep = has('win32') ? '\' : '/'
66

77
command! -nargs=+ -complete=customlist,fall#command#Fall#complete
88
\ Fall call fall#command#Fall#call([<f-args>])
9+
command! -nargs=? -complete=customlist,fall#command#FallResume#complete
10+
\ FallResume call fall#command#FallResume#call(<q-args>)
911

1012
command! -nargs=0 FallCustom call fall#command#FallCustom#call()
1113
command! -nargs=0 FallCustomReload call fall#command#FallCustomReload#call()

0 commit comments

Comments
 (0)