Skip to content

Commit

Permalink
👍 Add NaivePlugin that directly loads in main thread
Browse files Browse the repository at this point in the history
  • Loading branch information
lambdalisue committed Nov 26, 2023
1 parent e4b28b2 commit b5dcab8
Show file tree
Hide file tree
Showing 3 changed files with 344 additions and 0 deletions.
80 changes: 80 additions & 0 deletions denops/@denops-private/plugin/naive/denops.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ensure, is } from "https://deno.land/x/[email protected]/mod.ts";
import {
BatchError,
Context,
Denops,
Dispatcher,
Meta,
} from "../../../@denops/mod.ts";
import type { Service } from "../../service.ts";

const isBatchReturn = is.TupleOf([is.Array, is.String] as const);

export class DenopsImpl implements Denops {
readonly context: Record<string | number | symbol, unknown> = {};
readonly name: string;
dispatcher: Dispatcher = {};
#service: Service;

constructor(
name: string,
service: Service,
) {
this.name = name;
this.#service = service;
}

get meta(): Meta {
return this.#service.meta;
}

redraw(force?: boolean): Promise<void> {
return this.#service.host.redraw(force);
}

call(fn: string, ...args: unknown[]): Promise<unknown> {
return this.#service.host.call(fn, ...normArgs(args));
}

batch(
...calls: [string, ...unknown[]][]
): Promise<unknown[]> {
const normCalls = calls.map(([fn, ...args]) =>
[fn, ...normArgs(args)] as const
);
return this.#service.host.batch(...normCalls).then((ret) => {
const [results, errmsg] = ensure(ret, isBatchReturn);
if (errmsg !== "") {
throw new BatchError(errmsg, results);
}
return results;
});
}

cmd(cmd: string, ctx: Context = {}): Promise<void> {
return this.#service.host.call("denops#api#cmd", cmd, ctx).then();
}

eval(expr: string, ctx: Context = {}): Promise<unknown> {
return this.#service.host.call("denops#api#eval", expr, ctx);
}

dispatch(
name: string,
fn: string,
...args: unknown[]
): Promise<unknown> {
return this.#service.dispatch(name, fn, args);
}
}

function normArgs(args: unknown[]): unknown[] {
const normArgs = [];
for (const arg of args) {
if (arg === undefined) {
break;
}
normArgs.push(arg);
}
return normArgs;
}
221 changes: 221 additions & 0 deletions denops/@denops-private/plugin/naive/denops_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import * as path from "https://deno.land/[email protected]/path/mod.ts";
import {
assertEquals,
assertRejects,
} from "https://deno.land/[email protected]/assert/mod.ts";
import { test } from "https://deno.land/x/[email protected]/mod.ts";
import { BatchError } from "../../../@denops/mod.ts";

