Skip to content

Commit

Permalink
Merge pull request #260 from vim-denops/eval
Browse files Browse the repository at this point in the history
👍 add `eval` module and mark deprecated `helper/expr_string`
  • Loading branch information
lambdalisue committed Aug 26, 2024
2 parents 0fe0e5e + 6fea3d2 commit 8cd0dc0
Show file tree
Hide file tree
Showing 17 changed files with 2,601 additions and 186 deletions.
13 changes: 12 additions & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
"./batch": "./batch/mod.ts",
"./buffer": "./buffer/mod.ts",
"./bufname": "./bufname/mod.ts",
"./eval": "./eval/mod.ts",
"./eval/expression": "./eval/expression.ts",
"./eval/stringify": "./eval/stringify.ts",
"./eval/string": "./eval/string.ts",
"./eval/use-eval": "./eval/use_eval.ts",
"./function": "./function/mod.ts",
"./function/nvim": "./function/nvim/mod.ts",
"./function/vim": "./function/vim/mod.ts",
Expand Down Expand Up @@ -41,12 +46,13 @@
]
},
"imports": {
"@core/unknownutil": "jsr:@core/unknownutil@^4.0.0",
"@core/unknownutil": "jsr:@core/unknownutil@^4.3.0",
"@denops/core": "jsr:@denops/core@^7.0.0",
"@denops/test": "jsr:@denops/test@^3.0.1",
"@lambdalisue/errorutil": "jsr:@lambdalisue/errorutil@^1.0.0",
"@lambdalisue/itertools": "jsr:@lambdalisue/itertools@^1.1.2",
"@lambdalisue/unreachable": "jsr:@lambdalisue/unreachable@^1.0.1",
"@nick/dispose": "jsr:@nick/dispose@^1.1.0",
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/fs": "jsr:@std/fs@^1.0.0",
"@std/path": "jsr:@std/path@^1.0.2",
Expand All @@ -61,6 +67,11 @@
"jsr:@denops/std/batch": "./batch/mod.ts",
"jsr:@denops/std/buffer": "./buffer/mod.ts",
"jsr:@denops/std/bufname": "./bufname/mod.ts",
"jsr:@denops/std/eval": "./eval/mod.ts",
"jsr:@denops/std/eval/expression": "./eval/expression.ts",
"jsr:@denops/std/eval/stringify": "./eval/stringify.ts",
"jsr:@denops/std/eval/string": "./eval/string.ts",
"jsr:@denops/std/eval/use-eval": "./eval/use_eval.ts",
"jsr:@denops/std/function": "./function/mod.ts",
"jsr:@denops/std/function/nvim": "./function/nvim/mod.ts",
"jsr:@denops/std/function/vim": "./function/vim/mod.ts",
Expand Down
120 changes: 120 additions & 0 deletions eval/expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* This module provides utilities for creating Vim expressions in TypeScript.
*
* @module
*/

import type { Predicate } from "@core/unknownutil/type";
import { isIntersectionOf } from "@core/unknownutil/is/intersection-of";
import { isLiteralOf } from "@core/unknownutil/is/literal-of";
import { isObjectOf } from "@core/unknownutil/is/object-of";
import {
isVimEvaluatable,
type VimEvaluatable,
vimExpressionOf,
} from "./vim_evaluatable.ts";
import { stringify } from "./stringify.ts";

/**
* An object that defines a Vim's expression.
*
* Note that although it inherits from primitive `string` for convenience, it
* actually inherits from the `String` wrapper object.
*
* ```typescript
* import { assertEquals } from "jsr:@std/assert/equals";
* import { expr } from "jsr:@denops/std/eval/expression";
*
* const s: string = expr`foo`;
* assertEquals(typeof s, "object"); // is not "string"
* ```
*/
export type Expression = string & ExpressionProps;

interface ExpressionProps extends VimEvaluatable {
/**
* Used by the `JSON.stringify` to enable the transformation of an object's data to JSON serialization.
*/
toJSON(): string;

readonly [Symbol.toStringTag]: "Expression";
}

/**
* Create a {@linkcode Expression} that evaluates as a Vim expression.
*
* `raw_vim_expression` is a string text representing a raw Vim expression.
* Backslashes are treated as Vim syntax rather than JavaScript string escape
* sequences. Note that the raw Vim expression has not been verified and is
* therefore **UNSAFE**.
*
* `${value}` is the expression to be inserted at the current position, whose
* value is converted to the corresponding Vim expression. The conversion is
* safe and an error is thrown if it cannot be converted. Note that if the
* value contains `Expression` it will be inserted as-is, this is **UNSAFE**.
*
* ```typescript
* import { assertEquals } from "jsr:@std/assert/equals";
* import { expr } from "jsr:@denops/std/eval/expression";
*
* assertEquals(
* expr`raw_vim_expression`.toString(),
* "raw_vim_expression",
* );
*
* const value = ["foo", 123, null];
* assertEquals(
* expr`raw_vim_expression + ${value}`.toString(),
* "raw_vim_expression + ['foo',123,v:null]"
* );
* ```
*/
export function expr(
template: TemplateStringsArray,
...substitutions: TemplateSubstitutions
): Expression {
const values = substitutions.map(stringify);
const raw = String.raw(template, ...values);
return new ExpressionImpl(raw) as unknown as Expression;
}

/**
* Returns `true` if the value is a {@linkcode Expression}.
*
* ```typescript
* import { assert, assertFalse } from "jsr:@std/assert";
* import { isExpression, expr } from "jsr:@denops/std/eval/expression";
*
* assert(isExpression(expr`123`));
*
* assertFalse(isExpression("456"));
* ```
*/
export const isExpression: Predicate<Expression> = isIntersectionOf([
// NOTE: Do not check `isString` here, because `Expression` has a different type in definition (primitive `string`) and implementation (`String`).
isObjectOf({
[Symbol.toStringTag]: isLiteralOf("Expression"),
}),
isVimEvaluatable,
]) as unknown as Predicate<Expression>;

// deno-lint-ignore no-explicit-any
type TemplateSubstitutions = any[];

class ExpressionImpl extends String implements ExpressionProps {
get [Symbol.toStringTag]() {
return "Expression" as const;
}

[vimExpressionOf](): string {
return this.valueOf();
}

toJSON(): string {
return this[vimExpressionOf]();
}

[Symbol.for("Deno.customInspect")]() {
return `[${this[Symbol.toStringTag]}: ${Deno.inspect(this.valueOf())}]`;
}
}
73 changes: 73 additions & 0 deletions eval/expression_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { assertEquals } from "@std/assert";
import { test } from "@denops/test";
import { vimExpressionOf } from "./vim_evaluatable.ts";
import { rawString } from "./string.ts";

import { expr, isExpression } from "./expression.ts";

Deno.test("expr()", async (t) => {
await t.step("returns an `Expression`", () => {
const actual = expr`foo`;
assertEquals(actual[Symbol.toStringTag], "Expression");
});
});

test({
mode: "all",
name: "Expression",
fn: async (denops, t) => {
const expression = expr`[42 + ${123}, ${"foo"}, ${null}]`;
await t.step(".@@vimExpressionOf() returns a string", async (t) => {
const actual = expression[vimExpressionOf]();
assertEquals(typeof actual, "string");

await t.step("that evaluates as a Vim expression", async () => {
assertEquals(await denops.eval(actual), [165, "foo", null]);
});
});
await t.step(".toJSON() returns same as @@vimExpressionOf()", () => {
const actual = expression.toJSON();
const expected = expression[vimExpressionOf]();
assertEquals(actual, expected);
});
await t.step(".toString() returns same as @@vimExpressionOf()", () => {
const actual = expression.toString();
const expected = expression[vimExpressionOf]();
assertEquals(actual, expected);
});
await t.step('is inspected as `[Expression: "..."]`', () => {
assertEquals(
Deno.inspect(expression),
`[Expression: "[42 + 123, 'foo', v:null]"]`,
);
});
},
});

Deno.test("isExpression()", async (t) => {
await t.step("returns true if the value is Expression", () => {
const actual = isExpression(expr`foo`);
assertEquals(actual, true);
});
await t.step("returns false if the value is", async (t) => {
const tests: readonly [name: string, value: unknown][] = [
["string", "foo"],
["number", 123],
["undefined", undefined],
["null", null],
["true", true],
["false", false],
["Function", () => 0],
["symbol", Symbol("bar")],
["Array", [0, 1]],
["Object", { key: "a" }],
["RawString", rawString`baz`],
];
for (const [name, value] of tests) {
await t.step(name, () => {
const actual = isExpression(value);
assertEquals(actual, false);
});
}
});
});
38 changes: 38 additions & 0 deletions eval/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* A module to provide values that can be evaluated in Vim.
*
* ```typescript
* import type { Denops } from "jsr:@denops/std";
* import * as fn from "jsr:@denops/std/function";
* import type { Expression, RawString } from "jsr:@denops/std/eval";
* import { expr, rawString, stringify, useEval } from "jsr:@denops/std/eval";
*
* export async function main(denops: Denops): Promise<void> {
* // Create `Expression` with `expr`.
* const vimExpression: Expression = expr`expand('<cword>')`;
*
* // Create `RawString` with `rawString`.
* const vimKeySequence: RawString = rawString`\<Cmd>echo 'foo'\<CR>`;
*
* // Use values in `useEval` block.
* await useEval(denops, async (denops) => {
* await denops.cmd('echo expr', { expr: vimExpression });
* await fn.feedkeys(denops, vimKeySequence);
* });
*
* // Convert values to a string that can be parsed Vim's `eval()`.
* const vimEvaluable: string = stringify({
* expr: vimExpression,
* keys: vimKeySequence,
* });
* await denops.cmd('echo eval(value)', { value: vimEvaluable });
* }
* ```
*
* @module
*/

export * from "./expression.ts";
export * from "./string.ts";
export * from "./stringify.ts";
export * from "./use_eval.ts";
Loading

0 comments on commit 8cd0dc0

Please sign in to comment.