From a87c3f667c2d361b111b6f6d5577d25d59d8ef0a Mon Sep 17 00:00:00 2001 From: ambar Date: Fri, 5 Apr 2024 17:23:33 +0800 Subject: [PATCH] feat: allow multiple runtime preview for same document --- package.json | 27 ++++-- src/extension.ts | 149 ++++++++++++++++------------- src/extension/getWebviewContent.ts | 6 +- src/preview.tsx | 6 +- src/preview/head.ts | 7 +- tsconfig.json | 2 +- 6 files changed, 111 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index bfe5753..cfd2d43 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "commands": [ { "command": "liveCode.choosePreviewToTheSide", - "title": "Choose A Runtime Preview to the Side", + "title": "Choose Preview to the Side", "category": "Live Code", "icon": "images/logo-plain.svg" }, @@ -67,22 +67,22 @@ "editor/title": [ { "command": "liveCode.openBrowserPreviewToSide", - "when": "editorLangId in liveCode.supportedLanguageIds", + "when": "editorLangId in liveCode.supportedLanguageIds && config.liveCode.showBrowserPreviewInTitle", "group": "navigation" }, { "command": "liveCode.openNodePreviewToSide", - "when": "editorLangId in liveCode.supportedLanguageIds", + "when": "editorLangId in liveCode.supportedLanguageIds && config.liveCode.showNodePreviewInTitle", "group": "navigation" }, { "command": "liveCode.choosePreviewToTheSide", - "when": "editorLangId in liveCode.supportedLanguageIds", + "when": "editorLangId in liveCode.supportedLanguageIds && config.liveCode.showChoosePreviewInTitle", "group": "navigation" }, { "command": "liveCode.changeCurrentRuntimeOfPreview", - "when": "liveCode.isPreviewFocus", + "when": "activeWebviewPanelId == 'liveCode.preview'", "group": "navigation" } ] @@ -92,13 +92,28 @@ "command": "liveCode.choosePreviewToTheSide", "key": "ctrl+k l", "mac": "cmd+k l", - "when": "editorLangId in liveCode.supportedLanguageIds && !liveCode.isPreviewFocus" + "when": "editorLangId in liveCode.supportedLanguageIds" } ], "configuration": { "type": "object", "title": "Live Code", "properties": { + "liveCode.showChoosePreviewInTitle": { + "type": "boolean", + "default": false, + "description": "Wether to show the 'Choose Preview to the Side' button in the editor title" + }, + "liveCode.showBrowserPreviewInTitle": { + "type": "boolean", + "default": true, + "description": "Wether to show the 'Open Browser Preview to the Side' button in the editor title" + }, + "liveCode.showNodePreviewInTitle": { + "type": "boolean", + "default": true, + "description": "Wether to show the 'Open Node.js Preview to the Side' button in the editor title" + }, "liveCode.renderJSX": { "type": "boolean", "default": true, diff --git a/src/extension.ts b/src/extension.ts index 5d7320a..ddc0691 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,6 +11,7 @@ import * as nodeVM from './sandbox/nodeVM' import type {ExpContext} from './sandbox/types' import timeMark from './utils/timeMark' import {of} from './utils/promise' +import type {Worker} from 'worker_threads' const NAME = 'Live Code' const output = vscode.window.createOutputChannel(NAME) @@ -37,26 +38,35 @@ const jsLanguageIds = [ 'typescriptreact', ] -const runtimeTitleMap: Record = { +const runtimeNameMap: Record = { node: 'Node.js', browser: 'browser', } +// https://icon-sets.iconify.design/devicon/ +// https://icon-sets.iconify.design/devicon-plain/ +const runtimeIconMap: Record = { + browser: 'images/browser.svg', + node: 'images/node.svg', +} + const distDir = __dirname -// TODO: change to fsPath map -const documentPanelMap = new Map() +type RuntimePanelMap = Map +const documentRuntimePanelMap = new Map() +function* iteratePanel() { + for (const [doc, panelMap] of documentRuntimePanelMap) { + for (const [runtime, panel] of panelMap) { + yield [panel, doc, runtime] as const + } + } +} + // config send to preview -const panelConfigMap = new WeakMap< +const panelStateMap = new WeakMap< vscode.WebviewPanel, { currentRuntime: Runtime - } ->() -// instance data -const panelDataMap = new WeakMap< - vscode.WebviewPanel, - { - workerRef: Exclude + workerRef: {current: Worker | null} } >() let installPromise: ReturnType @@ -77,7 +87,7 @@ export function activate(context: vscode.ExtensionContext) { ) vscode.workspace.onDidChangeConfiguration(() => { - for (const [, panel] of documentPanelMap) { + for (const [panel] of iteratePanel()) { void panel.webview.postMessage({ type: 'configChange', data: {...vscode.workspace.getConfiguration('liveCode')}, @@ -92,9 +102,7 @@ export function activate(context: vscode.ExtensionContext) { ) vscode.workspace.onDidCloseTextDocument((doc) => { - if (!documentPanelMap.get(doc)) { - documentPanelMap.delete(doc) - } + documentRuntimePanelMap.delete(doc) }) context.subscriptions.push( @@ -109,10 +117,8 @@ export function activate(context: vscode.ExtensionContext) { if (!doc) { return } - const panel = documentPanelMap.get(doc) - if (panel) { + for (const [, panel] of documentRuntimePanelMap.get(doc) ?? []) { setWebviewContent(panel.webview) - return } }), vscode.commands.registerCommand( @@ -155,20 +161,26 @@ async function showRuntimePick(currentRuntime?: Runtime) { } async function choosePreviewToTheSide() { - const entry = [...documentPanelMap].find(([, x]) => x.active) - if (!entry) { + let activeEntry: [vscode.TextDocument, vscode.WebviewPanel] | void = undefined + for (const [panel, doc] of iteratePanel()) { + if (panel.active) { + activeEntry = [doc, panel] + break + } + } + if (!activeEntry) { const runtime = await showRuntimePick() if (runtime) { openPreviewToSide(runtime) } return } - const [document, panel] = entry - const {currentRuntime} = panelConfigMap.get(panel)! + const [document, panel] = activeEntry + const {currentRuntime} = panelStateMap.get(panel)! const runtime = await showRuntimePick(currentRuntime) if (runtime) { - panelConfigMap.set(panel, { - ...panelConfigMap.get(panel), + panelStateMap.set(panel, { + ...panelStateMap.get(panel), currentRuntime: runtime, }) void processDocument(document) @@ -180,10 +192,11 @@ function openPreviewToSide(runtime: Runtime) { if (!doc) { return } - const existingPanel = documentPanelMap.get(doc) - if (existingPanel) { - existingPanel.reveal() - return + for (const [panel, d, rt] of iteratePanel()) { + if (d === doc && rt === runtime) { + panel.reveal() + return + } } const panel = vscode.window.createWebviewPanel( 'liveCode.preview', @@ -193,9 +206,14 @@ function openPreviewToSide(runtime: Runtime) { enableScripts: true, } ) - documentPanelMap.set(doc, panel) - panelConfigMap.set(panel, {currentRuntime: runtime}) - panelDataMap.set(panel, {workerRef: {current: null}}) + if (!documentRuntimePanelMap.get(doc)) { + documentRuntimePanelMap.set(doc, new Map()) + } + documentRuntimePanelMap.get(doc)?.set(runtime, panel) + panelStateMap.set(panel, { + currentRuntime: runtime, + workerRef: {current: null}, + }) setPanelTitleAndIcon(panel, doc) setWebviewContent(panel.webview) panel.webview.onDidReceiveMessage((e: {type: string; data: unknown}) => { @@ -212,29 +230,19 @@ function openPreviewToSide(runtime: Runtime) { log(e.data) } }) - const setIsPreviewFocus = (value: boolean) => { - void vscode.commands.executeCommand( - 'setContext', - 'liveCode.isPreviewFocus', - value - ) - } - setIsPreviewFocus(true) - panel.onDidChangeViewState((e) => { - setIsPreviewFocus(e.webviewPanel.active) - }) panel.onDidDispose(() => { - setIsPreviewFocus(false) - documentPanelMap.delete(doc) - panelConfigMap.delete(panel) - panelDataMap.get(panel)?.workerRef?.current?.terminate() - panelDataMap.delete(panel) + const toRemove = [...iteratePanel()].find(([p]) => p === panel) + if (toRemove) { + documentRuntimePanelMap.get(toRemove[1])?.delete(toRemove[2]) + } + panelStateMap.get(panel)?.workerRef?.current?.terminate() + panelStateMap.delete(panel) }) } export function deactivate() { itsContext = null - documentPanelMap.clear() + documentRuntimePanelMap.clear() } const prettyPrint = (obj: unknown) => @@ -247,17 +255,24 @@ async function processDocument( document: vscode.TextDocument, shouldReload = false ) { - const panel = documentPanelMap.get(document) - if (!panel) { - return - } + const panels = [...(documentRuntimePanelMap.get(document)?.values() ?? [])] + await Promise.allSettled( + panels.map((panel) => processDocumentPanel(document, panel, shouldReload)) + ) +} + +async function processDocumentPanel( + document: vscode.TextDocument, + panel: vscode.WebviewPanel, + shouldReload = false +) { await installPromise // terminate prev worker for each run - const workerRef = panelDataMap.get(panel)?.workerRef + const workerRef = panelStateMap.get(panel)?.workerRef await workerRef?.current?.terminate() - const {currentRuntime} = panelConfigMap.get(panel)! + const {currentRuntime} = panelStateMap.get(panel)! const isBrowser = currentRuntime === 'browser' setPanelTitleAndIcon(panel, document) let error: unknown @@ -302,7 +317,7 @@ async function processDocument( type: shouldReload ? 'codeReload' : 'code', // data should be serialized data: { - platform: currentRuntime, + runtime: currentRuntime, config: {...vscode.workspace.getConfiguration('liveCode')}, result, logs, @@ -325,25 +340,18 @@ function debounce>(fn: T, wait: number) { } as T } -// https://icon-sets.iconify.design/devicon/ -// https://icon-sets.iconify.design/devicon-plain/ -const iconMap: Record = { - browser: 'images/browser.svg', - node: 'images/node.svg', -} - function setPanelTitleAndIcon( panel: vscode.WebviewPanel, document: vscode.TextDocument ) { - const {currentRuntime} = panelConfigMap.get(panel)! + const {currentRuntime} = panelStateMap.get(panel)! panel.title = `Preview` + (document ? ' ' + path.basename(document.uri.fsPath) : '') + - ` in ${runtimeTitleMap[currentRuntime]}` + ` in ${runtimeNameMap[currentRuntime]}` panel.iconPath = vscode.Uri.joinPath( itsContext!.extensionUri, - iconMap[currentRuntime] + runtimeIconMap[currentRuntime] ) } @@ -385,11 +393,16 @@ function bundleDocument( } function revealSourceLine(loc: {line: number; column: number}) { - const entry = [...documentPanelMap].find(([, x]) => x.active) - if (!entry) { + let document: vscode.TextDocument | null = null + for (const [panel, doc] of iteratePanel()) { + if (panel.active) { + document = doc + break + } + } + if (!document) { return } - const [document] = entry const editor = vscode.window.visibleTextEditors.find( (x) => x.document === document ) diff --git a/src/extension/getWebviewContent.ts b/src/extension/getWebviewContent.ts index 27902e1..10fbd4f 100644 --- a/src/extension/getWebviewContent.ts +++ b/src/extension/getWebviewContent.ts @@ -41,11 +41,7 @@ export default function getWebviewContent(appConfig: AppConfig) {
- +
diff --git a/src/preview.tsx b/src/preview.tsx index 069e43a..5c6c91a 100644 --- a/src/preview.tsx +++ b/src/preview.tsx @@ -97,7 +97,7 @@ declare global { } type Data = { - platform: Runtime + runtime: Runtime error?: unknown code?: string css?: string @@ -213,7 +213,7 @@ const App = () => { const [isLoading, setIsLoading] = useState(true) const previousState = vscode.getState() as {data: Data} const [data, setData] = useState(previousState?.data ?? {logs: []}) - const isBrowser = data.platform === 'browser' + const isBrowser = data.runtime === 'browser' const [[error, values], browserLogs] = useLiveCode( isBrowser ? data.code : void 0, isBrowser ? data.css : void 0 @@ -260,7 +260,7 @@ const App = () => { justifyContent: 'center', }} > - + ) } diff --git a/src/preview/head.ts b/src/preview/head.ts index b5d9e49..ad6cbab 100644 --- a/src/preview/head.ts +++ b/src/preview/head.ts @@ -1,9 +1,10 @@ // only import the components are used in the preview +// The bundle size is too large // search for `