From 7d170a14b7502884ef734af7413664699d008823 Mon Sep 17 00:00:00 2001 From: Le Vivilet Date: Sat, 1 Aug 2026 00:46:36 +0000 Subject: [PATCH] feature: route extension file reads through management worker --- .../CreateExtensionHostRpc.ts | 16 ---- .../src/parts/Initialize/Initialize.ts | 2 - .../diff-view/src/parts/ReadFile/ReadFile.ts | 24 ++++-- .../SendMessagePortToExtensionHostWorker.ts | 5 -- packages/diff-view/test/Initialize.test.ts | 9 +-- packages/diff-view/test/LoadContent.test.ts | 20 +---- .../diff-view/test/LoadFileContents.test.ts | 31 +------- packages/diff-view/test/ReadFile.test.ts | 76 ++++++------------- 8 files changed, 49 insertions(+), 134 deletions(-) delete mode 100644 packages/diff-view/src/parts/CreateExtensionHostRpc/CreateExtensionHostRpc.ts delete mode 100644 packages/diff-view/src/parts/SendMessagePortToExtensionHostWorker/SendMessagePortToExtensionHostWorker.ts diff --git a/packages/diff-view/src/parts/CreateExtensionHostRpc/CreateExtensionHostRpc.ts b/packages/diff-view/src/parts/CreateExtensionHostRpc/CreateExtensionHostRpc.ts deleted file mode 100644 index 585b9137..00000000 --- a/packages/diff-view/src/parts/CreateExtensionHostRpc/CreateExtensionHostRpc.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TransferMessagePortRpcParent } from '@lvce-editor/rpc' -import { ExtensionHost } from '@lvce-editor/rpc-registry' -import { VError } from '@lvce-editor/verror' -import { sendMessagePortToExtensionHostWorker } from '../SendMessagePortToExtensionHostWorker/SendMessagePortToExtensionHostWorker.ts' - -export const createExtensionHostRpc = async (): Promise => { - try { - const rpc = await TransferMessagePortRpcParent.create({ - commandMap: {}, - send: sendMessagePortToExtensionHostWorker, - }) - ExtensionHost.set(rpc) - } catch (error) { - throw new VError(error, `Failed to create extension host rpc`) - } -} diff --git a/packages/diff-view/src/parts/Initialize/Initialize.ts b/packages/diff-view/src/parts/Initialize/Initialize.ts index 9e2ea299..13b17eee 100644 --- a/packages/diff-view/src/parts/Initialize/Initialize.ts +++ b/packages/diff-view/src/parts/Initialize/Initialize.ts @@ -1,6 +1,5 @@ import { TextMeasurementWorker } from '@lvce-editor/rpc-registry' import type { DiffViewState } from '../DiffViewState/DiffViewState.ts' -import { createExtensionHostRpc } from '../CreateExtensionHostRpc/CreateExtensionHostRpc.ts' import { createTextMeasurementWorkerRpc } from '../CreateTextMeasurementWorkerRpc/CreateTextMeasurementWorkerRpc.ts' import { getKeys, get as getDiffViewState, set as setDiffViewState } from '../DiffViewStates/DiffViewStates.ts' import { initializeDiffWorker } from '../InitializeDiffWorker/InitializeDiffWorker.ts' @@ -30,7 +29,6 @@ const measureCharWidth = async (state: DiffViewState): Promise => { await Promise.all([ - createExtensionHostRpc(), createTextMeasurementWorkerRpc(), initializeDiffWorker(), initializeExtensionManagementWorker(), diff --git a/packages/diff-view/src/parts/ReadFile/ReadFile.ts b/packages/diff-view/src/parts/ReadFile/ReadFile.ts index 56fac2c5..0acc1be7 100644 --- a/packages/diff-view/src/parts/ReadFile/ReadFile.ts +++ b/packages/diff-view/src/parts/ReadFile/ReadFile.ts @@ -1,8 +1,23 @@ -import { ExtensionHost, FileSystemWorker, RendererWorker } from '@lvce-editor/rpc-registry' -import { getPath } from '../GetPath/GetPath.ts' +import { ExtensionManagementWorker, FileSystemWorker, RendererWorker } from '@lvce-editor/rpc-registry' import { getProtocol } from '../GetProtocol/GetProtocol.ts' import { toFileUri } from '../ToFileUri/ToFileUri.ts' +interface FileSystemProviderResult { + readonly found: boolean + readonly result?: unknown +} + +const readExtensionFile = async (protocol: string, uri: string): Promise => { + const response = (await ExtensionManagementWorker.invoke('Extensions.executeFileSystemProviderReadFile', protocol, uri)) as FileSystemProviderResult + if (!response.found) { + throw new Error(`no file system provider found for ${protocol}`) + } + if (typeof response.result !== 'string') { + throw new TypeError(`expected file system provider ${protocol} to return a string`) + } + return response.result +} + export const readFile = async (uri: string): Promise => { if (!uri || uri.startsWith('untitled://')) { return '' @@ -17,9 +32,8 @@ export const readFile = async (uri: string): Promise => { if (protocol === 'fetch') { return RendererWorker.readFile(uri) } - const path = getPath(protocol, uri) if (protocol === 'memfs') { - return ExtensionHost.invoke('FileSystemMemory.readFile', path) + return RendererWorker.readFile(uri) } - return ExtensionHost.invoke('ExtensionHostFileSystem.readFile', protocol, path) + return readExtensionFile(protocol, uri) } diff --git a/packages/diff-view/src/parts/SendMessagePortToExtensionHostWorker/SendMessagePortToExtensionHostWorker.ts b/packages/diff-view/src/parts/SendMessagePortToExtensionHostWorker/SendMessagePortToExtensionHostWorker.ts deleted file mode 100644 index 4d9e2758..00000000 --- a/packages/diff-view/src/parts/SendMessagePortToExtensionHostWorker/SendMessagePortToExtensionHostWorker.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { RendererWorker } from '@lvce-editor/rpc-registry' - -export const sendMessagePortToExtensionHostWorker = async (port: any): Promise => { - await RendererWorker.sendMessagePortToExtensionHostWorker(port, 0) -} diff --git a/packages/diff-view/test/Initialize.test.ts b/packages/diff-view/test/Initialize.test.ts index a40f643d..f64e0d01 100644 --- a/packages/diff-view/test/Initialize.test.ts +++ b/packages/diff-view/test/Initialize.test.ts @@ -1,17 +1,15 @@ import { expect, test } from '@jest/globals' -import { ExtensionHost, RendererWorker, TextMeasurementWorker } from '@lvce-editor/rpc-registry' +import { RendererWorker, TextMeasurementWorker } from '@lvce-editor/rpc-registry' import { initialize } from '../src/parts/Initialize/Initialize.ts' -test('initialize should set ExtensionHost and TextMeasurementWorker RPCs', async (): Promise => { +test('initialize should set TextMeasurementWorker RPC', async (): Promise => { const commandMap = { 'RendererWorker.sendMessagePortToDiffWorker': async (): Promise => {}, 'RendererWorker.sendMessagePortToErrorWorker': async (): Promise => {}, - 'RendererWorker.sendMessagePortToExtensionHostWorker': async (): Promise => {}, 'RendererWorker.sendMessagePortToExtensionManagementWorker': async (): Promise => {}, 'RendererWorker.sendMessagePortToFileSystemWorker': async (): Promise => {}, 'RendererWorker.sendMessagePortToSyntaxHighlightingWorker': async (): Promise => {}, 'RendererWorker.sendMessagePortToTextMeasurementWorker': async (): Promise => {}, - 'SendMessagePortToExtensionHostWorker.sendMessagePortToExtensionHostWorker': async (): Promise => {}, 'SendMessagePortToExtensionHostWorker.sendMessagePortToTextMeasurementWorker': async (): Promise => {}, 'SendMessagePortToTextMeasurementWorker.sendMessagePortToTextMeasurementWorker': async (): Promise => {}, } @@ -19,7 +17,6 @@ test('initialize should set ExtensionHost and TextMeasurementWorker RPCs', async await initialize() - expect(typeof ExtensionHost.invoke).toBe('function') expect(typeof TextMeasurementWorker.invoke).toBe('function') - await Promise.all([ExtensionHost.dispose(), TextMeasurementWorker.dispose()]) + await TextMeasurementWorker.dispose() }) diff --git a/packages/diff-view/test/LoadContent.test.ts b/packages/diff-view/test/LoadContent.test.ts index 785c1611..0a7f2c14 100644 --- a/packages/diff-view/test/LoadContent.test.ts +++ b/packages/diff-view/test/LoadContent.test.ts @@ -1,23 +1,12 @@ import { expect, jest, test } from '@jest/globals' import { RpcId } from '@lvce-editor/constants' -import { DiffWorker, ExtensionHost, ExtensionManagementWorker, FileSystemWorker, SyntaxHighlightingWorker } from '@lvce-editor/rpc-registry' +import { DiffWorker, ExtensionManagementWorker, FileSystemWorker, SyntaxHighlightingWorker } from '@lvce-editor/rpc-registry' import { registerMockRpc } from '@lvce-editor/rpc-registry' import { createDefaultState } from '../src/parts/CreateDefaultState/CreateDefaultState.ts' import { loadContent } from '../src/parts/LoadContent/LoadContent.ts' import { VisibleLineType } from '../src/parts/VisibleLine/VisibleLine.ts' test('loadContent loads both sides of an inline diff uri', async (): Promise => { - const extensionHostRpc = ExtensionHost.registerMockRpc({ - 'ExtensionHostFileSystem.readFile': async (protocol: string, path: string): Promise => { - if (protocol === 'data' && path === 'before-content') { - return 'before-content' - } - if (protocol === 'file' && path === '/tmp/after.txt') { - return 'after-content\nsecond-line' - } - throw new Error(`unexpected params: ${protocol} ${path}`) - }, - }) const fileSystemWorkerRpc = FileSystemWorker.registerMockRpc({ 'FileSystem.readFile': async (uri: string): Promise => { if (uri === 'file:///tmp/after.txt') { @@ -48,7 +37,6 @@ test('loadContent loads both sides of an inline diff uri', async (): Promise => { - throw new Error('should not invoke extension host') - }, - }) const fileSystemWorkerRpc = FileSystemWorker.registerMockRpc({ 'FileSystem.readFile': async (uri: string): Promise => { if (uri === 'file:///tmp/missing.txt') { @@ -158,7 +141,6 @@ test('loadContent stores pane load errors instead of throwing', async (): Promis const result = await loadContent(state, { minLineY: 0 }) - expect(extensionHostRpc.invocations).toEqual([]) expect(errorWorkerRpc.invocations).toEqual([['Errors.prepare', error]]) expect(fileSystemWorkerRpc.invocations).toEqual([['FileSystem.readFile', 'file:///tmp/missing.txt']]) expect(diffWorkerRpc.invocations).toEqual([]) diff --git a/packages/diff-view/test/LoadFileContents.test.ts b/packages/diff-view/test/LoadFileContents.test.ts index 1abb7c12..1eac73a1 100644 --- a/packages/diff-view/test/LoadFileContents.test.ts +++ b/packages/diff-view/test/LoadFileContents.test.ts @@ -1,28 +1,9 @@ import { expect, test } from '@jest/globals' -import { ExtensionHost, FileSystemWorker } from '@lvce-editor/rpc-registry' +import { FileSystemWorker } from '@lvce-editor/rpc-registry' import { loadFileContents } from '../src/parts/LoadFileContents/LoadFileContents.ts' test('loadFileContents loads both files in order', async (): Promise => { - const extensionHostInvocations: unknown[][] = [] const fileSystemWorkerInvocations: unknown[][] = [] - const extensionHostRpc = { - dispose: (): void => {}, - invoke: async (method: string, ...params: readonly unknown[]): Promise => { - extensionHostInvocations.push([method, ...params]) - if (method !== 'ExtensionHostFileSystem.readFile') { - throw new Error(`unexpected method: ${method}`) - } - const [protocol, path] = params - if (protocol === 'data' && path === 'before-content') { - return 'before-content' - } - if (protocol === 'file' && path === '/tmp/after.txt') { - return 'after-content' - } - throw new Error(`unexpected params: ${String(protocol)} ${String(path)}`) - }, - set: (): void => {}, - } const fileSystemWorkerRpc = { dispose: (): void => {}, invoke: async (method: string, ...params: readonly unknown[]): Promise => { @@ -38,12 +19,10 @@ test('loadFileContents loads both files in order', async (): Promise => { }, set: (): void => {}, } - ExtensionHost.set(extensionHostRpc as any) FileSystemWorker.set(fileSystemWorkerRpc as any) const result = await loadFileContents('data://before-content', '/tmp/after.txt') - expect(extensionHostInvocations).toEqual([]) expect(fileSystemWorkerInvocations).toEqual([['FileSystem.readFile', 'file:///tmp/after.txt']]) expect(result).toEqual({ contentLeft: 'before-content', @@ -62,13 +41,6 @@ test('loadFileContents captures per-side read errors', async (): Promise = Object.defineProperty(error, 'stack', { value: 'Error: file not found\n at read missing file', }) - const extensionHostRpc = { - dispose: (): void => {}, - invoke: async (): Promise => { - throw new Error('should not invoke extension host') - }, - set: (): void => {}, - } const fileSystemWorkerRpc = { dispose: (): void => {}, invoke: async (method: string, ...params: readonly unknown[]): Promise => { @@ -83,7 +55,6 @@ test('loadFileContents captures per-side read errors', async (): Promise = }, set: (): void => {}, } - ExtensionHost.set(extensionHostRpc as any) FileSystemWorker.set(fileSystemWorkerRpc as any) const result = await loadFileContents('data://before-content', '/tmp/after.txt') diff --git a/packages/diff-view/test/ReadFile.test.ts b/packages/diff-view/test/ReadFile.test.ts index 6b1530ac..3d6ad1fc 100644 --- a/packages/diff-view/test/ReadFile.test.ts +++ b/packages/diff-view/test/ReadFile.test.ts @@ -1,49 +1,20 @@ import { expect, test } from '@jest/globals' -import { ExtensionHost, FileSystemWorker, RendererWorker } from '@lvce-editor/rpc-registry' +import { ExtensionManagementWorker, FileSystemWorker, RendererWorker } from '@lvce-editor/rpc-registry' import { readFile } from '../src/parts/ReadFile/ReadFile.ts' test('readFile returns empty content for untitled uri', async (): Promise => { - const mockRpc = { - dispose: (): void => {}, - invocations: [] as readonly unknown[][], - invoke: async (method: string, ...params: readonly unknown[]): Promise => { - mockRpc.invocations = [...mockRpc.invocations, [method, ...params]] - throw new Error('should not invoke rpc') - }, - set: (): void => {}, - } - ExtensionHost.set(mockRpc as any) - const result = await readFile('untitled://Untitled-1') expect(result).toBe('') - expect(mockRpc.invocations).toEqual([]) }) test('readFile returns inline content for data uri', async (): Promise => { - const mockRpc = { - dispose: (): void => {}, - invocations: [] as readonly unknown[][], - invoke: async (method: string, ...params: readonly unknown[]): Promise => { - mockRpc.invocations = [...mockRpc.invocations, [method, ...params]] - throw new Error(`unexpected method: ${method} ${params.join(' ')}`) - }, - set: (): void => {}, - } - ExtensionHost.set(mockRpc as any) - const result = await readFile('data://before-content') expect(result).toBe('before-content') - expect(mockRpc.invocations).toEqual([]) }) test('readFile reads file content through file system worker', async (): Promise => { - const extensionHostRpc = ExtensionHost.registerMockRpc({ - 'ExtensionHostFileSystem.readFile': async (): Promise => { - throw new Error('should not call extension host for file uris') - }, - }) const fileSystemWorkerRpc = FileSystemWorker.registerMockRpc({ 'FileSystem.readFile': async (uri: string): Promise => { if (uri !== 'file:///tmp/after.txt') { @@ -56,15 +27,14 @@ test('readFile reads file content through file system worker', async (): Promise const result = await readFile('/tmp/after.txt') expect(result).toBe('after-content') - expect(extensionHostRpc.invocations).toEqual([]) expect(fileSystemWorkerRpc.invocations).toEqual([['FileSystem.readFile', 'file:///tmp/after.txt']]) }) -test('readFile reads memfs content through the memory file system command', async (): Promise => { - const extensionHostRpc = ExtensionHost.registerMockRpc({ - 'FileSystemMemory.readFile': async (path: string): Promise => { - if (path !== '/workspace/file.txt') { - throw new Error(`unexpected params: ${path}`) +test('readFile reads memfs content through the renderer file system', async (): Promise => { + const rendererWorkerRpc = RendererWorker.registerMockRpc({ + 'FileSystem.readFile': async (uri: string): Promise => { + if (uri !== 'memfs:///workspace/file.txt') { + throw new Error(`unexpected params: ${uri}`) } return 'memfs-content' }, @@ -73,15 +43,10 @@ test('readFile reads memfs content through the memory file system command', asyn const result = await readFile('memfs:///workspace/file.txt') expect(result).toBe('memfs-content') - expect(extensionHostRpc.invocations).toEqual([['FileSystemMemory.readFile', '/workspace/file.txt']]) + expect(rendererWorkerRpc.invocations).toEqual([['FileSystem.readFile', 'memfs:///workspace/file.txt']]) }) test('readFile reads fetch content through renderer file system', async (): Promise => { - const extensionHostRpc = ExtensionHost.registerMockRpc({ - 'ExtensionHostFileSystem.readFile': async (): Promise => { - throw new Error('should not call extension host file system for fetch uris') - }, - }) const rendererWorkerRpc = RendererWorker.registerMockRpc({ 'FileSystem.readFile': async (uri: string): Promise => { if (uri !== 'fetch:///playground/package-lock.json') { @@ -94,22 +59,31 @@ test('readFile reads fetch content through renderer file system', async (): Prom const result = await readFile('fetch:///playground/package-lock.json') expect(result).toBe('fetch-content') - expect(extensionHostRpc.invocations).toEqual([]) expect(rendererWorkerRpc.invocations).toEqual([['FileSystem.readFile', 'fetch:///playground/package-lock.json']]) }) -test('readFile reads non-file protocols through extension host', async (): Promise => { - const extensionHostRpc = ExtensionHost.registerMockRpc({ - 'ExtensionHostFileSystem.readFile': async (protocol: string, path: string): Promise => { - if (protocol !== 'git' || path !== 'HEAD~1:src/file.ts') { - throw new Error(`unexpected params: ${protocol} ${path}`) +test('readFile reads extension protocols through extension management worker', async (): Promise => { + const extensionManagementWorkerRpc = ExtensionManagementWorker.registerMockRpc({ + 'Extensions.executeFileSystemProviderReadFile': async (protocol: string, uri: string): Promise => { + if (protocol !== 'git-file-before' || uri !== 'git-file-before://file:///workspace/src/file.ts') { + throw new Error(`unexpected params: ${protocol} ${uri}`) } - return 'git-content' + return { found: true, result: 'git-content' } }, }) - const result = await readFile('git://HEAD~1:src/file.ts') + const result = await readFile('git-file-before://file:///workspace/src/file.ts') expect(result).toBe('git-content') - expect(extensionHostRpc.invocations).toEqual([['ExtensionHostFileSystem.readFile', 'git', 'HEAD~1:src/file.ts']]) + expect(extensionManagementWorkerRpc.invocations).toEqual([ + ['Extensions.executeFileSystemProviderReadFile', 'git-file-before', 'git-file-before://file:///workspace/src/file.ts'], + ]) +}) + +test('readFile rejects when no isolated extension provides the protocol', async (): Promise => { + ExtensionManagementWorker.registerMockRpc({ + 'Extensions.executeFileSystemProviderReadFile': async (): Promise => ({ found: false }), + }) + + await expect(readFile('missing:///workspace/file.txt')).rejects.toThrow('no file system provider found for missing') })