Skip to content

Commit af97833

Browse files
committed
👍 Add input() function in helper module
The `input()` function in `helper` module has more features than `input()` function in `function` module.
1 parent 180eb78 commit af97833

File tree

5 files changed

+381
-0
lines changed

5 files changed

+381
-0
lines changed

denops_std/helper/README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,92 @@ export async function main(denops: Denops): Promise<void> {
2525
}
2626
```
2727

28+
### input
29+
30+
Use `input()` which is a wrapper function of `input()` in
31+
[`function` module](../function/README.md) to show a prompt to get user input
32+
like:
33+
34+
```typescript
35+
import { Denops } from "https://deno.land/x/denops_std/mod.ts";
36+
import { input } from "https://deno.land/x/denops_std/helper/mod.ts";
37+
38+
export async function main(denops: Denops): Promise<void> {
39+
console.log(
40+
await input(denops, {
41+
prompt: "> ",
42+
text: "This is default value",
43+
}),
44+
);
45+
}
46+
```
47+
48+
Not like native `input()` function, it returns `null` instead of an empty string
49+
when user press `<Esc>` or `<C-c>` so that developers can distinguish if user
50+
cancelled or input an empty string.
51+
52+
It accepts a TypeScript callback as a completion function addition to built-in
53+
completions and Vim script custom completion like:
54+
55+
```typescript
56+
import { Denops } from "https://deno.land/x/denops_std/mod.ts";
57+
import { input } from "https://deno.land/x/denops_std/helper/mod.ts";
58+
59+
export async function main(denops: Denops): Promise<void> {
60+
// Built-in completions
61+
console.log(
62+
await input(denops, {
63+
prompt: "> ",
64+
text: "This is default value",
65+
completion: "command",
66+
}),
67+
);
68+
69+
// Vim script custom completion (assume 'MyVimScriptCompletion' is defined in Vim script)
70+
console.log(
71+
await input(denops, {
72+
prompt: "> ",
73+
text: "This is default value",
74+
completion: "custom,MyVimScriptCompletion",
75+
}),
76+
);
77+
78+
// TypeScript custom completion
79+
console.log(
80+
await input(denops, {
81+
prompt: "> ",
82+
text: "This is default value",
83+
completion: (
84+
arglead: string,
85+
cmdline: string,
86+
cursorpos: number,
87+
): Promise<string[]> => {
88+
return ["Hello", "World"];
89+
},
90+
}),
91+
);
92+
}
93+
```
94+
95+
If you'd like to guard input by `inputsave()` and `inputrestore()`, use
96+
`inputsave` option like:
97+
98+
```typescript
99+
import { Denops } from "https://deno.land/x/denops_std/mod.ts";
100+
import { input } from "https://deno.land/x/denops_std/helper/mod.ts";
101+
102+
export async function main(denops: Denops): Promise<void> {
103+
denops.dispatch = {
104+
say: async () => {
105+
return await input(denops, {
106+
prompt: "> ",
107+
inputsave: true,
108+
});
109+
},
110+
};
111+
}
112+
```
113+
28114
### batch
29115

30116
Deprecated in favor of [`batch`](./batch/README.md) module. Use `gather()`

denops_std/helper/input.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Denops, ensureNumber, ensureString } from "../deps.ts";
2+
import * as fn from "../function/mod.ts";
3+
import * as anonymous from "../anonymous/mod.ts";
4+
import * as helper from "../helper/mod.ts";
5+
6+
export type CustomCompletion = (
7+
arglead: string,
8+
cmdline: string,
9+
cursorpos: number,
10+
) => string[] | Promise<string[]>;
11+
12+
export type InputOptions = {
13+
prompt?: string;
14+
text?: string;
15+
completion?: fn.BuiltinCompletion | string | CustomCompletion;
16+
// Guard `input` by `inputsave` and `inputrestore`
17+
inputsave?: boolean;
18+
};
19+
20+
/**
21+
* Open a prompt to get user input.
22+
*
23+
* It is a wrapper function of Vim/Neovim's native `input()` function.
24+
* This version has the following advantages
25+
*
26+
* - Developers can use TypeScript custom completion function
27+
* - It returns `null` instead when user cancelled by <Esc> or <C-c>
28+
* - It automatically guard input when `inputsave` option is specified
29+
*/
30+
export async function input(
31+
denops: Denops,
32+
options: InputOptions = {},
33+
): Promise<string | null> {
34+
await helper.load(denops, new URL("./input.vim", import.meta.url));
35+
const completion = options.completion ?? null;
36+
if (completion && typeof completion !== "string") {
37+
const [id] = anonymous.once(denops, async (arglead, cmdline, cursorpos) => {
38+
ensureString(arglead);
39+
ensureString(cmdline);
40+
ensureNumber(cursorpos);
41+
return await completion(arglead, cmdline, cursorpos);
42+
});
43+
return await denops.call(
44+
"DenopsStdHelperInput",
45+
options.prompt ?? "",
46+
options.text ?? "",
47+
{ plugin: denops.name, id },
48+
options.inputsave ?? false,
49+
) as Promise<string | null>;
50+
}
51+
if (completion && !fn.isValidBuiltinCompletion(completion)) {
52+
if (
53+
!completion.startsWith("custom,") && !completion.startsWith("customlist,")
54+
) {
55+
throw new Error(`Invalid completion '${completion}' specified.`);
56+
}
57+
}
58+
return denops.call(
59+
"DenopsStdHelperInput",
60+
options.prompt ?? "",
61+
options.text ?? "",
62+
completion,
63+
options.inputsave ?? false,
64+
) as Promise<string | null>;
65+
}

denops_std/helper/input.vim

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
if exists('g:denops_std_helper_input_loaded')
2+
finish
3+
endif
4+
let g:denops_std_helper_input_loaded = 1
5+
6+
let s:escape_token = '###DenopsStdHelperInputCancelled###'
7+
8+
function! DenopsStdHelperInput(prompt, text, completion, inputsave) abort
9+
if type(a:completion) is# v:t_dict
10+
let s:completion = copy(a:completion)
11+
let completion = printf('customlist,%s', s:completion_proxy_name)
12+
else
13+
let completion = a:completion
14+
endif
15+
if a:inputsave
16+
call inputsave()
17+
endif
18+
try
19+
return s:input(a:prompt, a:text, completion)
20+
finally
21+
if a:inputsave
22+
call inputrestore()
23+
endif
24+
endtry
25+
endfunction
26+
27+
function! s:completion_proxy(arglead, cmdline, cursorpos) abort
28+
return denops#request(
29+
\ s:completion.plugin,
30+
\ s:completion.id,
31+
\ [a:arglead, a:cmdline, a:cursorpos],
32+
\)
33+
endfunction
34+
35+
if has('nvim')
36+
function! s:input(prompt, text, completion) abort
37+
let options = {
38+
\ 'prompt': a:prompt,
39+
\ 'default': a:text,
40+
\ 'cancelreturn': s:escape_token,
41+
\}
42+
if a:completion isnot# v:null
43+
let options['completion'] = a:completion
44+
endif
45+
let result = input(options)
46+
if result ==# s:escape_token
47+
return v:null
48+
endif
49+
return result
50+
endfunction
51+
else
52+
function! s:input(prompt, text, completion) abort
53+
keepjumps keepalt botright 0new
54+
execute printf('cnoremap <nowait><buffer> <Esc> <C-u>%s<CR>', s:escape_token)
55+
try
56+
let result = a:completion is# v:null
57+
\ ? input(a:prompt, a:text)
58+
\ : input(a:prompt, a:text, a:completion)
59+
if result ==# s:escape_token
60+
return v:null
61+
endif
62+
return result
63+
catch /^Vim:Interrupt$/
64+
return v:null
65+
finally
66+
bwipeout!
67+
endtry
68+
endfunction
69+
endif
70+
71+
let s:completion_proxy_name = get(funcref('s:completion_proxy'), 'name')

denops_std/helper/input_test.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { assertEquals, assertThrowsAsync, test } from "../deps_test.ts";
2+
import { input } from "./input.ts";
3+
import { execute } from "./execute.ts";
4+
import * as autocmd from "../autocmd/mod.ts";
5+
6+
test({
7+
mode: "all",
8+
name: "input() returns user input text",
9+
fn: async (denops) => {
10+
await autocmd.group(denops, "denops_std_helper_input", (helper) => {
11+
helper.remove("*");
12+
helper.define(
13+
"CmdlineEnter",
14+
"*",
15+
`call feedkeys("Hello world!\\<CR>", "t")`,
16+
);
17+
});
18+
const result = await input(denops);
19+
assertEquals(result, "Hello world!");
20+
},
21+
});
22+
test({
23+
mode: "all",
24+
name: "input() assign specified text as a default input",
25+
fn: async (denops) => {
26+
await autocmd.group(denops, "denops_std_helper_input", (helper) => {
27+
helper.remove("*");
28+
helper.define("CmdlineEnter", "*", `call feedkeys("\\<CR>", "t")`);
29+
});
30+
const result = await input(denops, {
31+
text: "Hello world!",
32+
});
33+
assertEquals(result, "Hello world!");
34+
},
35+
});
36+
test({
37+
mode: "all",
38+
name: "input() uses specified built-in completion",
39+
fn: async (denops) => {
40+
await autocmd.group(denops, "denops_std_helper_input", (helper) => {
41+
helper.remove("*");
42+
helper.define("CmdlineEnter", "*", `call feedkeys("\\<Tab>\\<CR>", "t")`);
43+
});
44+
const result = await input(denops, {
45+
completion: "option",
46+
});
47+
assertEquals(result, "all");
48+
},
49+
});
50+
test({
51+
mode: "all",
52+
name: "input() uses specified custom completion",
53+
fn: async (denops) => {
54+
await autocmd.group(denops, "denops_std_helper_input", (helper) => {
55+
helper.remove("*");
56+
helper.define("CmdlineEnter", "*", `call feedkeys("\\<Tab>\\<CR>", "t")`);
57+
});
58+
const result = await input(denops, {
59+
completion: (_arglead, _cmdline, _cursorpos) => {
60+
return ["Hello world!"];
61+
},
62+
});
63+
assertEquals(result, "Hello world!");
64+
},
65+
});
66+
test({
67+
mode: "all",
68+
name: "input() uses specified custom completion (Vim script)",
69+
fn: async (denops) => {
70+
await execute(
71+
denops,
72+
`
73+
function! DenopsStdFunctionInputTest(arglead, cmdline, cursorpos) abort
74+
return ["Hello world!"]
75+
endfunction
76+
`,
77+
);
78+
await autocmd.group(denops, "denops_std_function_input", (helper) => {
79+
helper.remove("*");
80+
helper.define("CmdlineEnter", "*", `call feedkeys("\\<Tab>\\<CR>", "t")`);
81+
});
82+
const result = await input(denops, {
83+
completion: "customlist,DenopsStdFunctionInputTest",
84+
});
85+
assertEquals(result, "Hello world!");
86+
},
87+
});
88+
test({
89+
mode: "all",
90+
name: "input() uses specified custom completion (TypeScript)",
91+
fn: async (denops) => {
92+
await autocmd.group(denops, "denops_std_helper_input", (helper) => {
93+
helper.remove("*");
94+
helper.define("CmdlineEnter", "*", `call feedkeys("\\<Tab>\\<CR>", "t")`);
95+
});
96+
const result = await input(denops, {
97+
completion: (_arglead, _cmdline, _cursorpos) => {
98+
return ["Hello world!"];
99+
},
100+
});
101+
assertEquals(result, "Hello world!");
102+
},
103+
});
104+
test({
105+
mode: "all",
106+
name: "input() throws an error when invalid completion is specified",
107+
fn: async (denops) => {
108+
await assertThrowsAsync(
109+
async () => {
110+
await input(denops, { completion: "custom:Invalid" });
111+
},
112+
undefined,
113+
"Invalid completion",
114+
);
115+
},
116+
});
117+
test({
118+
mode: "all",
119+
name: "input() returns `null` when <Esc> is pressed",
120+
fn: async (denops) => {
121+
// XXX
122+
if (denops.meta.host === "vim") {
123+
console.warn("Skip while the test does not work properly on Vim");
124+
return;
125+
}
126+
await autocmd.group(denops, "denops_std_helper_input", (helper) => {
127+
helper.remove("*");
128+
helper.define(
129+
"CmdlineEnter",
130+
"*",
131+
`call feedkeys("Hello world!\\<Esc>", "t")`,
132+
);
133+
});
134+
const result = await input(denops);
135+
assertEquals(result, null);
136+
},
137+
});
138+
test({
139+
mode: "all",
140+
name: "input() returns `null` when <C-c> is pressed",
141+
fn: async (denops) => {
142+
// XXX
143+
if (denops.meta.host === "vim") {
144+
console.warn("Skip while the test does not work properly on Vim");
145+
return;
146+
}
147+
await autocmd.group(denops, "denops_std_helper_input", (helper) => {
148+
helper.remove("*");
149+
helper.define(
150+
"CmdlineEnter",
151+
"*",
152+
`call feedkeys("Hello world!\\<C-c>", "t")`,
153+
);
154+
});
155+
const result = await input(denops);
156+
assertEquals(result, null);
157+
},
158+
});

denops_std/helper/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from "./batch.ts";
22
export * from "./echo.ts";
33
export * from "./execute.ts";
44
export * from "./load.ts";
5+
export * from "./input.ts";

0 commit comments

Comments
 (0)