Skip to content

Commit 8cd0dc0

Browse files
authored
Merge pull request #260 from vim-denops/eval
👍 add `eval` module and mark deprecated `helper/expr_string`
2 parents 0fe0e5e + 6fea3d2 commit 8cd0dc0

17 files changed

+2601
-186
lines changed

deno.jsonc

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
"./batch": "./batch/mod.ts",
99
"./buffer": "./buffer/mod.ts",
1010
"./bufname": "./bufname/mod.ts",
11+
"./eval": "./eval/mod.ts",
12+
"./eval/expression": "./eval/expression.ts",
13+
"./eval/stringify": "./eval/stringify.ts",
14+
"./eval/string": "./eval/string.ts",
15+
"./eval/use-eval": "./eval/use_eval.ts",
1116
"./function": "./function/mod.ts",
1217
"./function/nvim": "./function/nvim/mod.ts",
1318
"./function/vim": "./function/vim/mod.ts",
@@ -41,12 +46,13 @@
4146
]
4247
},
4348
"imports": {
44-
"@core/unknownutil": "jsr:@core/unknownutil@^4.0.0",
49+
"@core/unknownutil": "jsr:@core/unknownutil@^4.3.0",
4550
"@denops/core": "jsr:@denops/core@^7.0.0",
4651
"@denops/test": "jsr:@denops/test@^3.0.1",
4752
"@lambdalisue/errorutil": "jsr:@lambdalisue/errorutil@^1.0.0",
4853
"@lambdalisue/itertools": "jsr:@lambdalisue/itertools@^1.1.2",
4954
"@lambdalisue/unreachable": "jsr:@lambdalisue/unreachable@^1.0.1",
55+
"@nick/dispose": "jsr:@nick/dispose@^1.1.0",
5056
"@std/assert": "jsr:@std/assert@^1.0.0",
5157
"@std/fs": "jsr:@std/fs@^1.0.0",
5258
"@std/path": "jsr:@std/path@^1.0.2",
@@ -61,6 +67,11 @@
6167
"jsr:@denops/std/batch": "./batch/mod.ts",
6268
"jsr:@denops/std/buffer": "./buffer/mod.ts",
6369
"jsr:@denops/std/bufname": "./bufname/mod.ts",
70+
"jsr:@denops/std/eval": "./eval/mod.ts",
71+
"jsr:@denops/std/eval/expression": "./eval/expression.ts",
72+
"jsr:@denops/std/eval/stringify": "./eval/stringify.ts",
73+
"jsr:@denops/std/eval/string": "./eval/string.ts",
74+
"jsr:@denops/std/eval/use-eval": "./eval/use_eval.ts",
6475
"jsr:@denops/std/function": "./function/mod.ts",
6576
"jsr:@denops/std/function/nvim": "./function/nvim/mod.ts",
6677
"jsr:@denops/std/function/vim": "./function/vim/mod.ts",

eval/expression.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* This module provides utilities for creating Vim expressions in TypeScript.
3+
*
4+
* @module
5+
*/
6+
7+
import type { Predicate } from "@core/unknownutil/type";
8+
import { isIntersectionOf } from "@core/unknownutil/is/intersection-of";
9+
import { isLiteralOf } from "@core/unknownutil/is/literal-of";
10+
import { isObjectOf } from "@core/unknownutil/is/object-of";
11+
import {
12+
isVimEvaluatable,
13+
type VimEvaluatable,
14+
vimExpressionOf,
15+
} from "./vim_evaluatable.ts";
16+
import { stringify } from "./stringify.ts";
17+
18+
/**
19+
* An object that defines a Vim's expression.
20+
*
21+
* Note that although it inherits from primitive `string` for convenience, it
22+
* actually inherits from the `String` wrapper object.
23+
*
24+
* ```typescript
25+
* import { assertEquals } from "jsr:@std/assert/equals";
26+
* import { expr } from "jsr:@denops/std/eval/expression";
27+
*
28+
* const s: string = expr`foo`;
29+
* assertEquals(typeof s, "object"); // is not "string"
30+
* ```
31+
*/
32+
export type Expression = string & ExpressionProps;
33+
34+
interface ExpressionProps extends VimEvaluatable {
35+
/**
36+
* Used by the `JSON.stringify` to enable the transformation of an object's data to JSON serialization.
37+
*/
38+
toJSON(): string;
39+
40+
readonly [Symbol.toStringTag]: "Expression";
41+
}
42+
43+
/**
44+
* Create a {@linkcode Expression} that evaluates as a Vim expression.
45+
*
46+
* `raw_vim_expression` is a string text representing a raw Vim expression.
47+
* Backslashes are treated as Vim syntax rather than JavaScript string escape
48+
* sequences. Note that the raw Vim expression has not been verified and is
49+
* therefore **UNSAFE**.
50+
*
51+
* `${value}` is the expression to be inserted at the current position, whose
52+
* value is converted to the corresponding Vim expression. The conversion is
53+
* safe and an error is thrown if it cannot be converted. Note that if the
54+
* value contains `Expression` it will be inserted as-is, this is **UNSAFE**.
55+
*
56+
* ```typescript
57+
* import { assertEquals } from "jsr:@std/assert/equals";
58+
* import { expr } from "jsr:@denops/std/eval/expression";
59+
*
60+
* assertEquals(
61+
* expr`raw_vim_expression`.toString(),
62+
* "raw_vim_expression",
63+
* );
64+
*
65+
* const value = ["foo", 123, null];
66+
* assertEquals(
67+
* expr`raw_vim_expression + ${value}`.toString(),
68+
* "raw_vim_expression + ['foo',123,v:null]"
69+
* );
70+
* ```
71+
*/
72+
export function expr(
73+
template: TemplateStringsArray,
74+
...substitutions: TemplateSubstitutions
75+
): Expression {
76+
const values = substitutions.map(stringify);
77+
const raw = String.raw(template, ...values);
78+
return new ExpressionImpl(raw) as unknown as Expression;
79+
}
80+
81+
/**
82+
* Returns `true` if the value is a {@linkcode Expression}.
83+
*
84+
* ```typescript
85+
* import { assert, assertFalse } from "jsr:@std/assert";
86+
* import { isExpression, expr } from "jsr:@denops/std/eval/expression";
87+
*
88+
* assert(isExpression(expr`123`));
89+
*
90+
* assertFalse(isExpression("456"));
91+
* ```
92+
*/
93+
export const isExpression: Predicate<Expression> = isIntersectionOf([
94+
// NOTE: Do not check `isString` here, because `Expression` has a different type in definition (primitive `string`) and implementation (`String`).
95+
isObjectOf({
96+
[Symbol.toStringTag]: isLiteralOf("Expression"),
97+
}),
98+
isVimEvaluatable,
99+
]) as unknown as Predicate<Expression>;
100+
101+
// deno-lint-ignore no-explicit-any
102+
type TemplateSubstitutions = any[];
103+
104+
class ExpressionImpl extends String implements ExpressionProps {
105+
get [Symbol.toStringTag]() {
106+
return "Expression" as const;
107+
}
108+
109+
[vimExpressionOf](): string {
110+
return this.valueOf();
111+
}
112+
113+
toJSON(): string {
114+
return this[vimExpressionOf]();
115+
}
116+
117+
[Symbol.for("Deno.customInspect")]() {
118+
return `[${this[Symbol.toStringTag]}: ${Deno.inspect(this.valueOf())}]`;
119+
}
120+
}

eval/expression_test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { assertEquals } from "@std/assert";
2+
import { test } from "@denops/test";
3+
import { vimExpressionOf } from "./vim_evaluatable.ts";
4+
import { rawString } from "./string.ts";
5+
6+
import { expr, isExpression } from "./expression.ts";
7+
8+
Deno.test("expr()", async (t) => {
9+
await t.step("returns an `Expression`", () => {
10+
const actual = expr`foo`;
11+
assertEquals(actual[Symbol.toStringTag], "Expression");
12+
});
13+
});
14+
15+
test({
16+
mode: "all",
17+
name: "Expression",
18+
fn: async (denops, t) => {
19+
const expression = expr`[42 + ${123}, ${"foo"}, ${null}]`;
20+
await t.step(".@@vimExpressionOf() returns a string", async (t) => {
21+
const actual = expression[vimExpressionOf]();
22+
assertEquals(typeof actual, "string");
23+
24+
await t.step("that evaluates as a Vim expression", async () => {
25+
assertEquals(await denops.eval(actual), [165, "foo", null]);
26+
});
27+
});
28+
await t.step(".toJSON() returns same as @@vimExpressionOf()", () => {
29+
const actual = expression.toJSON();
30+
const expected = expression[vimExpressionOf]();
31+
assertEquals(actual, expected);
32+
});
33+
await t.step(".toString() returns same as @@vimExpressionOf()", () => {
34+
const actual = expression.toString();
35+
const expected = expression[vimExpressionOf]();
36+
assertEquals(actual, expected);
37+
});
38+
await t.step('is inspected as `[Expression: "..."]`', () => {
39+
assertEquals(
40+
Deno.inspect(expression),
41+
`[Expression: "[42 + 123, 'foo', v:null]"]`,
42+
);
43+
});
44+
},
45+
});
46+
47+
Deno.test("isExpression()", async (t) => {
48+
await t.step("returns true if the value is Expression", () => {
49+
const actual = isExpression(expr`foo`);
50+
assertEquals(actual, true);
51+
});
52+
await t.step("returns false if the value is", async (t) => {
53+
const tests: readonly [name: string, value: unknown][] = [
54+
["string", "foo"],
55+
["number", 123],
56+
["undefined", undefined],
57+
["null", null],
58+
["true", true],
59+
["false", false],
60+
["Function", () => 0],
61+
["symbol", Symbol("bar")],
62+
["Array", [0, 1]],
63+
["Object", { key: "a" }],
64+
["RawString", rawString`baz`],
65+
];
66+
for (const [name, value] of tests) {
67+
await t.step(name, () => {
68+
const actual = isExpression(value);
69+
assertEquals(actual, false);
70+
});
71+
}
72+
});
73+
});

eval/mod.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* A module to provide values that can be evaluated in Vim.
3+
*
4+
* ```typescript
5+
* import type { Denops } from "jsr:@denops/std";
6+
* import * as fn from "jsr:@denops/std/function";
7+
* import type { Expression, RawString } from "jsr:@denops/std/eval";
8+
* import { expr, rawString, stringify, useEval } from "jsr:@denops/std/eval";
9+
*
10+
* export async function main(denops: Denops): Promise<void> {
11+
* // Create `Expression` with `expr`.
12+
* const vimExpression: Expression = expr`expand('<cword>')`;
13+
*
14+
* // Create `RawString` with `rawString`.
15+
* const vimKeySequence: RawString = rawString`\<Cmd>echo 'foo'\<CR>`;
16+
*
17+
* // Use values in `useEval` block.
18+
* await useEval(denops, async (denops) => {
19+
* await denops.cmd('echo expr', { expr: vimExpression });
20+
* await fn.feedkeys(denops, vimKeySequence);
21+
* });
22+
*
23+
* // Convert values to a string that can be parsed Vim's `eval()`.
24+
* const vimEvaluable: string = stringify({
25+
* expr: vimExpression,
26+
* keys: vimKeySequence,
27+
* });
28+
* await denops.cmd('echo eval(value)', { value: vimEvaluable });
29+
* }
30+
* ```
31+
*
32+
* @module
33+
*/
34+
35+
export * from "./expression.ts";
36+
export * from "./string.ts";
37+
export * from "./stringify.ts";
38+
export * from "./use_eval.ts";

0 commit comments

Comments
 (0)