-
-
Couldn't load subscription status.
- Fork 33
👍 add plugin unload feature #385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
93d53bf
3759c61
9552317
4c197e9
a519c0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,35 @@ | ||||||||||||||
| // TODO: #349 Update `Entrypoint` in denops-core, remove this module from `$.test.exclude` in `deno.jsonc`, and remove this module. | ||||||||||||||
| import type { Denops } from "jsr:@denops/[email protected]"; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Denops's entrypoint definition. | ||||||||||||||
| * | ||||||||||||||
| * Use this type to ensure the `main` function is properly implemented like: | ||||||||||||||
| * | ||||||||||||||
| * ```ts | ||||||||||||||
| * import type { Entrypoint } from "jsr:@denops/core"; | ||||||||||||||
| * | ||||||||||||||
| * export const main: Entrypoint = (denops) => { | ||||||||||||||
| * // ... | ||||||||||||||
| * } | ||||||||||||||
| * ``` | ||||||||||||||
| * | ||||||||||||||
| * If an `AsyncDisposable` object is returned, resources can be disposed of | ||||||||||||||
| * asynchronously when the plugin is unloaded, like: | ||||||||||||||
| * | ||||||||||||||
| * ```ts | ||||||||||||||
| * import type { Entrypoint } from "jsr:@denops/core"; | ||||||||||||||
| * | ||||||||||||||
| * export const main: Entrypoint = (denops) => { | ||||||||||||||
| * // ... | ||||||||||||||
| * return { | ||||||||||||||
| * [Symbol.asyncDispose]: () => { | ||||||||||||||
| * // Dispose resources... | ||||||||||||||
| * } | ||||||||||||||
| * } | ||||||||||||||
| * } | ||||||||||||||
| * ``` | ||||||||||||||
| */ | ||||||||||||||
| export type Entrypoint = ( | ||||||||||||||
| denops: Denops, | ||||||||||||||
| ) => void | AsyncDisposable | Promise<void | AsyncDisposable>; | ||||||||||||||
|
Comment on lines
+33
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid using Using -export type Entrypoint = (
- denops: Denops,
-) => void | AsyncDisposable | Promise<void | AsyncDisposable>;
+export type Entrypoint = (
+ denops: Denops,
+) => undefined | AsyncDisposable | Promise<undefined | AsyncDisposable>;Committable suggestion
Suggested change
ToolsBiome
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| // TODO: #349 Import `Entrypoint` from denops-core. | ||
| // import type { Entrypoint } from "jsr:@denops/[email protected]"; | ||
| import type { Entrypoint } from "./plugin.ts"; | ||
| import type { Denops, Meta } from "jsr:@denops/[email protected]"; | ||
| import { toFileUrl } from "jsr:@std/[email protected]/to-file-url"; | ||
| import { toErrorObject } from "jsr:@lambdalisue/[email protected]"; | ||
|
|
@@ -38,11 +41,7 @@ export class Service implements HostService, AsyncDisposable { | |
| this.#host = host; | ||
| } | ||
|
|
||
| async load( | ||
| name: string, | ||
| script: string, | ||
| suffix = "", | ||
| ): Promise<void> { | ||
| async load(name: string, script: string): Promise<void> { | ||
| if (this.#closed) { | ||
| throw new Error("Service closed"); | ||
| } | ||
|
|
@@ -58,26 +57,39 @@ export class Service implements HostService, AsyncDisposable { | |
| const denops = new DenopsImpl(name, this.#meta, this.#host, this); | ||
| const plugin = new Plugin(denops, name, script); | ||
| this.#plugins.set(name, plugin); | ||
| await plugin.load(suffix); | ||
| this.#getWaiter(name).resolve(); | ||
| try { | ||
| await plugin.waitLoaded(); | ||
| this.#getWaiter(name).resolve(); | ||
| } catch { | ||
| this.#plugins.delete(name); | ||
| } | ||
| } | ||
|
|
||
| reload( | ||
| name: string, | ||
| ): Promise<void> { | ||
| async #unload(name: string): Promise<Plugin | undefined> { | ||
| const plugin = this.#plugins.get(name); | ||
| if (!plugin) { | ||
| if (this.#meta.mode === "debug") { | ||
| console.log(`A denops plugin '${name}' is not loaded yet. Skip`); | ||
| } | ||
| return Promise.resolve(); | ||
| return; | ||
| } | ||
| this.#waiters.get(name)?.promise.finally(() => { | ||
| this.#waiters.delete(name); | ||
| }); | ||
| this.#plugins.delete(name); | ||
| this.#waiters.delete(name); | ||
| // Import module with fragment so that reload works properly | ||
| // https://github.com/vim-denops/denops.vim/issues/227 | ||
| const suffix = `#${performance.now()}`; | ||
| return this.load(name, plugin.script, suffix); | ||
| await plugin.unload(); | ||
| return plugin; | ||
| } | ||
|
|
||
| async unload(name: string): Promise<void> { | ||
| await this.#unload(name); | ||
| } | ||
|
|
||
| async reload(name: string): Promise<void> { | ||
| const plugin = await this.#unload(name); | ||
| if (plugin) { | ||
| await this.load(name, plugin.script); | ||
| } | ||
| } | ||
|
|
||
| waitLoaded(name: string): Promise<void> { | ||
|
|
@@ -137,14 +149,17 @@ export class Service implements HostService, AsyncDisposable { | |
| } | ||
| } | ||
|
|
||
| close(): Promise<void> { | ||
| async close(): Promise<void> { | ||
| if (!this.#closed) { | ||
| this.#closed = true; | ||
| const error = new Error("Service closed"); | ||
| for (const { reject } of this.#waiters.values()) { | ||
| reject(error); | ||
| } | ||
| this.#waiters.clear(); | ||
| await Promise.all( | ||
| [...this.#plugins.values()].map((plugin) => plugin.unload()), | ||
| ); | ||
| this.#plugins.clear(); | ||
| this.#host = undefined; | ||
| this.#closedWaiter.resolve(); | ||
|
|
@@ -161,8 +176,14 @@ export class Service implements HostService, AsyncDisposable { | |
| } | ||
| } | ||
|
|
||
| type PluginModule = { | ||
| main: Entrypoint; | ||
| }; | ||
|
|
||
| class Plugin { | ||
| #denops: Denops; | ||
| #loadedWaiter: Promise<void>; | ||
| #disposable: Partial<AsyncDisposable> = {}; | ||
Milly marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| readonly name: string; | ||
| readonly script: string; | ||
|
|
@@ -171,13 +192,19 @@ class Plugin { | |
| this.#denops = denops; | ||
| this.name = name; | ||
| this.script = resolveScriptUrl(script); | ||
| this.#loadedWaiter = this.#load(); | ||
| } | ||
|
|
||
| async load(suffix = ""): Promise<void> { | ||
| waitLoaded(): Promise<void> { | ||
| return this.#loadedWaiter; | ||
| } | ||
|
|
||
| async #load(): Promise<void> { | ||
| const suffix = createScriptSuffix(this.script); | ||
| try { | ||
| await emit(this.#denops, `DenopsSystemPluginPre:${this.name}`); | ||
| const mod = await import(`${this.script}${suffix}`); | ||
| await mod.main(this.#denops); | ||
| const mod: PluginModule = await import(`${this.script}${suffix}`); | ||
| this.#disposable = await mod.main(this.#denops) ?? {}; | ||
Milly marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| await emit(this.#denops, `DenopsSystemPluginPost:${this.name}`); | ||
| } catch (e) { | ||
| // Show a warning message when Deno module cache issue is detected | ||
|
|
@@ -195,6 +222,27 @@ class Plugin { | |
| } | ||
| console.error(`Failed to load plugin '${this.name}': ${e}`); | ||
| await emit(this.#denops, `DenopsSystemPluginFail:${this.name}`); | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| async unload(): Promise<void> { | ||
| try { | ||
| // Wait for the load to complete to make the events atomically. | ||
| await this.#loadedWaiter; | ||
| } catch { | ||
| // Load failed, do nothing | ||
| return; | ||
| } | ||
| try { | ||
| await emit(this.#denops, `DenopsSystemPluginUnloadPre:${this.name}`); | ||
| await this.#disposable[Symbol.asyncDispose]?.(); | ||
| await emit(this.#denops, `DenopsSystemPluginUnloadPost:${this.name}`); | ||
| } catch (e) { | ||
| console.error(`Failed to unload plugin '${this.name}': ${e}`); | ||
| await emit(this.#denops, `DenopsSystemPluginUnloadFail:${this.name}`); | ||
| } finally { | ||
| this.#disposable = {}; | ||
Milly marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
|
|
@@ -210,6 +258,16 @@ class Plugin { | |
| } | ||
| } | ||
|
|
||
| const loadedScripts = new Set<string>(); | ||
|
|
||
| function createScriptSuffix(script: string): string { | ||
| // Import module with fragment so that reload works properly | ||
| // https://github.com/vim-denops/denops.vim/issues/227 | ||
| const suffix = loadedScripts.has(script) ? `#${performance.now()}` : ""; | ||
| loadedScripts.add(script); | ||
| return suffix; | ||
| } | ||
|
|
||
| async function emit(denops: Denops, name: string): Promise<void> { | ||
| try { | ||
| await denops.cmd(`doautocmd <nomodeline> User ${name}`); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.