diff --git a/package-lock.json b/package-lock.json index d1aa27f5ab..5a2dca5bfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2458,6 +2458,19 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sveltestack/svelte-query": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sveltestack/svelte-query/-/svelte-query-1.6.0.tgz", + "integrity": "sha512-C0wWuh6av1zu3Pzwrg6EQmX3BhDZQ4gMAdYu6Tfv4bjbEZTB00uEDz52z92IZdONh+iUKuyo0xRZ2e16k2Xifg==", + "peerDependencies": { + "broadcast-channel": "^4.5.0" + }, + "peerDependenciesMeta": { + "broadcast-channel": { + "optional": true + } + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "dev": true, @@ -18229,6 +18242,7 @@ "packages/webview-ui": { "version": "0.0.1", "dependencies": { + "@sveltestack/svelte-query": "^1.6.0", "@vscode/webview-ui-toolkit": "^1.2.2", "fast-equals": "^5.0.1", "sirv-cli": "^2.0.2", diff --git a/packages/client/src/webview/AppState/Subscribables/internal/AbstractSubscribable.ts b/packages/client/src/webview/AppState/Subscribables/internal/AbstractSubscribable.ts index 091d6d0c29..fa37a571e3 100644 --- a/packages/client/src/webview/AppState/Subscribables/internal/AbstractSubscribable.ts +++ b/packages/client/src/webview/AppState/Subscribables/internal/AbstractSubscribable.ts @@ -11,7 +11,7 @@ export abstract class AbstractSubscribable extends InheritableDisposable impl protected _isNotifyBusy = false; constructor() { - super([() => this.done(), () => this._eventListeners.clear()]); + super([() => this.done(), () => this._eventListeners.clear()], 'AbstractSubscribable'); } protected _hasSubscribers() { @@ -42,7 +42,7 @@ export abstract class AbstractSubscribable extends InheritableDisposable impl private _unSub(s: SubscriberLike) { this._subscriptions.delete(s); - this._markAsDone(s); + // this._markAsDone(s); this._tryToStop(); } @@ -71,7 +71,7 @@ export abstract class AbstractSubscribable extends InheritableDisposable impl public subscribe(s: SubscriberLike): Disposable { this._subscriptions.add(s); this._start(); - return createDisposable(() => this._unSub(s)); + return createDisposable(() => this._unSub(s), undefined, 'subscribe'); } protected notify(value: T): void { @@ -97,7 +97,7 @@ export abstract class AbstractSubscribable extends InheritableDisposable impl public onEvent(etOrL: EventType | EventListener, listener?: EventListener): Disposable { if (typeof etOrL === 'function') { this._eventListeners.add(etOrL); - return createDisposable(() => this._eventListeners.delete(etOrL)); + return createDisposable(() => this._eventListeners.delete(etOrL), undefined, 'onEvent'); } const eventType = etOrL; @@ -106,6 +106,6 @@ export abstract class AbstractSubscribable extends InheritableDisposable impl listener?.(e); }; this._eventListeners.add(eventlistener); - return createDisposable(() => this._eventListeners.delete(eventlistener)); + return createDisposable(() => this._eventListeners.delete(eventlistener), undefined, `onEvent ${eventType}`); } } diff --git a/packages/client/src/webview/AppState/index.ts b/packages/client/src/webview/AppState/index.ts index 5cde7c8f81..ed8c0332b9 100644 --- a/packages/client/src/webview/AppState/index.ts +++ b/packages/client/src/webview/AppState/index.ts @@ -1,2 +1,2 @@ -export { getWebviewGlobalStore } from './store'; +export { calcDocSettings, getWebviewGlobalStore } from './store'; export { awaitSubscribable } from './Subscribables/helpers/awaitSubscribable'; diff --git a/packages/client/src/webview/AppState/store.ts b/packages/client/src/webview/AppState/store.ts index d25e7141c6..3d3441d9de 100644 --- a/packages/client/src/webview/AppState/store.ts +++ b/packages/client/src/webview/AppState/store.ts @@ -1,5 +1,5 @@ -import type { DisposableClassic, DisposableHybrid } from 'utils-disposables'; -import { createDisposableFromList, disposeOf } from 'utils-disposables'; +import type { DisposableClassic, DisposableHybrid, DisposableLike } from 'utils-disposables'; +import { createDisposable, createDisposableFromList, disposeOf, injectDisposable } from 'utils-disposables'; import type { TextDocument, TextEditor, Uri } from 'vscode'; import { window } from 'vscode'; import { getLogLevel, LogLevel, setLogLevel } from 'vscode-webview-rpc/logger'; @@ -8,15 +8,15 @@ import type { WatchFieldList, WatchFields } from 'webview-api'; import { getDependencies } from '../../di'; import { calcSettings } from '../../infoViewer/infoHelper'; import type { AppStateData } from '../apiTypes'; -import { awaitPromise, createSubscribableView, delayUnsubscribe, map, pipe, rx, throttle } from './Subscribables'; +import { createSubscribableView, pipe, rx, throttle } from './Subscribables'; import { toSubscriberFn } from './Subscribables/helpers/toSubscriber'; import type { MakeSubscribable, StoreValue } from './Subscribables/StoreValue'; import { createStoreValue } from './Subscribables/StoreValue'; -import type { Subscribable, SubscriberLike } from './Subscribables/Subscribables'; +import type { SubscriberLike } from './Subscribables/Subscribables'; export interface Storage { seq: number; - state: MakeSubscribable; + state: MakeSubscribable; dispose(): void; } @@ -24,52 +24,61 @@ const debug = false; debug && setLogLevel(LogLevel.debug); -const writableState = { - logLevel: createStoreValue(getLogLevel()), - todos: createStoreValue([]), -} as const; - let store: Storage | undefined = undefined; export function getWebviewGlobalStore(): Storage { if (store) return store; - const currentDocument = rx(subscribeToCurrentDocument, createSubscribableView, delayUnsubscribe(5000), throttle(500)); + const currentDocumentSub = rx(subscribeToCurrentDocument); + const currentDocument = pipe(currentDocumentSub, throttle(500), /* delayUnsubscribe(5000), */ createSubscribableView); currentDocument.onEvent('onNotify', (event) => console.log('current document update: %o', event)); function dispose() { + disposeOf(currentDocumentSub); const _store = store; store = undefined; if (!_store) return; Object.values(_store.state).forEach((s) => disposeOf(s)); } - const _store: Storage = { - seq: 1, - state: { - ...writableState, - currentDocument, - docSettings: rx(currentDocument, subscribeToDocSettings, createSubscribableView, delayUnsubscribe(5000), throttle(500)), + const writableState = { + logLevel: createStoreValue(getLogLevel()), + todos: createStoreValue([]), + } as const; + + const _store: Storage = injectDisposable( + { + seq: 1, + state: { + ...writableState, + currentDocument, + }, }, dispose, - }; + 'getWebviewGlobalStore', + ); return (store = _store); } function subscribeToCurrentDocument(subscriber: SubscriberLike): DisposableHybrid { const emitter = toSubscriberFn(subscriber); - const disposables: DisposableClassic[] = []; + const disposables: DisposableLike[] = [createDisposable(() => console.error('Dispose Last'), undefined, 'Dispose Last')]; const disposable = createDisposableFromList(disposables); setCurrentDocument(window.activeTextEditor); - window.onDidChangeActiveTextEditor(setCurrentDocument, undefined, disposables); - window.onDidChangeTextEditorSelection( - (event) => event.textEditor === window.activeTextEditor && setCurrentDocument(event.textEditor), - undefined, - disposables, + disposables.push(disposeClassic(window.onDidChangeActiveTextEditor(setCurrentDocument, undefined))); + disposables.push( + disposeClassic( + window.onDidChangeTextEditorSelection( + (event) => event.textEditor === window.activeTextEditor && setCurrentDocument(event.textEditor), + undefined, + ), + ), ); + disposables.push(createDisposable(() => console.error('Dispose First'), undefined, 'Dispose First')); + return disposable; function setCurrentDocument(textEditor: TextEditor | undefined) { @@ -88,19 +97,10 @@ function subscribeToCurrentDocument(subscriber: SubscriberLike): Subscribable { - async function calcDocSettings(doc: AppStateData['currentDocument']): Promise { - const textDoc = (doc && findMatchTextDocument(doc?.url)) || undefined; - const di = getDependencies(); - return calcSettings(textDoc, undefined, di.client, console.log); - } - - return pipe( - src, - throttle(1000), - map(calcDocSettings), - awaitPromise((err, emitter) => (console.error(err), emitter(null))), - ); +export async function calcDocSettings(doc?: string) { + const textDoc = (doc && findMatchTextDocument(doc)) || undefined; + const di = getDependencies(); + return calcSettings(textDoc, undefined, di.client, console.log); } export interface StateUpdate { @@ -154,3 +154,7 @@ function normalizeUrlToString(url: UrlLike): string { const decoded = decodeURIComponent(decodeURIComponent(url.toString())).normalize('NFC'); return decoded.replace(/^file:\/\/\/[a-z]:/i, (fileUrl) => fileUrl.toLowerCase()); } + +function disposeClassic(disposable: DisposableClassic): DisposableHybrid { + return createDisposable(() => disposable.dispose(), undefined, 'disposeClassic'); +} diff --git a/packages/client/src/webview/api/api.ts b/packages/client/src/webview/api/api.ts index 443fa5bec2..c8b020b521 100644 --- a/packages/client/src/webview/api/api.ts +++ b/packages/client/src/webview/api/api.ts @@ -1,4 +1,4 @@ -import { createDisposeMethodFromList, type DisposableLike, disposeOf, injectDisposable } from 'utils-disposables'; +import { createDisposableList, type DisposableLike, disposeOf, injectDisposable, makeDisposable } from 'utils-disposables'; import { window } from 'vscode'; import { type MessageConnection } from 'vscode-jsonrpc/node'; import type { RequestResult, SetValueRequest, SetValueResult, WatchFieldList, WatchFields } from 'webview-api'; @@ -6,7 +6,7 @@ import { createServerSideSpellInfoWebviewApi } from 'webview-api'; import type { ServerSideApi, ServerSideApiDef } from '../apiTypes'; import { awaitSubscribable, getWebviewGlobalStore } from '../AppState'; -import { type Storage, updateState, watchFieldList } from '../AppState/store'; +import { calcDocSettings, type Storage, updateState, watchFieldList } from '../AppState/store'; import type { StoreValue } from '../AppState/Subscribables/StoreValue'; import type { Subscribable } from '../AppState/Subscribables/Subscribables'; import { sampleList } from './staticTestData'; @@ -18,8 +18,8 @@ export function createApi(connection: MessageConnection) { export function bindApiAndStore(connection: MessageConnection, store: Storage): ServerSideApi { let watcher: DisposableLike | undefined = undefined; const fieldsToWatch = new Set(); - const disposables: DisposableLike[] = [() => disposeOf(watcher)]; - const dispose = createDisposeMethodFromList(disposables); + const disposables = createDisposableList([() => disposeOf(watcher)], 'bindApiAndStore'); + const dispose = disposables.dispose; const api: ServerSideApiDef = { serverRequests: { @@ -27,7 +27,7 @@ export function bindApiAndStore(connection: MessageConnection, store: Storage): getLogLevel: () => resolveRequest(store.state.logLevel), getTodos: () => resolveRequest(store.state.todos), getCurrentDocument: () => resolveRequest(store.state.currentDocument), - getDocSettings: () => resolveRequest(store.state.docSettings), + getDocSettings: calcDocSettings, setLogLevel: (r) => updateStateRequest(r, store.state.logLevel), setTodos: (r) => updateStateRequest(r, store.state.todos), watchFields, @@ -43,9 +43,9 @@ export function bindApiAndStore(connection: MessageConnection, store: Storage): }; const serverSideApi = createServerSideSpellInfoWebviewApi(connection, api); - disposables.push(serverSideApi); + disposables.push(makeDisposable(serverSideApi)); - return injectDisposable({ ...serverSideApi }, dispose); + return injectDisposable({ ...serverSideApi }, dispose, 'bindApiAndStore'); /** Add fields to be watched. */ function watchFields(req: WatchFieldList) { diff --git a/packages/client/src/webview/index.ts b/packages/client/src/webview/index.ts index 09f5016314..6518864996 100644 --- a/packages/client/src/webview/index.ts +++ b/packages/client/src/webview/index.ts @@ -1,42 +1 @@ -import type { ExtensionContext } from 'vscode'; -import { commands, window } from 'vscode'; -import { supportedViewsByName } from 'webview-api'; - -import { getWebviewGlobalStore } from './AppState/store'; -import { HelloWorldPanel } from './panels/HelloWorldPanel'; -import { TodoViewProvider } from './providers/TodoViewProvider'; -import { WebviewApiViewProvider } from './providers/viewProviders'; - -export const registeredCommands = ['cspell-info.showHelloWorld'] as const; - -type CommandNames = (typeof registeredCommands)[number]; - -type RegisteredCommandNames = { - [P in CommandNames]: P; -}; - -const rCommands = Object.fromEntries(registeredCommands.map((name) => [name, name] as const)) as RegisteredCommandNames; - -export function activate(context: ExtensionContext) { - const { subscriptions, extensionUri } = context; - - const views = [ - new TodoViewProvider(extensionUri), - new WebviewApiViewProvider(extensionUri, supportedViewsByName['cspell-info'], 'cspell-info.infoView'), - ]; - - for (const view of views) { - subscriptions.push(window.registerWebviewViewProvider(view.viewType, view)); - } - - // Create the show hello world command - const showHelloWorldCommand = commands.registerCommand(rCommands['cspell-info.showHelloWorld'], () => { - HelloWorldPanel.render(context.extensionUri); - }); - - // Add command to the extension context - subscriptions.push(showHelloWorldCommand, { dispose: () => HelloWorldPanel.currentPanel?.dispose() }); - - // Add state clean up. - subscriptions.push(getWebviewGlobalStore()); -} +export { activate, registeredCommands } from './webview'; diff --git a/packages/client/src/webview/panels/HelloWorldPanel.ts b/packages/client/src/webview/panels/HelloWorldPanel.ts index 616e62792b..20da477977 100644 --- a/packages/client/src/webview/panels/HelloWorldPanel.ts +++ b/packages/client/src/webview/panels/HelloWorldPanel.ts @@ -1,4 +1,5 @@ -import type { Disposable, Uri, WebviewPanel } from 'vscode'; +import { createDisposableList } from 'utils-disposables'; +import type { Uri, WebviewPanel } from 'vscode'; import { ViewColumn, window } from 'vscode'; import { HelloWorldView } from '../views/HelloWorldView'; @@ -16,7 +17,7 @@ import { HelloWorldView } from '../views/HelloWorldView'; export class HelloWorldPanel { public static currentPanel: HelloWorldPanel | undefined; private readonly _panel: WebviewPanel; - private _disposables: Disposable[] = []; + private _disposables = createDisposableList(undefined, 'HelloWorldPanel'); /** * The HelloWorldPanel class private constructor (called only from the render method). @@ -30,8 +31,7 @@ export class HelloWorldPanel { // Set an event listener to listen for when the panel is disposed (i.e. when the user closes // the panel or when the panel is closed programmatically) - this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - + this._disposables.push(this._panel.onDidDispose(() => this.dispose())); this._disposables.push(HelloWorldView.bindView(this._panel.webview, extensionUri)); } @@ -75,11 +75,6 @@ export class HelloWorldPanel { // this._panel.dispose() is the first element on the list.; // Dispose of all disposables (i.e. commands) for the current webview panel - while (this._disposables.length) { - const disposable = this._disposables.pop(); - if (disposable) { - disposable.dispose(); - } - } + this._disposables.dispose(); } } diff --git a/packages/client/src/webview/views/AppView.ts b/packages/client/src/webview/views/AppView.ts index 8bacf8b487..063bbcd937 100644 --- a/packages/client/src/webview/views/AppView.ts +++ b/packages/client/src/webview/views/AppView.ts @@ -1,4 +1,5 @@ -import type { Disposable, Webview, WebviewOptions } from 'vscode'; +import { createDisposableList, makeDisposable } from 'utils-disposables'; +import type { Webview, WebviewOptions } from 'vscode'; import { Uri } from 'vscode'; import { createConnectionToWebview } from 'vscode-webview-rpc/extension'; import type { SupportedViews } from 'webview-api'; @@ -8,17 +9,18 @@ import { getNonce } from '../utilities/getNonce'; import { getUri } from '../utilities/getUri'; /** - * This class manages the state and behavior of HelloWorld webview panels. + * This class manages the state and behavior of the webview panels. * * It contains all the data and methods for: * - * - Creating and rendering HelloWorld webview panels + * - Creating and rendering webview panels * - Properly cleaning up and disposing of webview resources when the panel is closed * - Setting the HTML (and by proxy CSS/JavaScript) content of the webview panel * - Setting message listeners so data can be passed between the webview and extension */ export class AppView { - private _disposables: Disposable[] = []; + private _disposables = createDisposableList(undefined, 'AppView'); + public readonly dispose = this._disposables.dispose; /** * The HelloWorldPanel class private constructor (called only from the render method). @@ -38,9 +40,9 @@ export class AppView { // Set an event listener to listen for messages passed from the webview context const rpc = createConnectionToWebview(webview); - this._disposables.push(createApi(rpc)); + this._disposables.push(makeDisposable(createApi(rpc), 'createApi')); rpc.listen(); - this._disposables.push(rpc); + this._disposables.push(makeDisposable(rpc, 'rpc')); } public calcOptions(): WebviewOptions { @@ -53,22 +55,6 @@ export class AppView { }; } - /** - * Cleans up and disposes of webview resources when the webview panel is closed. - */ - public dispose() { - // Dispose of the current webview panel - // this._panel.dispose() is the first element on the list.; - - // Dispose of all disposables (i.e. commands) for the current webview panel - while (this._disposables.length) { - const disposable = this._disposables.pop(); - if (disposable) { - disposable.dispose(); - } - } - } - /** * Defines and returns the HTML that should be rendered within the webview panel. * diff --git a/packages/client/src/webview/webview.ts b/packages/client/src/webview/webview.ts new file mode 100644 index 0000000000..c18ff4fa69 --- /dev/null +++ b/packages/client/src/webview/webview.ts @@ -0,0 +1,57 @@ +import type { ExtensionContext } from 'vscode'; +import { commands, window } from 'vscode'; +import { supportedViewsByName } from 'webview-api'; + +import { getWebviewGlobalStore } from './AppState'; +// import { getWebviewGlobalStore } from './AppState/store'; +import { HelloWorldPanel } from './panels/HelloWorldPanel'; +import { TodoViewProvider } from './providers/TodoViewProvider'; +import { WebviewApiViewProvider } from './providers/viewProviders'; + +export const registeredCommands = ['cspell-info.showHelloWorld'] as const; + +type CommandNames = (typeof registeredCommands)[number]; + +type RegisteredCommandNames = { + [P in CommandNames]: P; +}; + +const rCommands = Object.fromEntries(registeredCommands.map((name) => [name, name] as const)) as RegisteredCommandNames; + +export function activate(context: ExtensionContext) { + const { subscriptions, extensionUri } = context; + + // subscriptions.push(debugDispose('Dispose Activate 0')); + + const views = [ + new TodoViewProvider(extensionUri), + new WebviewApiViewProvider(extensionUri, supportedViewsByName['cspell-info'], 'cspell-info.infoView'), + ]; + + for (const view of views) { + subscriptions.push(window.registerWebviewViewProvider(view.viewType, view)); + } + + // subscriptions.push(debugDispose('Dispose Activate 1')); + + // Create the show hello world command + const showHelloWorldCommand = commands.registerCommand(rCommands['cspell-info.showHelloWorld'], () => { + HelloWorldPanel.render(context.extensionUri); + }); + + // subscriptions.push(debugDispose('Dispose Activate 2')); + + // Add command to the extension context + subscriptions.push(showHelloWorldCommand, { dispose: () => HelloWorldPanel.currentPanel?.dispose() }); + + // subscriptions.push(debugDispose('Dispose Activate 3')); + + // Add state clean up. + subscriptions.push(getWebviewGlobalStore()); + + // subscriptions.push(debugDispose('Dispose Activate 4')); +} + +// function debugDispose(name: string) { +// return createDisposable(() => console.error(name), undefined, name); +// } diff --git a/packages/webview-api/src/api.ts b/packages/webview-api/src/api.ts index d95ab7961a..6a0e831202 100644 --- a/packages/webview-api/src/api.ts +++ b/packages/webview-api/src/api.ts @@ -29,7 +29,7 @@ export interface ServerRequestsAPI { getLogLevel(): RequestResult; getTodos(): RequestResult; getCurrentDocument(): RequestResult; - getDocSettings(): RequestResult; + getDocSettings(docUrl?: string): Settings | null; resetTodos(): SetValueResult; setLogLevel(req: SetValueRequest): SetValueResult; setTodos(req: SetValueRequest): SetValueResult; diff --git a/packages/webview-api/src/apiModels.ts b/packages/webview-api/src/apiModels.ts index f9cbaa6cf2..ea6a1e66de 100644 --- a/packages/webview-api/src/apiModels.ts +++ b/packages/webview-api/src/apiModels.ts @@ -22,7 +22,6 @@ export interface AppStateData { todos: TodoList; logLevel: LogLevel; readonly currentDocument: TextDocumentRef | null; - readonly docSettings: Settings | null; } export type WatchFields = keyof AppStateData; diff --git a/packages/webview-ui/package.json b/packages/webview-ui/package.json index 226e6794f3..12766a1dc4 100644 --- a/packages/webview-ui/package.json +++ b/packages/webview-ui/package.json @@ -13,6 +13,7 @@ }, "type": "module", "dependencies": { + "@sveltestack/svelte-query": "^1.6.0", "@vscode/webview-ui-toolkit": "^1.2.2", "fast-equals": "^5.0.1", "sirv-cli": "^2.0.2", diff --git a/packages/webview-ui/src/App.svelte b/packages/webview-ui/src/App.svelte index 81aedc4895..9be3db8fd1 100644 --- a/packages/webview-ui/src/App.svelte +++ b/packages/webview-ui/src/App.svelte @@ -8,6 +8,7 @@ import { createDisposableFromList } from 'utils-disposables'; import { appState } from './state/appState'; import { LogLevel, setLogLevel } from 'webview-api'; + import { QueryClient, QueryClientProvider } from '@sveltestack/svelte-query'; // In order to use the Webview UI Toolkit web components they // must be registered with the browser (i.e. webview) using the @@ -37,24 +38,28 @@ const disposables = [logLevel.subscribe((level) => setLogLevel(level))]; const disposable = createDisposableFromList(disposables); + const queryClient = new QueryClient(); + onDestroy(() => { disposable.dispose(); }); -
-
- {#if view == supportedViewsByName['hello-world']} - - {:else if view == supportedViewsByName.todo} - - {:else if view == supportedViewsByName['cspell-info']} - - {:else} -

Unknown View {view}

- {/if} -
-
+ +
+
+ {#if view == supportedViewsByName['hello-world']} + + {:else if view == supportedViewsByName.todo} + + {:else if view == supportedViewsByName['cspell-info']} + + {:else} +

Unknown View {view}

+ {/if} +
+
+