diff --git a/.vscode/launch.json b/.vscode/launch.json index b916dad9b2..9f537e9bea 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,6 +20,24 @@ "/**" ] }, + { + "name": "Client: Launch Extension -- All extensions", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}" + ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/packages/client/dist/**/*.js" + ], + "smartStep": true, + "skipFiles": [ + "/**" + ] + }, { "name": "Client: Launch Extension - .gitignore", "type": "extensionHost", diff --git a/cspell-words.txt b/cspell-words.txt index ac3afdbbd6..0ffa81623c 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -13,6 +13,7 @@ codeblock codicon codicons colorscheme +comms cosmiconfig darkblue dcopy diff --git a/package-lock.json b/package-lock.json index a2f2cff6d0..5160d5eadf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17620,14 +17620,15 @@ "cspell-glob": "^7.3.7", "cspell-lib": "^7.3.7", "gensequence": "^6.0.0", + "json-rpc-api": "file:../json-rpc-api", "node-watch": "^0.7.4", "rxjs": "^7.8.1", "utils-disposables": "file:../utils-disposables", + "utils-logger": "file:../utils-logger", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", "vscode-languageserver-types": "^3.17.5", - "vscode-uri": "^3.0.8", - "vscode-webview-rpc": "file:../webview-rpc" + "vscode-uri": "^3.0.8" }, "bin": { "build": "build.mjs" @@ -17816,8 +17817,7 @@ "dependencies": { "json-rpc-api": "file:../json-rpc-api", "utils-disposables": "file:../utils-disposables", - "utils-logger": "file:../utils-logger", - "vscode-webview-rpc": "file:../webview-rpc" + "utils-logger": "file:../utils-logger" }, "devDependencies": { "vitest": "^0.34.6" diff --git a/packages/__utils/package.json b/packages/__utils/package.json index 34f950a957..b77e17e108 100644 --- a/packages/__utils/package.json +++ b/packages/__utils/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "Shared Utils between server and client", "private": true, + "type": "commonjs", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { diff --git a/packages/_server/package.json b/packages/_server/package.json index 91d8151b1d..7de00c9a11 100644 --- a/packages/_server/package.json +++ b/packages/_server/package.json @@ -14,25 +14,15 @@ }, "type": "module", "main": "dist/main.cjs", - "typings": "dist/api.d.cts", "exports": { - ".": "./dist/main.cjs", - "./api": "./dist/api.cjs" - }, - "typesVersions": { - "*": { - "*.d.cts": [ - "dist/*.d.cts" - ], - "api": [ - "dist/api.d.cts" - ], - "dist/api.d.cts": [ - "dist/api.d.cts" - ], - "*": [ - "dist/*.d.cts" - ] + ".": { + "import": "./dist/main.cjs", + "require": "./dist/main.cjs" + }, + "./api": { + "types": "./dist/api.d.cts", + "import": "./dist/api.cjs", + "require": "./dist/api.cjs" } }, "devDependencies": { @@ -52,14 +42,15 @@ "cspell-glob": "^7.3.7", "cspell-lib": "^7.3.7", "gensequence": "^6.0.0", + "json-rpc-api": "file:../json-rpc-api", "node-watch": "^0.7.4", "rxjs": "^7.8.1", "utils-disposables": "file:../utils-disposables", + "utils-logger": "file:../utils-logger", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", "vscode-languageserver-types": "^3.17.5", - "vscode-uri": "^3.0.8", - "vscode-webview-rpc": "file:../webview-rpc" + "vscode-uri": "^3.0.8" }, "scripts": { "build": "npm run build:esbuild && npm run build:api && npm run build:tsc && npm run build-schema", diff --git a/packages/_server/src/api/CommandsToClient.ts b/packages/_server/src/api/CommandsToClient.ts new file mode 100644 index 0000000000..1c267204ca --- /dev/null +++ b/packages/_server/src/api/CommandsToClient.ts @@ -0,0 +1,11 @@ +import type { ConfigurationTarget } from './apiModels.js'; +import type { OrPromise } from './types.js'; + +export interface CommandsToClient { + addWordsToVSCodeSettingsFromServer: (words: string[], documentUri: string, target: ConfigurationTarget) => void; + addWordsToDictionaryFileFromServer: (words: string[], documentUri: string, dict: { uri: string; name: string }) => void; + addWordsToConfigFileFromServer: (words: string[], documentUri: string, config: { uri: string; name: string }) => void; +} +export type ClientSideCommandHandlerApi = { + [command in keyof CommandsToClient as `cSpell.${command}`]: (...params: Parameters) => OrPromise; +}; diff --git a/packages/_server/src/api/api.ts b/packages/_server/src/api/api.ts new file mode 100644 index 0000000000..594fcf88fc --- /dev/null +++ b/packages/_server/src/api/api.ts @@ -0,0 +1,111 @@ +import type { + ApiPrefix, + ApplyNotificationAPI, + ApplyRequestAPI, + ClientAPIDef, + ClientSideMethods, + Logger, + MessageConnection, + RpcAPI, + ServerAPIDef, + ServerSideMethods, +} from 'json-rpc-api'; +import { createClientApi, createServerApi } from 'json-rpc-api'; + +import type { + GetConfigurationForDocumentRequest, + GetConfigurationForDocumentResult, + IsSpellCheckEnabledResult, + OnSpellCheckDocumentStep, + SpellingSuggestionsResult, + SplitTextIntoWordsResult, + TextDocumentInfo, + WorkspaceConfigForDocumentRequest, + WorkspaceConfigForDocumentResponse, +} from './apiModels.js'; + +export type { Logger, MessageConnection } from 'json-rpc-api'; + +/** Requests that can be made to the server */ +export interface ServerRequestsAPI { + getConfigurationForDocument(req: GetConfigurationForDocumentRequest): GetConfigurationForDocumentResult; + isSpellCheckEnabled(req: TextDocumentInfo): IsSpellCheckEnabledResult; + splitTextIntoWords(req: string): SplitTextIntoWordsResult; + spellingSuggestions(req: TextDocumentInfo): SpellingSuggestionsResult; +} + +/** Notifications that can be sent to the server */ +export interface ServerNotificationsAPI { + notifyConfigChange: () => void; + registerConfigurationFile: (path: string) => void; +} + +/** + * Requests that can be made from the server to the client(vscode extension) + * Note: RPC requests to the client/extension is rare. + */ +export interface ClientRequestsAPI { + // addWordsToVSCodeSettingsFromServer: (words: string[], documentUri: string, target: ConfigurationTarget) => void; + // addWordsToDictionaryFileFromServer: (words: string[], documentUri: string, dict: { uri: string; name: string }) => void; + // addWordsToConfigFileFromServer: (words: string[], documentUri: string, config: { uri: string; name: string }) => void; + onWorkspaceConfigForDocumentRequest: (req: WorkspaceConfigForDocumentRequest) => WorkspaceConfigForDocumentResponse; +} + +/** Notifications from the server to the client(vscode extension) */ +export interface ClientNotificationsAPI { + onSpellCheckDocument(step: OnSpellCheckDocumentStep): void; +} + +export interface SpellCheckerServerAPI extends RpcAPI { + serverRequests: ApplyRequestAPI; + serverNotifications: ApplyNotificationAPI; + clientRequests: ApplyRequestAPI; + clientNotifications: ApplyNotificationAPI; +} + +/** + * Used on the server side to communicate with the client(extension). + */ +export interface ServerSideApi extends ServerSideMethods {} +/** + * Used in the client(extension) to communicate with the server. + */ +export interface ClientSideApi extends ClientSideMethods {} + +export type ServerSideApiDef = ServerAPIDef; +export type ClientSideApiDef = ClientAPIDef; + +export interface ServerSideHandlers { + serverRequests: DefineHandlers; + serverNotifications: DefineHandlers; +} + +// todo: make '' when all old apis are removed. +const pfx = '_'; + +const apiPrefix: ApiPrefix = { + serverNotifications: pfx, + serverRequests: pfx, + clientNotifications: pfx, + clientRequests: pfx, +}; + +export function createServerSideApi( + connection: MessageConnection, + api: ServerAPIDef, + logger: Logger | undefined, +): ServerSideApi { + return createServerApi(connection, api, logger, apiPrefix); +} + +export function createClientSideApi( + connection: MessageConnection, + api: ClientAPIDef, + logger: Logger | undefined, +): ClientSideApi { + return createClientApi(connection, api, logger, apiPrefix); +} + +type DefineHandlers = { + [P in keyof T]: Exclude; +}; diff --git a/packages/_server/src/api/api.old.ts b/packages/_server/src/api/apiModels.ts similarity index 55% rename from packages/_server/src/api/api.old.ts rename to packages/_server/src/api/apiModels.ts index 0aadd8c270..c8d777f940 100644 --- a/packages/_server/src/api/api.old.ts +++ b/packages/_server/src/api/apiModels.ts @@ -9,116 +9,20 @@ export type { ConfigTargetDictionary, ConfigTargetVSCode, } from '../config/configTargets.mjs'; -export type { - CSpellUserSettings, - CustomDictionaries, - CustomDictionary, - CustomDictionaryEntry, - CustomDictionaryScope, - CustomDictionaryWithScope, - DictionaryDefinition, - DictionaryDefinitionCustom, - DictionaryFileTypes, - LanguageSetting, - SpellCheckerSettings, - SpellCheckerSettingsProperties, -} from '../config/cspellConfig/index.mjs'; - -export type ExtensionId = 'cSpell'; - -export type DiagnosticSource = ExtensionId; - -export type VSCodeSettingsCspell = { - [key in ExtensionId]?: config.CSpellUserSettings; -}; - -/** - * Method signatures for requests to the Server. - */ -export type ServerRequestApi = { - [key in keyof ServerMethods]: ApiReqResFn; -}; - -/** - * Internal Server Handler signatures to the Server API - */ -export type ServerRequestApiHandlers = ApiHandlers; - -/** - * Server RPC Request and Result types - */ -export type ServerMethods = { - getConfigurationForDocument: ReqRes; - isSpellCheckEnabled: ReqRes; - splitTextIntoWords: ReqRes; - spellingSuggestions: ReqRes; -}; - -/** - * One way RPC calls to the server - */ -export type ServerNotifyApi = { - notifyConfigChange: () => void; - registerConfigurationFile: (path: string) => void; -}; - -/** - * Notification that can be sent to the client - */ -export type ClientNotifications = { - onSpellCheckDocument: OnSpellCheckDocumentStep; -}; - -/** - * Client side API for listening to notifications from the server - */ -export type ClientNotificationsApi = { - [method in keyof ClientNotifications]: (p: ClientNotifications[method]) => void; -}; - -/** - * Internal - API for sending notifications to the client - */ -export type SendClientNotificationsApi = { - [method in keyof ClientNotifications as `send${Capitalize}`]: (p: ClientNotifications[method]) => void; -}; -/** - * Requests that can be made of the client - */ -export type RequestsToClient = { - onWorkspaceConfigForDocumentRequest: ReqRes; -}; - -/** - * Internal - API for sending requests to the client - */ -export type SendRequestsToClientApi = { - [method in keyof RequestsToClient as `send${Capitalize}`]: ApiReqResFn; -}; - -export type ClientSideCommandHandlerApi = { - [command in keyof CommandsToClient as `cSpell.${command}`]: (...params: Parameters) => OrPromise; -}; -export interface CommandsToClient { - addWordsToVSCodeSettingsFromServer: (words: string[], documentUri: string, target: ConfigurationTarget) => void; - addWordsToDictionaryFileFromServer: (words: string[], documentUri: string, dict: { uri: string; name: string }) => void; - addWordsToConfigFileFromServer: (words: string[], documentUri: string, config: { uri: string; name: string }) => void; +export interface BlockedFileReason { + code: string; + message: string; + documentationRefUri?: UriString; } -export type RequestsToClientApiHandlers = ApiHandlers; +export type UriString = string; +export type DocumentUri = UriString; -export interface GetConfigurationForDocumentRequest extends TextDocumentInfo { - /** used to calculate configTargets, configTargets will be empty if undefined. */ - workspaceConfig?: WorkspaceConfigForDocument; -} +export type StartIndex = number; +export type EndIndex = number; -export interface GetConfigurationForDocumentResult extends IsSpellCheckEnabledResult { - settings: config.CSpellUserSettings | undefined; - docSettings: config.CSpellUserSettings | undefined; - configFiles: UriString[]; - configTargets: ConfigTarget[]; -} +export type RangeTuple = [StartIndex, EndIndex]; export interface ExcludeRef { glob: string; @@ -158,11 +62,17 @@ export interface TextDocumentInfo { text?: string; } -export type ServerRequestMethods = keyof ServerMethods; +export interface GetConfigurationForDocumentRequest extends TextDocumentInfo { + /** used to calculate configTargets, configTargets will be empty if undefined. */ + workspaceConfig?: WorkspaceConfigForDocument; +} -export type ServerRequestMethodConstants = { - [key in ServerRequestMethods]: key; -}; +export interface GetConfigurationForDocumentResult extends IsSpellCheckEnabledResult { + settings: config.CSpellUserSettings | undefined; + docSettings: config.CSpellUserSettings | undefined; + configFiles: UriString[]; + configTargets: ConfigTarget[]; +} export interface TextDocumentRef { uri: UriString; @@ -177,11 +87,6 @@ export interface MatchPatternsToDocumentRequest extends TextDocumentRef { patterns: (string | NamedPattern)[]; } -export type StartIndex = number; -export type EndIndex = number; - -export type RangeTuple = [StartIndex, EndIndex]; - export interface RegExpMatch { regexp: string; matches: RangeTuple[]; @@ -265,44 +170,25 @@ export type FieldExistsInTarget = { export type ConfigurationTarget = ConfigScopeVScode; -export type UriString = string; -export type DocumentUri = UriString; - -export type Req = T extends { request: infer R } ? R : never; -export type Res = T extends { response: infer R } ? R : never; -export type Fn = T extends { fn: infer R } ? R : never; -export type OrPromise = Promise | T; - -export type ReqRes = { - request: Req; - response: Res; -}; - -/** - * Utility type to combine the Request and Response to create the Handler function - */ -export type RequestResponseFn = { - request: Req; - response: Res; - fn: ApiReqHandler; -}; - -export type ApiReqResFn = ApiFn, Res>; -export type ApiFn = (req: Req) => Promise; +export type { + CSpellUserSettings, + CustomDictionaries, + CustomDictionary, + CustomDictionaryEntry, + CustomDictionaryScope, + CustomDictionaryWithScope, + DictionaryDefinition, + DictionaryDefinitionCustom, + DictionaryFileTypes, + LanguageSetting, + SpellCheckerSettings, + SpellCheckerSettingsProperties, +} from '../config/cspellConfig/index.mjs'; -export type ApiReqHandler = ApiHandler, Res>; -export type ApiHandler = (req: Req) => OrPromise; +export type ExtensionId = 'cSpell'; -export type ApiHandlers = { - [M in keyof ApiReqRes]: ApiReqHandler; -}; +export type DiagnosticSource = ExtensionId; -export type ApiReqResMethods = { - [M in keyof ApiReqRes]: ApiReqResFn; +export type VSCodeSettingsCspell = { + [key in ExtensionId]?: config.CSpellUserSettings; }; - -export interface BlockedFileReason { - code: string; - message: string; - documentationRefUri?: UriString; -} diff --git a/packages/_server/src/api/index.ts b/packages/_server/src/api/index.ts index 7754cb5e89..588b2a1b66 100644 --- a/packages/_server/src/api/index.ts +++ b/packages/_server/src/api/index.ts @@ -1 +1,4 @@ -export * from './api.old.js'; +export * from './api.js'; +// export * from './api.old.js'; +export * from './apiModels.js'; +export * from './CommandsToClient.js'; diff --git a/packages/_server/src/api/types.ts b/packages/_server/src/api/types.ts new file mode 100644 index 0000000000..e4baf75009 --- /dev/null +++ b/packages/_server/src/api/types.ts @@ -0,0 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export type Func = (...p: any) => any; +export type AsyncFunc = (...p: any) => Promise; +export type ReturnPromise = T extends Func ? (T extends AsyncFunc ? T : (...p: Parameters) => Promise>) : never; +export type OrPromise = Promise | T; diff --git a/packages/_server/src/clientApi.mts b/packages/_server/src/clientApi.mts deleted file mode 100644 index 87681d50dc..0000000000 --- a/packages/_server/src/clientApi.mts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Connection } from 'vscode-languageserver'; -import { NotificationType, RequestType } from 'vscode-languageserver'; - -import type { - ClientNotifications, - OnSpellCheckDocumentStep, - Req, - RequestsToClient, - Res, - SendClientNotificationsApi, - SendRequestsToClientApi, - WorkspaceConfigForDocumentRequest, -} from './api.js'; - -export interface ClientApi extends SendClientNotificationsApi, SendRequestsToClientApi {} - -export function createClientApi(connection: Connection): ClientApi { - function sendNotification(method: M, param: ClientNotifications[M]) { - const n = new NotificationType(method); - connection.sendNotification(n, param); - } - - function sendRequest(method: M, param: Req) { - const req = new RequestType, Res, undefined>(method); - return connection.sendRequest(req, param); - } - - return { - sendOnSpellCheckDocument: (param: OnSpellCheckDocumentStep) => sendNotification('onSpellCheckDocument', param), - sendOnWorkspaceConfigForDocumentRequest: (param: WorkspaceConfigForDocumentRequest) => - sendRequest('onWorkspaceConfigForDocumentRequest', param), - }; -} diff --git a/packages/_server/src/clientApi.test.mts b/packages/_server/src/clientApi.test.mts deleted file mode 100644 index 7923e9af6e..0000000000 --- a/packages/_server/src/clientApi.test.mts +++ /dev/null @@ -1,54 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; -import type { Connection } from 'vscode-languageserver'; - -import type { OnSpellCheckDocumentStep, WorkspaceConfigForDocumentRequest } from './api.js'; -import { createClientApi } from './clientApi.mjs'; - -const stub: any = { - sendNotification: vi.fn(), - sendRequest: vi.fn(), -}; -const connection = stub as Connection; - -const mockConnection = vi.mocked(connection); - -describe('Validate Client Api', () => { - beforeEach(() => { - mockConnection.sendNotification.mockClear(); - mockConnection.sendRequest.mockClear(); - }); - - test('sendOnSpellCheckDocument', () => { - const api = createClientApi(connection); - const p: OnSpellCheckDocumentStep = { - uri: 'uri', - seq: 1, - step: 'step', - ts: 1, - version: 1, - }; - api.sendOnSpellCheckDocument(p); - expect(mockConnection.sendNotification).toHaveBeenLastCalledWith( - expect.objectContaining({ - method: 'onSpellCheckDocument', - numberOfParams: 1, - }), - p, - ); - }); - - test('sendOnWorkspaceConfigForDocumentRequest', () => { - const api = createClientApi(connection); - const req: WorkspaceConfigForDocumentRequest = { - uri: 'uri', - }; - api.sendOnWorkspaceConfigForDocumentRequest(req); - expect(mockConnection.sendRequest).toHaveBeenLastCalledWith( - expect.objectContaining({ - method: 'onWorkspaceConfigForDocumentRequest', - numberOfParams: 1, - }), - req, - ); - }); -}); diff --git a/packages/_server/src/codeActions.mts b/packages/_server/src/codeActions.mts index 67cc3d45b4..ae8251111c 100644 --- a/packages/_server/src/codeActions.mts +++ b/packages/_server/src/codeActions.mts @@ -7,7 +7,7 @@ import type { TextDocument } from 'vscode-languageserver-textdocument'; import type { Diagnostic } from 'vscode-languageserver-types'; import { CodeAction, CodeActionKind, TextEdit } from 'vscode-languageserver-types'; -import type { ClientApi } from './clientApi.mjs'; +import type { ServerSideApi } from './api.js'; import { clientCommands as cc } from './commands.mjs'; import type { ConfigScope, ConfigTarget, ConfigTargetCSpell, ConfigTargetDictionary, ConfigTargetVSCode } from './config/configTargets.mjs'; import { ConfigKinds, ConfigScopes } from './config/configTargets.mjs'; @@ -42,7 +42,7 @@ export function onCodeActionHandler( documents: TextDocuments, fnSettings: (doc: TextDocument) => Promise, fnSettingsVersion: (doc: TextDocument) => number, - clientApi: ClientApi, + clientApi: ServerSideApi, ): (params: CodeActionParams) => Promise { const codeActionHandler = new CodeActionHandler(documents, fnSettings, fnSettingsVersion, clientApi); @@ -64,7 +64,7 @@ class CodeActionHandler { readonly documents: TextDocuments, readonly fnSettings: (doc: TextDocument) => Promise, readonly fnSettingsVersion: (doc: TextDocument) => number, - readonly clientApi: ClientApi, + readonly clientApi: ServerSideApi, ) { this.settingsCache = new Map(); this.sugGen = new SuggestionGenerator((doc) => this.getSettings(doc)); @@ -134,7 +134,7 @@ class CodeActionHandler { log(`CodeAction Uri Not allowed: ${uri}`); return []; } - const pWorkspaceConfig = this.clientApi.sendOnWorkspaceConfigForDocumentRequest({ uri }); + const pWorkspaceConfig = this.clientApi.clientRequest.onWorkspaceConfigForDocumentRequest({ uri }); function replaceText(range: LangServerRange, text?: string) { return TextEdit.replace(range, text || ''); @@ -194,7 +194,7 @@ class CodeActionHandler { if (eslintSpellCheckerDiags.length > 1) return []; const { settings: docSetting, dictionary } = await this.getSettings(textDocument); - const pWorkspaceConfig = this.clientApi.sendOnWorkspaceConfigForDocumentRequest({ uri }); + const pWorkspaceConfig = this.clientApi.clientRequest.onWorkspaceConfigForDocumentRequest({ uri }); async function genCodeActions(_dictionary: SpellingDictionary) { const word = extractText(textDocument, params.range); diff --git a/packages/_server/src/progressNotifier.mts b/packages/_server/src/progressNotifier.mts index bcf9ee1078..ba7b1dc02c 100644 --- a/packages/_server/src/progressNotifier.mts +++ b/packages/_server/src/progressNotifier.mts @@ -1,7 +1,6 @@ import type { TextDocument } from 'vscode-languageserver-textdocument'; -import type { OnSpellCheckDocumentStep } from './api.js'; -import type { ClientApi } from './clientApi.mjs'; +import type { OnSpellCheckDocumentStep, ServerSideApi } from './api.js'; let seq = 0; @@ -9,10 +8,10 @@ export interface ProgressNotifier { emitSpellCheckDocumentStep: (doc: TextDocument, step: string, numIssues?: number) => void; } -export function createProgressNotifier(clientApi: ClientApi): ProgressNotifier { +export function createProgressNotifier(clientApi: ServerSideApi): ProgressNotifier { return { emitSpellCheckDocumentStep: (doc, step, numIssues) => - clientApi.sendOnSpellCheckDocument(toOnSpellCheckDocumentStep(doc, step, numIssues)), + clientApi.clientNotification.onSpellCheckDocument(toOnSpellCheckDocumentStep(doc, step, numIssues)), }; } diff --git a/packages/_server/src/progressNotifier.test.mts b/packages/_server/src/progressNotifier.test.mts index de1cfddbb1..d8a56f56ad 100644 --- a/packages/_server/src/progressNotifier.test.mts +++ b/packages/_server/src/progressNotifier.test.mts @@ -1,30 +1,60 @@ +import type { ExcludeDisposableHybrid } from 'utils-disposables'; +import { injectDisposable } from 'utils-disposables'; import { describe, expect, test, vi } from 'vitest'; -import type { Connection } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { createClientApi } from './clientApi.mjs'; +import type { MessageConnection, ServerSideApi } from './api.js'; import { createProgressNotifier } from './progressNotifier.mjs'; +import { createServerApi } from './serverApi.mjs'; -vi.mock('./clientApi'); +vi.mock('./serverApi'); -const mockedCreateClientApi = vi.mocked(createClientApi); +const mockedCreateClientApi = vi.mocked(createServerApi); // const mockedCreateConnection = jest.mocked(createConnection); mockedCreateClientApi.mockImplementation(() => { - return { - sendOnSpellCheckDocument: vi.fn(), - sendOnWorkspaceConfigForDocumentRequest: vi.fn(), - }; + const mock: ServerSideApi = injectDisposable>( + { + clientRequest: { + onWorkspaceConfigForDocumentRequest: vi.fn(), + }, + clientNotification: { + onSpellCheckDocument: vi.fn(), + }, + serverRequest: { + getConfigurationForDocument: { subscribe: vi.fn() }, + isSpellCheckEnabled: { subscribe: vi.fn() }, + splitTextIntoWords: { subscribe: vi.fn() }, + spellingSuggestions: { subscribe: vi.fn() }, + }, + serverNotification: { + notifyConfigChange: { subscribe: vi.fn() }, + registerConfigurationFile: { subscribe: vi.fn() }, + }, + }, + () => undefined, + ); + return mock; }); -const stub: any = {}; -const connection = stub as Connection; +const connection: MessageConnection = { + onNotification: vi.fn(), + onRequest: vi.fn(), + sendNotification: vi.fn(), + sendRequest: vi.fn((() => Promise.resolve(undefined)) as () => any), +}; + +// const mockConnection = vi.mocked(connection); + +const logger = { + log: vi.fn(), +}; describe('Validate Progress Notifier', () => { test('createProgressNotifier', async () => { - const clientApi = createClientApi(connection); + const clientApi = createServerApi(connection, {}, logger); const notifier = createProgressNotifier(clientApi); - const mockSendOnSpellCheckDocument = vi.mocked(clientApi.sendOnSpellCheckDocument); + const mockSendOnSpellCheckDocument = vi.mocked(clientApi.clientNotification.onSpellCheckDocument); expect(notifier.emitSpellCheckDocumentStep).toBeDefined(); diff --git a/packages/_server/src/server.mts b/packages/_server/src/server.mts index 94859c7f13..3dec715644 100644 --- a/packages/_server/src/server.mts +++ b/packages/_server/src/server.mts @@ -1,5 +1,3 @@ -// cSpell:ignore pycache - import { LogFileConnection } from '@internal/common-utils/index.js'; import { log, logError, logger, logInfo, setWorkspaceBase, setWorkspaceFolders } from '@internal/common-utils/log.js'; import { toFileUri, toUri } from '@internal/common-utils/uriHelper.js'; @@ -9,10 +7,10 @@ import { extractImportErrors, getDefaultSettings, refreshDictionaryCache } from import type { Subscription } from 'rxjs'; import { interval, ReplaySubject } from 'rxjs'; import { debounce, debounceTime, filter, mergeMap, take, tap } from 'rxjs/operators'; +import { LogLevelMasks } from 'utils-logger'; import { TextDocument } from 'vscode-languageserver-textdocument'; import type * as Api from './api.js'; -import { createClientApi } from './clientApi.mjs'; import { onCodeActionHandler } from './codeActions.mjs'; import { calculateConfigTargets } from './config/configTargetsHelper.mjs'; import { ConfigWatcher } from './config/configWatcher.mjs'; @@ -29,9 +27,11 @@ import { } from './config/documentSettings.mjs'; import type { TextDocumentUri } from './config/vscode.config.mjs'; import { createProgressNotifier } from './progressNotifier.mjs'; +import { createServerApi } from './serverApi.mjs'; import { defaultIsTextLikelyMinifiedOptions, isTextLikelyMinified } from './utils/analysis.mjs'; import { debounce as simpleDebounce } from './utils/debounce.mjs'; import { textToWords } from './utils/index.mjs'; +import { createPrecisionLogger } from './utils/logging.mjs'; import * as Validator from './validator.mjs'; import type { Diagnostic, @@ -46,9 +46,9 @@ import { CodeActionKind, createConnection, ProposedFeatures, TextDocuments, Text log('Starting Spell Checker Server'); -type ServerNotificationApiHandlers = { - [key in keyof Api.ServerNotifyApi]: (p: Parameters) => void | Promise; -}; +// type ServerNotificationApiHandlers = { +// [key in keyof Api.ServerNotifyApi]: (...p: Parameters) => void | Promise; +// }; const tds = CSpell; @@ -88,21 +88,34 @@ export function run(): void { const configWatcher = new ConfigWatcher(); disposables.push(configWatcher); - const requestMethodApi: Api.ServerRequestApiHandlers = { - isSpellCheckEnabled: handleIsSpellCheckEnabled, - getConfigurationForDocument: handleGetConfigurationForDocument, - splitTextIntoWords: handleSplitTextIntoWords, - spellingSuggestions: handleSpellingSuggestions, - }; - // Create a connection for the server. The connection uses Node's IPC as a transport log('Create Connection'); const connection = createConnection(ProposedFeatures.all); const documentSettings = new DocumentSettings(connection, defaultSettings); - const clientApi = createClientApi(connection); - const progressNotifier = createProgressNotifier(clientApi); + const _logger = createPrecisionLogger().setLogLevelMask(LogLevelMasks.none); + + const clientServerApi = createServerApi( + connection, + { + serverNotifications: { + notifyConfigChange: onConfigChange, + registerConfigurationFile, + }, + serverRequests: { + getConfigurationForDocument: handleGetConfigurationForDocument, + isSpellCheckEnabled: handleIsSpellCheckEnabled, + splitTextIntoWords: handleSplitTextIntoWords, + spellingSuggestions: handleSpellingSuggestions, + }, + }, + _logger, + ); + + disposables.push(clientServerApi); + + const progressNotifier = createProgressNotifier(clientServerApi); // Create a simple text document manager. const documents = new TextDocuments(TextDocument); @@ -178,21 +191,12 @@ export function run(): void { return documentSettings.getUriSettings(uri || ''); } - function registerConfigurationFile([path]: [string]) { + function registerConfigurationFile(path: string) { documentSettings.registerConfigurationFile(path); logInfo('Register Configuration File', path); triggerUpdateConfig.next(undefined); } - const serverNotificationApiHandlers: ServerNotificationApiHandlers = { - notifyConfigChange: () => onConfigChange(), - registerConfigurationFile: registerConfigurationFile, - }; - - Object.entries(serverNotificationApiHandlers).forEach(([method, fn]) => { - connection.onNotification(method, fn); - }); - interface TextDocumentInfo { uri?: string; languageId?: string; @@ -279,11 +283,6 @@ export function run(): void { return {}; } - // Register API Handlers - Object.entries(requestMethodApi).forEach(([name, fn]) => { - connection.onRequest(name, fn); - }); - interface DocSettingPair { doc: TextDocument; settings: CSpellUserSettings; @@ -619,7 +618,7 @@ export function run(): void { return folders || undefined; } - connection.onCodeAction(onCodeActionHandler(documents, getBaseSettings, () => documentSettings.version, clientApi)); + connection.onCodeAction(onCodeActionHandler(documents, getBaseSettings, () => documentSettings.version, clientServerApi)); // Free up the validation streams on shutdown. connection.onShutdown(() => { diff --git a/packages/_server/src/serverApi.mts b/packages/_server/src/serverApi.mts new file mode 100644 index 0000000000..d0e137ec56 --- /dev/null +++ b/packages/_server/src/serverApi.mts @@ -0,0 +1,37 @@ +import type { Logger, MessageConnection, ServerSideApi, ServerSideApiDef, ServerSideHandlers } from './api.js'; +import { createServerSideApi } from './api.js'; + +export type { ServerSideHandlers } from './api.js'; + +export type PartialServerSideHandlers = PartialPartial; + +export function createServerApi(connection: MessageConnection, handlers: PartialServerSideHandlers, logger: Logger): ServerSideApi { + const api: ServerSideApiDef = { + serverRequests: { + getConfigurationForDocument: true, + isSpellCheckEnabled: true, + splitTextIntoWords: true, + spellingSuggestions: true, + ...handlers.serverRequests, + }, + serverNotifications: { + notifyConfigChange: true, + registerConfigurationFile: true, + ...handlers.serverNotifications, + }, + clientRequests: { + // addWordsToConfigFileFromServer: true, + // addWordsToDictionaryFileFromServer: true, + // addWordsToVSCodeSettingsFromServer: true, + onWorkspaceConfigForDocumentRequest: true, + }, + clientNotifications: { + onSpellCheckDocument: true, + }, + }; + return createServerSideApi(connection, api, logger); +} + +export type PartialPartial = { + [K in keyof T]?: Partial; +}; diff --git a/packages/_server/src/serverApi.test.mts b/packages/_server/src/serverApi.test.mts new file mode 100644 index 0000000000..45faaa48f1 --- /dev/null +++ b/packages/_server/src/serverApi.test.mts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; + +import type { MessageConnection, OnSpellCheckDocumentStep, ServerSideHandlers, WorkspaceConfigForDocumentRequest } from './api.js'; +import { createServerApi } from './serverApi.mjs'; + +const connection: MessageConnection = { + onNotification: vi.fn(), + onRequest: vi.fn(), + sendNotification: vi.fn(), + sendRequest: vi.fn((() => Promise.resolve(undefined)) as () => any), +}; + +const mockConnection = vi.mocked(connection); + +const logger = { + log: vi.fn(), +}; + +describe('Validate Client Api', () => { + beforeEach(() => { + mockConnection.sendNotification.mockClear(); + mockConnection.sendRequest.mockClear(); + }); + + test('sendOnSpellCheckDocument', () => { + const handlers = mockHandlers(); + const api = createServerApi(connection, handlers, logger); + const p: OnSpellCheckDocumentStep = { + uri: 'uri', + seq: 1, + step: 'step', + ts: 1, + version: 1, + }; + api.clientNotification.onSpellCheckDocument(p); + expect(mockConnection.sendNotification).toHaveBeenLastCalledWith(expect.stringContaining('onSpellCheckDocument'), [p]); + }); + + test('sendOnWorkspaceConfigForDocumentRequest', () => { + const handlers = mockHandlers(); + const api = createServerApi(connection, handlers, logger); + const req: WorkspaceConfigForDocumentRequest = { + uri: 'uri', + }; + api.clientRequest.onWorkspaceConfigForDocumentRequest(req); + expect(mockConnection.sendRequest).toHaveBeenLastCalledWith(expect.stringContaining('onWorkspaceConfigForDocumentRequest'), [req]); + }); +}); + +function mockHandlers(): ServerSideHandlers { + return { + serverNotifications: { + notifyConfigChange: vi.fn(), + registerConfigurationFile: vi.fn(), + }, + serverRequests: { + getConfigurationForDocument: vi.fn(() => ({ + languageEnabled: undefined, + fileEnabled: true, + fileIsIncluded: true, + fileIsExcluded: false, + excludedBy: undefined, + gitignored: undefined, + gitignoreInfo: undefined, + blockedReason: undefined, + settings: undefined, + docSettings: undefined, + configFiles: [], + configTargets: [], + })), + isSpellCheckEnabled: vi.fn(() => ({ + languageEnabled: undefined, + fileEnabled: true, + fileIsIncluded: true, + fileIsExcluded: false, + excludedBy: undefined, + gitignored: undefined, + gitignoreInfo: undefined, + blockedReason: undefined, + })), + splitTextIntoWords: vi.fn(() => ({ words: [] })), + spellingSuggestions: vi.fn(), + }, + }; +} diff --git a/packages/_server/src/utils/logging.mts b/packages/_server/src/utils/logging.mts new file mode 100644 index 0000000000..1a318ca8d0 --- /dev/null +++ b/packages/_server/src/utils/logging.mts @@ -0,0 +1,14 @@ +import { format } from 'node:util'; + +import { logger as internalLogger } from '@internal/common-utils/log.js'; +import { createLogger } from 'utils-logger'; + +export function createPrecisionLogger() { + return createLogger({ + log: (...params) => internalLogger.log(format(...params)), + info: (...params) => internalLogger.info(format(...params)), + debug: (...params) => internalLogger.debug(format(...params)), + error: (...params) => internalLogger.error(format(...params)), + warn: (...params) => internalLogger.warn(format(...params)), + }); +} diff --git a/packages/_server/tsconfig.json b/packages/_server/tsconfig.json index 54daa59934..d14f39190a 100644 --- a/packages/_server/tsconfig.json +++ b/packages/_server/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.packages.base.json", "compilerOptions": { - "moduleResolution": "Node16", "outDir": "./dist", "types": ["node"] }, diff --git a/packages/client/src/client/client.ts b/packages/client/src/client/client.ts index 1a3ca01974..fb2d4af1ef 100644 --- a/packages/client/src/client/client.ts +++ b/packages/client/src/client/client.ts @@ -122,6 +122,7 @@ export class CSpellClient implements Disposable { this.client = new LanguageClient('cspell', 'Code Spell Checker', serverOptions, clientOptions); this.client.registerProposedFeatures(); this.serverApi = createServerApi(this.client); + context.subscriptions.push(this.serverApi); this.initWhenReady(); } @@ -196,9 +197,9 @@ export class CSpellClient implements Disposable { return this.notifySettingsChanged(); } - public async whenReady(fn: () => R): Promise { + public async whenReady(fn: () => R): Promise> { await this.onReady(); - return fn(); + return await fn(); } private onReady(): Promise { diff --git a/packages/client/src/client/server/server.ts b/packages/client/src/client/server/server.ts index 1d89a9f8f6..2d41b1a79a 100644 --- a/packages/client/src/client/server/server.ts +++ b/packages/client/src/client/server/server.ts @@ -1,20 +1,8 @@ -import type { - ClientNotifications, - ClientNotificationsApi, - Fn, - Req, - RequestResponseFn, - RequestsToClient, - Res, - ServerMethods, - ServerNotifyApi, - ServerRequestApi, -} from 'code-spell-checker-server/api'; +import type { ClientSideApi, ClientSideApiDef } from 'code-spell-checker-server/api'; +import { createClientSideApi } from 'code-spell-checker-server/api'; import type { CodeAction, CodeActionParams, Command, LanguageClient } from 'vscode-languageclient/node'; -import { CodeActionRequest, NotificationType, RequestType } from 'vscode-languageclient/node'; +import { CodeActionRequest } from 'vscode-languageclient/node'; export type { - ClientNotifications, - ClientNotificationsApi, ClientSideCommandHandlerApi, ConfigKind, ConfigScope, @@ -30,7 +18,6 @@ export type { DictionaryDefinition, DictionaryDefinitionCustom, FieldExistsInTarget, - Fn, GetConfigurationForDocumentResult, IsSpellCheckEnabledResult, LanguageSetting, @@ -38,36 +25,33 @@ export type { NamedPattern, OnSpellCheckDocumentStep, PatternMatch, - Req, - RequestResponseFn, - RequestsToClient, - Res, - ServerMethods, - ServerNotifyApi, - ServerRequestApi, SpellCheckerSettingsProperties, SplitTextIntoWordsResult, WorkspaceConfigForDocumentRequest, WorkspaceConfigForDocumentResponse, } from 'code-spell-checker-server/api'; -export interface ServerApi extends ServerRequestApi, ServerNotifyApi, ServerEventApi, RequestsFromServerHandlerApi {} +interface ServerSide { + getConfigurationForDocument: ClientSideApi['serverRequest']['getConfigurationForDocument']; + isSpellCheckEnabled: ClientSideApi['serverRequest']['isSpellCheckEnabled']; + notifyConfigChange: ClientSideApi['serverNotification']['notifyConfigChange']; + registerConfigurationFile: ClientSideApi['serverNotification']['registerConfigurationFile']; + spellingSuggestions: ClientSideApi['serverRequest']['spellingSuggestions']; +} + +interface ExtensionSide { + onSpellCheckDocument: ClientSideApi['clientNotification']['onSpellCheckDocument']['subscribe']; + onWorkspaceConfigForDocumentRequest: ClientSideApi['clientRequest']['onWorkspaceConfigForDocumentRequest']['subscribe']; +} +export interface ServerApi extends ServerSide, ExtensionSide, Disposable {} type Disposable = { dispose: () => void; }; -type ServerEventApi = { - [K in keyof ClientNotifications]: (handler: ClientNotificationsApi[K]) => Disposable; -}; - -type RequestsFromServer = { - [K in keyof RequestsToClient]: RequestResponseFn; -}; - -type RequestsFromServerHandlerApi = { - [M in keyof RequestsFromServer]: (handler: Fn) => Disposable; -}; +// type RequestsFromServerHandlerApi = { +// [M in keyof RequestsFromServer]: (handler: Fn) => Disposable; +// }; type RequestCodeActionResult = (Command | CodeAction)[] | null; @@ -78,36 +62,77 @@ export async function requestCodeAction(client: LanguageClient, params: CodeActi } export function createServerApi(client: LanguageClient): ServerApi { - async function sendRequest(method: M, param: Req): Promise> { - const r = new RequestType, Res, void>(method); - const result = await client.sendRequest(r, param); - return result; - } - - function onNotify(method: M, fn: ClientNotificationsApi[M]) { - const n = new NotificationType(method); - return client.onNotification(n, fn); - } + const def: ClientSideApiDef = { + serverRequests: { + isSpellCheckEnabled: true, + getConfigurationForDocument: true, + spellingSuggestions: true, + splitTextIntoWords: true, + }, + serverNotifications: { + notifyConfigChange: true, + registerConfigurationFile: true, + }, + clientNotifications: { + onSpellCheckDocument: true, + }, + clientRequests: { + // addWordsToConfigFileFromServer: true, + // addWordsToDictionaryFileFromServer: true, + // addWordsToVSCodeSettingsFromServer: true, + onWorkspaceConfigForDocumentRequest: true, + }, + }; - function onRequest(method: M, fn: Fn) { - const n = new RequestType, Res, void>(method); - return client.onRequest(n, fn); - } - - function sendNotification(method: K, ...params: Parameters): void { - client.sendNotification(method, params); - } + const rpcApi = createClientSideApi(client, def, { log: () => undefined }); const api: ServerApi = { - isSpellCheckEnabled: (param) => sendRequest('isSpellCheckEnabled', param), - getConfigurationForDocument: (param) => sendRequest('getConfigurationForDocument', param), - splitTextIntoWords: (param) => sendRequest('splitTextIntoWords', param), - spellingSuggestions: (param) => sendRequest('spellingSuggestions', param), - notifyConfigChange: (...params) => sendNotification('notifyConfigChange', ...params), - registerConfigurationFile: (...params) => sendNotification('registerConfigurationFile', ...params), - onSpellCheckDocument: (fn) => onNotify('onSpellCheckDocument', fn), - onWorkspaceConfigForDocumentRequest: (fn) => onRequest('onWorkspaceConfigForDocumentRequest', fn), + isSpellCheckEnabled: log2Sfn(rpcApi.serverRequest.isSpellCheckEnabled, 'isSpellCheckEnabled'), + getConfigurationForDocument: log2Sfn(rpcApi.serverRequest.getConfigurationForDocument, 'getConfigurationForDocument'), + spellingSuggestions: log2Sfn(rpcApi.serverRequest.spellingSuggestions, 'spellingSuggestions'), + notifyConfigChange: log2Sfn(rpcApi.serverNotification.notifyConfigChange, 'notifyConfigChange'), + registerConfigurationFile: log2Sfn(rpcApi.serverNotification.registerConfigurationFile, 'registerConfigurationFile'), + onSpellCheckDocument: (fn) => rpcApi.clientNotification.onSpellCheckDocument.subscribe(log2Cfn(fn, 'onSpellCheckDocument')), + onWorkspaceConfigForDocumentRequest: (fn) => + rpcApi.clientRequest.onWorkspaceConfigForDocumentRequest.subscribe(log2Cfn(fn, 'onWorkspaceConfigForDocumentRequest')), + + dispose: rpcApi.dispose, }; return api; } + +let reqNum = 0; +const debugCommunication = true; +const debugServerComms = true; +const debugClientComms = true; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function log2Sfn

(fn: (...p: P) => T | Promise, reqName: string): (...p: P) => Promise { + return (...params: P) => log2S(params, fn(...params), reqName); +} + +function log2S(params: P, value: Promise | T, reqName: string): Promise { + return logCommunication('Server R/N', params, value, reqName, debugServerComms); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function log2Cfn

(fn: (...p: P) => T | Promise, reqName: string): (...p: P) => Promise { + return (...params: P) => log2C(params, fn(...params), reqName); +} + +function log2C(params: P, value: Promise | T, reqName: string): Promise> { + return logCommunication('Client R/N', params, value, reqName, debugClientComms); +} + +async function logCommunication(kind: string, params: P, value: Promise | T, name: string, log: boolean): Promise> { + const id = ++reqNum; + let result: Awaited | undefined = undefined; + log && debugCommunication && console.log('%s %i Start %s: %s(%o)', new Date().toISOString(), id, kind, name, params); + try { + result = await value; + return result; + } finally { + log && debugCommunication && console.log('%s %i End %s: %s, %o', new Date().toISOString(), id, kind, name, result); + } +} diff --git a/packages/json-rpc-api/src/index.ts b/packages/json-rpc-api/src/index.ts index 5bc1d5cb26..b4ae2ffa5e 100644 --- a/packages/json-rpc-api/src/index.ts +++ b/packages/json-rpc-api/src/index.ts @@ -1,13 +1,13 @@ -export { - type ApplyNotificationAPI, - type ApplyRequestAPI, - type ClientAPIDef, - type ClientSideMethods, - createClientApi, - createServerApi, - type Logger, - type RpcAPI, - type ServerAPIDef, - type ServerSideMethods, +export type { + ApiPrefix, + ApplyNotificationAPI, + ApplyRequestAPI, + ClientAPIDef, + ClientSideMethods, + Logger, + RpcAPI, + ServerAPIDef, + ServerSideMethods, } from './json-rpc-api.js'; +export { createClientApi, createServerApi } from './json-rpc-api.js'; export type { MessageConnection } from './types.js'; diff --git a/packages/json-rpc-api/src/json-rpc-api.ts b/packages/json-rpc-api/src/json-rpc-api.ts index c24107eec9..9dcacc5764 100644 --- a/packages/json-rpc-api/src/json-rpc-api.ts +++ b/packages/json-rpc-api/src/json-rpc-api.ts @@ -13,11 +13,13 @@ import type { ReturnPromise, } from './types'; -export const apiPrefix = { - serverRequest: 'sr_', - serverNotification: 'sn_', - clientRequest: 'cr_', - clientNotification: 'cn_', +export type ApiPrefix = Record; + +export const defaultApiPrefix: ApiPrefix = { + serverRequests: 'sr_', + serverNotifications: 'sn_', + clientRequests: 'cr_', + clientNotifications: 'cn_', } as const; type CallBack = Func; @@ -51,6 +53,10 @@ export interface Subscribable { subscribe(fn: T): DisposableHybrid; } +export interface SingleSubscriber { + subscribe(fn: T | ReturnPromise): DisposableHybrid; +} + export interface PubSub extends Subscribable { publish: (...args: Parameters) => Promise; } @@ -70,6 +76,10 @@ type WrapInSubscribable = { [P in keyof A]: A[P] extends CallBack ? Subscribable : never; }; +type WrapInSingleSubscriber = { + [P in keyof A]: A[P] extends CallBack ? SingleSubscriber : never; +}; + type WrapInPubSub = { [P in keyof A]: A[P] extends CallBack ? PubSub : never; }; @@ -77,12 +87,12 @@ type WrapInPubSub = { export type ServerSideMethods = { clientRequest: MakeMethodsAsync>; clientNotification: MakeMethodsAsync>; - serverRequest: WrapInSubscribable>; + serverRequest: WrapInSingleSubscriber>; serverNotification: WrapInSubscribable>; } & DisposableHybrid; export type ClientSideMethods = { - clientRequest: WrapInSubscribable>; + clientRequest: WrapInSingleSubscriber>; clientNotification: WrapInSubscribable>; serverRequest: MakeMethodsAsync>; serverNotification: MakeMethodsAsync>; @@ -120,20 +130,21 @@ export function createServerApi( connection: MessageConnection, api: ServerAPIDef, logger?: Logger, + apiPrefix: ApiPrefix = defaultApiPrefix, ): ServerSideMethods { const _disposables: DisposableLike[] = []; const serverRequest = mapRequestsToPubSub>(api.serverRequests, logger); const serverNotification = mapNotificationsToPubSub>(api.serverNotifications, logger); - bindRequests(connection, apiPrefix.serverRequest, serverRequest, _disposables, logger); - bindNotifications(connection, apiPrefix.serverNotification, serverNotification, _disposables, logger); + bindRequests(connection, apiPrefix.serverRequests, serverRequest, _disposables, logger); + bindNotifications(connection, apiPrefix.serverNotifications, serverNotification, _disposables, logger); type CR = ClientRequests; type CN = ClientNotifications; - const clientRequest = mapRequestsToFn(connection, apiPrefix.clientRequest, api.clientRequests, logger); - const clientNotification = mapNotificationsToFn(connection, apiPrefix.clientNotification, api.clientNotifications, logger); + const clientRequest = mapRequestsToFn(connection, apiPrefix.clientRequests, api.clientRequests, logger); + const clientNotification = mapNotificationsToFn(connection, apiPrefix.clientNotifications, api.clientNotifications, logger); return injectDisposable( { @@ -156,20 +167,21 @@ export function createClientApi( connection: MessageConnection, api: ClientAPIDef, logger?: Logger, + apiPrefix: ApiPrefix = defaultApiPrefix, ): ClientSideMethods { const _disposables: DisposableLike[] = []; const clientRequest = mapRequestsToPubSub>(api.clientRequests, logger); const clientNotification = mapNotificationsToPubSub>(api.clientNotifications, logger); - bindRequests(connection, apiPrefix.clientRequest, clientRequest, _disposables, logger); - bindNotifications(connection, apiPrefix.clientNotification, clientNotification, _disposables, logger); + bindRequests(connection, apiPrefix.clientRequests, clientRequest, _disposables, logger); + bindNotifications(connection, apiPrefix.clientNotifications, clientNotification, _disposables, logger); type SR = ServerRequests; type SN = ServerNotifications; - const serverRequest = mapRequestsToFn(connection, apiPrefix.serverRequest, api.serverRequests, logger); - const serverNotification = mapNotificationsToFn(connection, apiPrefix.serverNotification, api.serverNotifications, logger); + const serverRequest = mapRequestsToFn(connection, apiPrefix.serverRequests, api.serverRequests, logger); + const serverNotification = mapNotificationsToFn(connection, apiPrefix.serverNotifications, api.serverNotifications, logger); return injectDisposable( { @@ -292,14 +304,14 @@ function createPubMultipleSubscribers void async function publish(..._p: Parameters) { for (const s of subscribers) { - logger?.log(`notify ${name} %o`, s); + logger?.log(`notify ${name} %s`, typeof s); // eslint-disable-next-line prefer-rest-params await s(...arguments); } } function subscribe(s: Subscriber): DisposableHybrid { - logger?.log(`subscribe to ${name} %o`, s); + logger?.log(`subscribe to ${name} %s`, typeof s); subscribers.add(s); return createDisposable(() => subscribers.delete(s)); } @@ -318,7 +330,7 @@ function createPubSingleSubscriber any>(nam function subscribe(s: Subscriber): DisposableHybrid { subscriber = s; - logger?.log(`subscribe to ${name} %o`, s); + logger?.log(`subscribe to ${name} %s`, typeof s); return createDisposable(() => { if (subscriber === s) { subscriber = undefined; diff --git a/packages/utils-disposables/src/disposable.test.ts b/packages/utils-disposables/src/disposable.test.ts index b6cdb6dd66..b612f66c20 100644 --- a/packages/utils-disposables/src/disposable.test.ts +++ b/packages/utils-disposables/src/disposable.test.ts @@ -1,6 +1,6 @@ import { describe, expect, jest, test } from '@jest/globals'; -import type { DisposableLike, DisposeFn } from './disposable.js'; +import type { DisposableHybrid, DisposableLike, DisposeFn, ExcludeDisposableHybrid } from './disposable.js'; import { createDisposable, createDisposableFromList, @@ -72,6 +72,22 @@ describe('disposable', () => { expect(myObj.callMe).toHaveBeenCalledTimes(1); }); + test('injectDisposable into Disposable type', () => { + const myObj = { + callMe: jest.fn(), + }; + type MyObj = typeof myObj & DisposableHybrid; + const dispose = jest.fn(myObj.callMe); + const myDisposable: MyObj = injectDisposable>(myObj, dispose); + + function use() { + using _obj = myDisposable; + } + use(); + expect(dispose).toHaveBeenCalledTimes(1); + expect(myObj.callMe).toHaveBeenCalledTimes(1); + }); + test('double disposed', () => { const dispose = jest.fn(); const myDisposable = createDisposable(dispose); diff --git a/packages/utils-disposables/src/disposable.ts b/packages/utils-disposables/src/disposable.ts index 66ed68e36d..b9a160f33f 100644 --- a/packages/utils-disposables/src/disposable.ts +++ b/packages/utils-disposables/src/disposable.ts @@ -267,3 +267,5 @@ export function isDisposableHybrid(disposable: unknown): disposable is Disposabl function dbgPad(): string { return ' '.repeat(debugDepth); } + +export type ExcludeDisposableHybrid = Omit; diff --git a/packages/utils-disposables/src/index.ts b/packages/utils-disposables/src/index.ts index 5f82890a23..21d9faa16c 100644 --- a/packages/utils-disposables/src/index.ts +++ b/packages/utils-disposables/src/index.ts @@ -4,6 +4,7 @@ export type { DisposableHybrid, DisposableLike, DisposeFn, + ExcludeDisposableHybrid, Logger, } from './disposable.js'; export { diff --git a/packages/webview-api/package.json b/packages/webview-api/package.json index 1bce27907a..5a21082675 100644 --- a/packages/webview-api/package.json +++ b/packages/webview-api/package.json @@ -17,8 +17,7 @@ "dependencies": { "json-rpc-api": "file:../json-rpc-api", "utils-disposables": "file:../utils-disposables", - "utils-logger": "file:../utils-logger", - "vscode-webview-rpc": "file:../webview-rpc" + "utils-logger": "file:../utils-logger" }, "engines": { "node": ">18.0.0" diff --git a/packages/webview-api/src/api.test.ts b/packages/webview-api/src/api.test.ts index a065f2c117..7e535c9b95 100644 --- a/packages/webview-api/src/api.test.ts +++ b/packages/webview-api/src/api.test.ts @@ -1,6 +1,6 @@ +import type { MessageConnection } from 'json-rpc-api'; import { createDisposable } from 'utils-disposables'; import { describe, expect, test, vi } from 'vitest'; -import type { MessageConnection } from 'vscode-webview-rpc'; import type { ClientSideApiDef } from './api'; import * as api from './api';