Skip to content

Commit 6e14e74

Browse files
committed
👍 Add helper/expr_string
1 parent 86dda39 commit 6e14e74

File tree

2 files changed

+706
-0
lines changed

2 files changed

+706
-0
lines changed

denops_std/helper/expr_string.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import type {
2+
Context,
3+
Denops,
4+
Dispatcher,
5+
Meta,
6+
} from "https://deno.land/x/denops_core@v5.0.0/mod.ts";
7+
import { execute } from "./execute.ts";
8+
import { generateUniqueString } from "../util.ts";
9+
10+
const EXPR_STRING_MARK = "__denops_expr_string";
11+
12+
/**
13+
* String that marked as Vim's string constant format.
14+
*/
15+
export type ExprString = string & {
16+
/**
17+
* @internal
18+
*/
19+
readonly [EXPR_STRING_MARK]: 1;
20+
};
21+
22+
type Jsonable = {
23+
toJSON(key: string | number | undefined): string;
24+
};
25+
26+
// deno-lint-ignore no-explicit-any
27+
type TemplateSubstitutions = any[];
28+
29+
const cacheKey = "denops_std/helper/exprStr@1";
30+
31+
async function ensurePrerequisites(denops: Denops): Promise<string> {
32+
if (typeof denops.context[cacheKey] === "string") {
33+
return denops.context[cacheKey];
34+
}
35+
const suffix = generateUniqueString();
36+
denops.context[cacheKey] = suffix;
37+
const script = `
38+
let g:loaded_denops_std_helper_exprStr_${suffix} = 1
39+
40+
function DenopsStdHelperExprStringCall_${suffix}(fn, args) abort
41+
return call(a:fn, eval(a:args))
42+
endfunction
43+
`;
44+
await execute(denops, script);
45+
return suffix;
46+
}
47+
48+
/**
49+
* Tagged template function that marks a string as Vim's string constant format.
50+
* Returns a `String` wrapper object instead of a primitive string.
51+
*
52+
* ```typescript
53+
* import { exprQuote } from "./expr_string.ts";
54+
*
55+
* console.log(exprQuote`foo` == "foo"); // outputs: true
56+
* console.log(exprQuote`foo` === "foo"); // outputs: false
57+
* console.log(exprQuote`foo,${40 + 2}` == "foo,42"); // outputs: true
58+
* ```
59+
*
60+
* @see useExprString for usage
61+
*/
62+
export function exprQuote(
63+
template: TemplateStringsArray,
64+
...substitutions: TemplateSubstitutions
65+
): ExprString {
66+
const raw = String.raw(template, ...substitutions);
67+
return Object.assign(raw, {
68+
[EXPR_STRING_MARK]: 1 as const,
69+
});
70+
}
71+
72+
/**
73+
* Returns `true` if the value is a string marked as Vim's string constant format.
74+
*
75+
* ```typescript
76+
* import { exprQuote, isExprString } from "./expr_string.ts";
77+
*
78+
* console.log(isExprString(exprQuote`foo`)); // outputs: true
79+
* console.log(isExprString("foo")); // outputs: false
80+
* ```
81+
*/
82+
export function isExprString(x: unknown): x is ExprString {
83+
return x instanceof String && (x as ExprString)[EXPR_STRING_MARK] === 1;
84+
}
85+
86+
function isJsonable(x: unknown): x is Jsonable {
87+
return x != null && typeof (x as Jsonable).toJSON === "function";
88+
}
89+
90+
/**
91+
* @internal
92+
*/
93+
export function vimStringify(value: unknown, key?: string | number): string {
94+
if (isJsonable(value)) {
95+
return vimStringify(JSON.parse(value.toJSON(key)));
96+
}
97+
if (isExprString(value)) {
98+
return `"${value.replaceAll('"', '\\"')}"`;
99+
}
100+
if (value == null || ["function", "symbol"].includes(typeof value)) {
101+
return "v:null";
102+
}
103+
if (typeof value === "boolean" || value instanceof Boolean) {
104+
return value == true ? "v:true" : "v:false";
105+
}
106+
if (typeof value === "number" || value instanceof Number) {
107+
// Replace `5e-10` to `5.0e-10`
108+
return `${value}`.replace(/^(\d+)e/, "$1.0e");
109+
}
110+
if (typeof value === "string" || value instanceof String) {
111+
return `'${value.replaceAll("'", "''")}'`;
112+
}
113+
if (Array.isArray(value)) {
114+
return `[${value.map(vimStringify).join(",")}]`;
115+
}
116+
if (typeof value === "object") {
117+
return `{${
118+
Object.entries(value)
119+
.filter(([, value]) =>
120+
!["undefined", "function", "symbol"].includes(typeof value)
121+
)
122+
.map(([key, value]) =>
123+
`'${key.replaceAll("'", "''")}':${vimStringify(value, key)}`
124+
)
125+
.join(",")
126+
}}`;
127+
}
128+
const type = Object.prototype.toString.call(value).slice(8, -1);
129+
throw new TypeError(`${type} value can't be serialized`);
130+
}
131+
132+
function trimEndOfArgs(args: unknown[]): unknown[] {
133+
const last = args.findIndex((v) => v === undefined);
134+
return last < 0 ? args : args.slice(0, last);
135+
}
136+
137+
class ExprStringHelper implements Denops {
138+
#denops: Denops;
139+
140+
constructor(denops: Denops) {
141+
this.#denops = denops;
142+
}
143+
144+
get name(): string {
145+
return this.#denops.name;
146+
}
147+
148+
get meta(): Meta {
149+
return this.#denops.meta;
150+
}
151+
152+
get context(): Record<string | number | symbol, unknown> {
153+
return this.#denops.context;
154+
}
155+
156+
get dispatcher(): Dispatcher {
157+
return this.#denops.dispatcher;
158+
}
159+
160+
set dispatcher(dispatcher: Dispatcher) {
161+
this.#denops.dispatcher = dispatcher;
162+
}
163+
164+
redraw(force?: boolean): Promise<void> {
165+
return this.#denops.redraw(force);
166+
}
167+
168+
async call(fn: string, ...args: unknown[]): Promise<unknown> {
169+
const suffix = await ensurePrerequisites(this.#denops);
170+
return this.#denops.call(
171+
`DenopsStdHelperExprStringCall_${suffix}`,
172+
fn,
173+
vimStringify(trimEndOfArgs(args)),
174+
);
175+
}
176+
177+
async batch(...calls: [string, ...unknown[]][]): Promise<unknown[]> {
178+
const suffix = await ensurePrerequisites(this.#denops);
179+
const callHelper = `DenopsStdHelperExprStringCall_${suffix}`;
180+
return this.#denops.batch(
181+
...calls.map(([fn, ...args]): [string, ...unknown[]] => [
182+
callHelper,
183+
fn,
184+
vimStringify(trimEndOfArgs(args)),
185+
]),
186+
);
187+
}
188+
189+
async cmd(cmd: string, ctx: Context = {}): Promise<void> {
190+
await this.call("denops#api#cmd", cmd, ctx);
191+
}
192+
193+
eval(expr: string, ctx: Context = {}): Promise<unknown> {
194+
return this.call("denops#api#eval", expr, ctx);
195+
}
196+
197+
dispatch(name: string, fn: string, ...args: unknown[]): Promise<unknown> {
198+
return this.#denops.dispatch(name, fn, ...args);
199+
}
200+
}
201+
202+
/**
203+
* Call the denops function using Vim's string constant format.
204+
*
205+
* ```typescript
206+
* import { Denops } from "../mod.ts";
207+
* import { exprQuote as q, useExprString } from "./expr_string.ts";
208+
* import * as fn from "../function/mod.ts";
209+
*
210+
* export async function main(denops: Denops): Promise<void> {
211+
* await useExprString(denops, async (denops) => {
212+
* await fn.feedkeys(denops, q`\<Cmd>echo 'foo'\<CR>`)
213+
* await denops.cmd('echo value', { value: q`\U0001F680` })
214+
* });
215+
* }
216+
* ```
217+
*/
218+
export function useExprString<T extends unknown>(
219+
denops: Denops,
220+
executor: (helper: ExprStringHelper) => Promise<T>,
221+
): Promise<T> {
222+
const helper = new ExprStringHelper(denops);
223+
return executor(helper);
224+
}

0 commit comments

Comments
 (0)