@lvce-editor/api 8.40.0 → 8.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { executeSourceControlAcceptInput, executeSourceControlAdd, executeSource
12
12
  export { createViewInstance, dispatchViewEvent, disposeViewInstance, executeViewProvider, getViewRegistrySnapshot, renderViewInstance, registerView, resetViewRegistry, saveViewInstanceState, } from './parts/ViewRegistry/ViewRegistry.ts';
13
13
  export { createOutputChannel, getOutputChannelRegistrySnapshot, resetOutputChannelRegistry } from './parts/OutputChannel/OutputChannel.ts';
14
14
  export { getStatusBarItemProviderRegistrySnapshot, registerStatusBarItemProvider, resetStatusBarItemProviderRegistry, } from './parts/StatusBar/StatusBar.ts';
15
- export type { RegisteredView, View, ViewAction, ViewContext, ViewEvent, ViewKind, MenuEntry, ViewRegistrySnapshot, ViewRenderResult, VirtualDomViewInstance, } from './parts/View/View.ts';
15
+ export type { RegisteredView, View, ViewAction, ViewCommand, ViewContext, ViewEvent, ViewKind, MenuEntry, ViewRegistrySnapshot, ViewRenderResult, VirtualDomViewInstance, } from './parts/View/View.ts';
16
16
  export { handleExtensionManagementMessagePort } from './parts/HandleExtensionManagementMessagePort/HandleExtensionManagementMessagePort.ts';
17
17
  export type { Command } from './parts/Command/Command.ts';
18
18
  export type { CommandCallback } from './parts/CommandCallback/CommandCallback.ts';
@@ -42,10 +42,13 @@ export interface VirtualDomViewInstance {
42
42
  readonly render: () => readonly VirtualDomNode[] | Promise<readonly VirtualDomNode[]>;
43
43
  readonly renderActions?: () => readonly ViewAction[] | Promise<readonly ViewAction[]>;
44
44
  readonly renderFocus?: (oldContext: Readonly<Record<string, boolean>>, newContext: Readonly<Record<string, boolean>>) => string | Promise<string>;
45
+ readonly renderTitle?: () => string | Promise<string>;
45
46
  readonly saveState?: () => unknown;
46
47
  }
47
- export interface View {
48
- readonly create: (context?: ViewContext) => unknown;
48
+ export type ViewCommand<State = VirtualDomViewInstance, Args extends readonly any[] = readonly any[]> = (state: State, ...args: Args) => State | Promise<State>;
49
+ export interface View<State = unknown> {
50
+ readonly commands?: Readonly<Record<string, ViewCommand<State>>>;
51
+ readonly create: (context?: ViewContext) => State | Promise<State>;
49
52
  readonly displayName?: string;
50
53
  readonly eventListeners?: readonly DomEventListener[];
51
54
  readonly icon?: string;
@@ -69,11 +72,13 @@ export interface ViewRegistrySnapshot {
69
72
  export interface ViewRenderResultDom {
70
73
  readonly dom: readonly VirtualDomNode[];
71
74
  readonly focusSelector?: string;
75
+ readonly title?: string;
72
76
  readonly type: 'setDom';
73
77
  }
74
78
  export interface ViewRenderResultPatches {
75
79
  readonly focusSelector?: string;
76
80
  readonly patches: readonly unknown[];
81
+ readonly title?: string;
77
82
  readonly type: 'setPatches';
78
83
  }
79
84
  export type ViewRenderResult = ViewRenderResultDom | ViewRenderResultPatches;
@@ -1,6 +1,6 @@
1
1
  import type { Disposable } from '../Disposable/Disposable.ts';
2
2
  import type { MenuEntry, View, ViewAction, ViewContext, ViewEvent, ViewRegistrySnapshot, ViewRenderResult } from '../View/View.ts';
3
- export declare const registerView: (view: View) => Disposable;
3
+ export declare const registerView: <State>(view: View<State>) => Disposable;
4
4
  export declare const executeViewProvider: (id: string) => unknown;
5
5
  export declare const createViewInstance: (viewId: string, uid: number, context?: ViewContext) => Promise<ViewRenderResult>;
6
6
  export declare const dispatchViewEvent: (uid: number, event: ViewEvent) => Promise<ViewRenderResult>;
@@ -1,11 +1,14 @@
1
1
  import { ExtensionManagementWorker } from '@lvce-editor/rpc-registry';
2
2
  import { diffTree } from '@lvce-editor/virtual-dom-worker';
3
+ import { registerCommand } from "../CommandRegistry/CommandRegistry.js";
3
4
  import { ExtensionApiError } from "../ExtensionApiError/ExtensionApiError.js";
4
5
  const views = Object.create(null);
5
6
  const instances = Object.create(null);
7
+ const instanceUidsByView = Object.create(null);
6
8
  const renderedDoms = Object.create(null);
7
9
  const contexts = Object.create(null);
8
10
  const contextViewIds = Object.create(null);
11
+ const viewCommandDisposables = Object.create(null);
9
12
  const assertBoolean = (value, message) => {
10
13
  if (value !== undefined && typeof value !== 'boolean') {
11
14
  throw new ExtensionApiError(message);
@@ -48,6 +51,23 @@ const assertEventListeners = (view) => {
48
51
  assertEventListener(view.id, listener, index);
49
52
  }
50
53
  };
54
+ const assertCommands = (view) => {
55
+ if (view.commands === undefined) {
56
+ return;
57
+ }
58
+ if (view.kind !== 'virtualDom') {
59
+ throw new ExtensionApiError(`view ${view.id} commands require virtualDom kind`);
60
+ }
61
+ if (!view.commands || typeof view.commands !== 'object' || Array.isArray(view.commands)) {
62
+ throw new ExtensionApiError(`view ${view.id} commands must be an object`);
63
+ }
64
+ for (const [id, command] of Object.entries(view.commands)) {
65
+ assertString(id, `view ${view.id} command is missing id`);
66
+ if (typeof command !== 'function') {
67
+ throw new ExtensionApiError(`view ${view.id} command ${id} must be a function`);
68
+ }
69
+ }
70
+ };
51
71
  const assertView = (view) => {
52
72
  if (!view) {
53
73
  throw new ExtensionApiError('view is not defined');
@@ -61,6 +81,7 @@ const assertView = (view) => {
61
81
  if (view.id in views) {
62
82
  throw new ExtensionApiError(`view ${view.id} is already registered`);
63
83
  }
84
+ assertCommands(view);
64
85
  assertEventListeners(view);
65
86
  };
66
87
  const toRegisteredView = (view) => {
@@ -81,12 +102,61 @@ const toRegisteredView = (view) => {
81
102
  }
82
103
  return registeredView;
83
104
  };
105
+ const getActiveViewInstance = (viewId) => {
106
+ const instanceUids = instanceUidsByView[viewId];
107
+ if (!instanceUids) {
108
+ return undefined;
109
+ }
110
+ const uid = [...instanceUids].at(-1);
111
+ if (uid === undefined) {
112
+ return undefined;
113
+ }
114
+ return [uid, instances[uid]];
115
+ };
116
+ const executeViewCommand = async (view, commandId, args) => {
117
+ const activeInstance = getActiveViewInstance(view.id);
118
+ if (!activeInstance) {
119
+ return;
120
+ }
121
+ const [uid, instance] = activeInstance;
122
+ const command = view.commands[commandId];
123
+ const newInstance = await command(instance, ...args);
124
+ assertVirtualDomViewInstance(view.id, newInstance);
125
+ instances[uid] = newInstance;
126
+ await ExtensionManagementWorker.invoke('Extensions.requestViewRerender', uid);
127
+ };
128
+ const registerViewCommands = (view) => {
129
+ const disposables = [];
130
+ try {
131
+ for (const id of Object.keys(view.commands || {})) {
132
+ disposables.push(registerCommand({
133
+ execute(...args) {
134
+ return executeViewCommand(view, id, args);
135
+ },
136
+ id,
137
+ }));
138
+ }
139
+ }
140
+ catch (error) {
141
+ for (const disposable of disposables) {
142
+ disposable.dispose();
143
+ }
144
+ throw error;
145
+ }
146
+ return disposables;
147
+ };
84
148
  export const registerView = (view) => {
85
149
  assertView(view);
150
+ const commandDisposables = registerViewCommands(view);
86
151
  views[view.id] = view;
152
+ viewCommandDisposables[view.id] = commandDisposables;
87
153
  return {
88
154
  dispose() {
155
+ for (const disposable of commandDisposables) {
156
+ disposable.dispose();
157
+ }
89
158
  delete views[view.id];
159
+ delete viewCommandDisposables[view.id];
90
160
  },
91
161
  };
92
162
  };
@@ -253,6 +323,23 @@ const withFocusSelector = async (result, instance, contextChange) => {
253
323
  focusSelector,
254
324
  };
255
325
  };
326
+ const withTitle = async (result, instance) => {
327
+ if (typeof instance.renderTitle !== 'function') {
328
+ return result;
329
+ }
330
+ const title = await instance.renderTitle();
331
+ if (typeof title !== 'string') {
332
+ throw new ExtensionApiError('view renderTitle result must be a string');
333
+ }
334
+ return {
335
+ ...result,
336
+ title,
337
+ };
338
+ };
339
+ const withRenderMetadata = async (result, instance, contextChange) => {
340
+ const resultWithFocus = await withFocusSelector(result, instance, contextChange);
341
+ return withTitle(resultWithFocus, instance);
342
+ };
256
343
  const maybeClearContext = async (uid, viewId) => {
257
344
  if (!contexts[uid]) {
258
345
  return;
@@ -284,6 +371,9 @@ export const createViewInstance = async (viewId, uid, context) => {
284
371
  });
285
372
  assertVirtualDomViewInstance(viewId, instance);
286
373
  instances[uid] = instance;
374
+ const instanceUids = instanceUidsByView[viewId] || new Set();
375
+ instanceUids.add(uid);
376
+ instanceUidsByView[viewId] = instanceUids;
287
377
  contextViewIds[uid] = viewId;
288
378
  const dom = await renderDom(instance);
289
379
  renderedDoms[uid] = dom;
@@ -292,22 +382,25 @@ export const createViewInstance = async (viewId, uid, context) => {
292
382
  type: 'setDom',
293
383
  };
294
384
  const contextChange = await maybeNotifyContextChanged(uid, viewId, instance);
295
- return withFocusSelector(result, instance, contextChange);
385
+ return withRenderMetadata(result, instance, contextChange);
296
386
  };
297
387
  export const dispatchViewEvent = async (uid, event) => {
298
388
  const instance = getVirtualDomInstance(uid);
389
+ const instanceUids = instanceUidsByView[contextViewIds[uid]];
390
+ instanceUids.delete(uid);
391
+ instanceUids.add(uid);
299
392
  if (typeof instance.handleEvent === 'function') {
300
393
  await instance.handleEvent(event);
301
394
  }
302
395
  const result = await renderPatches(uid, instance);
303
396
  const contextChange = await maybeNotifyContextChanged(uid, contextViewIds[uid], instance);
304
- return withFocusSelector(result, instance, contextChange);
397
+ return withRenderMetadata(result, instance, contextChange);
305
398
  };
306
399
  export const renderViewInstance = async (uid) => {
307
400
  const instance = getVirtualDomInstance(uid);
308
401
  const result = await renderPatches(uid, instance);
309
402
  const contextChange = await maybeNotifyContextChanged(uid, contextViewIds[uid], instance);
310
- return withFocusSelector(result, instance, contextChange);
403
+ return withRenderMetadata(result, instance, contextChange);
311
404
  };
312
405
  export const disposeViewInstance = async (uid) => {
313
406
  const instance = instances[uid];
@@ -315,6 +408,12 @@ export const disposeViewInstance = async (uid) => {
315
408
  await instance.dispose();
316
409
  }
317
410
  await maybeClearContext(uid, contextViewIds[uid]);
411
+ const viewId = contextViewIds[uid];
412
+ const instanceUids = instanceUidsByView[viewId];
413
+ instanceUids?.delete(uid);
414
+ if (instanceUids?.size === 0) {
415
+ delete instanceUidsByView[viewId];
416
+ }
318
417
  delete instances[uid];
319
418
  delete renderedDoms[uid];
320
419
  delete contextViewIds[uid];
@@ -348,7 +447,11 @@ export const getViewRegistrySnapshot = () => {
348
447
  };
349
448
  export const resetViewRegistry = () => {
350
449
  for (const id of Object.keys(views)) {
450
+ for (const disposable of viewCommandDisposables[id] || []) {
451
+ disposable.dispose();
452
+ }
351
453
  delete views[id];
454
+ delete viewCommandDisposables[id];
352
455
  }
353
456
  for (const uid of Object.keys(instances)) {
354
457
  delete instances[Number(uid)];
@@ -362,4 +465,7 @@ export const resetViewRegistry = () => {
362
465
  for (const uid of Object.keys(contextViewIds)) {
363
466
  delete contextViewIds[Number(uid)];
364
467
  }
468
+ for (const viewId of Object.keys(instanceUidsByView)) {
469
+ delete instanceUidsByView[viewId];
470
+ }
365
471
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/api",
3
- "version": "8.40.0",
3
+ "version": "8.42.0",
4
4
  "description": "Tree-shakeable extension API for Lvce Editor extensions.",
5
5
  "keywords": [
6
6
  "lvce-editor",