test({
mode: "all",
name: "impl",
fn: async (denops, t) => {
await t.step({
name: "denops.redraw() does nothing",
fn: async () => {
assertEquals(
await denops.redraw(),
undefined,
);

assertEquals(
await denops.redraw(true),
undefined,
);

assertEquals(
await denops.redraw(false),
undefined,
);
},
});

await t.step({
name: "denops.call() calls a Vim/Neovim function and return a result",
fn: async () => {
assertEquals(
await denops.call("range", 10),
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
);
},
});

await t.step({
name: "denops.call() calls a Vim/Neovim function and throw an error",
fn: async () => {
await assertRejects(
async () => {
await denops.call("no-such-function");
},
"E117: Unknown function: no-such-function",
);
},
});

await t.step({
name:
"denops.call() drop arguments after `undefined` (but `null`) for convenience",
fn: async () => {
assertEquals(
await denops.call("denops#api#id", 0, 1, 2),
[0, 1, 2],
);
assertEquals(
await denops.call("denops#api#id", 0, 1, undefined, 2),
[0, 1],
);
assertEquals(
await denops.call("denops#api#id", 0, undefined, 1, 2),
[0],
);
assertEquals(
await denops.call("denops#api#id", 0, 1, null, 2),
[0, 1, null, 2],
);
assertEquals(
await denops.call("denops#api#id", 0, null, 1, 2),
[0, null, 1, 2],
);
},
});

await t.step({
name: "denops.cmd() invoke a Vim/Neovim command",
fn: async () => {
await denops.cmd("execute 'let g:denops_test = value'", {
value: "Hello World",
});
assertEquals(
await denops.eval("g:denops_test") as string,
"Hello World",
);
},
});

await t.step({
name: "denops.cmd() invoke a Vim/Neovim command and throw an error",
fn: async () => {
await assertRejects(
async () => {
await denops.cmd("NoSuchCommand");
},
"E492: Not an editor command: NoSuchCommand",
);
},
});

await t.step({
name:
"denops.eval() evaluate a Vim/Neovim expression and return a result",
fn: async () => {
await denops.cmd("execute 'let g:denops_test = value'", {
value: "Hello World",
});
assertEquals(
await denops.eval("g:denops_test") as string,
"Hello World",
);
},
});

await t.step({
name: "denops.eval() evaluate a Vim/Neovim expression and throw an error",
fn: async () => {
await assertRejects(
async () => {
await denops.eval("g:no_such_variable");
},
"g:no_such_variable",
// Vim: "E15: Invalid expression: g:no_such_variable",
// Neovim: "E121: Undefined variable: g:no_such_variable",
);
},
});

await t.step({
name:
"denops.batch() calls multiple Vim/Neovim functions and return results",
fn: async () => {
const results = await denops.batch(["range", 1], ["range", 2], [
"range",
3,
]);
assertEquals(results, [[0], [0, 1], [0, 1, 2]]);
},
});

await t.step({
name:
"denops.batch() calls multiple Vim/Neovim functions and throws an error with results",
fn: async () => {
await assertRejects(async () => {
await denops.batch(
["range", 1],
["no-such-function", 2],
["range", 3],
);
}, BatchError);
},
});

await t.step({
name:
"denops.batch() drop arguments after `undefined` (but `null`) for convenience",
fn: async () => {
const results = await denops.batch(
["denops#api#id", 0, 1, 2],
["denops#api#id", 0, 1, undefined, 2],
["denops#api#id", 0, undefined, 1, 2],
["denops#api#id", 0, 1, null, 2],
["denops#api#id", 0, null, 1, 2],
);
assertEquals(results, [[0, 1, 2], [0, 1], [0], [0, 1, null, 2], [
0,
null,
1,
2,
]]);
},
});

await t.step({
name: "denops.call() works properly even when called concurrently",
fn: async () => {
const cwd = await denops.call("getcwd") as string;
await denops.cmd("edit dummy1");
await denops.cmd("file dummy2");
const results = await Promise.all([
denops.call("expand", "%"),
denops.call("expand", "%:p"),
denops.call("expand", "%hello"),
denops.call("expand", "#"),
denops.call("expand", "#:p"),
denops.call("expand", "#hello"),
]);
assertEquals(results, [
"dummy2",
path.join(cwd, "dummy2"),
"dummy2",
"dummy1",
path.join(cwd, "dummy1"),
"dummy1",
]);
},
});

await t.step({
name: "denops.dispatch() invokes APIs of the plugin",
fn: async () => {
denops.dispatcher = {
hello(name: unknown): Promise<unknown> {
return Promise.resolve(`Hello ${name}`);
},
};
assertEquals(
await denops.dispatch(denops.name, "hello", "denops"),
"Hello denops",
);
},
});
},
});
43 changes: 43 additions & 0 deletions denops/@denops-private/plugin/naive/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Service } from "../../service.ts";
import type { Plugin } from "../base.ts";
import type { Denops, Dispatcher } from "../../../@denops/mod.ts";
import { DenopsImpl } from "./denops.ts";

export class NaivePlugin implements Plugin {
#denops: DenopsImpl;

readonly name: string;
readonly script: string;

dispatcher: Dispatcher = {};

constructor(name: string, script: string, service: Service) {
this.name = name;
this.script = script;
this.#denops = new DenopsImpl(name, service);
const suffix = `#${performance.now()}`;
import(`${script}${suffix}`).then(async (mod) => {
try {
await emit(this.#denops, `DenopsSystemPluginPre:${name}`);
await mod.main(this.#denops);
await emit(this.#denops, `DenopsSystemPluginPost:${name}`);
} catch (e) {
console.error(e);
await emit(this.#denops, `DenopsSystemPluginFail:${name}`);
}
});
}

async call(fn: string, ...args: unknown[]): Promise<unknown> {
return await this.#denops.dispatcher[fn](...args);
}

dispose(): void {
// Do nothing
}
}

function emit(denops: Denops, name: string): Promise<void> {
return denops.cmd(`doautocmd <nomodeline> User ${name}`)
.catch((e) => console.warn(`Failed to emit ${name}: ${e}`));
}

0 comments on commit b5dcab8

Please sign in to comment.