Skip to content

Commit

Permalink
feat: allow multiple runtime preview
Browse files Browse the repository at this point in the history
  • Loading branch information
ambar committed Apr 6, 2024
1 parent f0ffbbd commit fb1fdf7
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 67 deletions.
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -77,7 +77,7 @@
},
{
"command": "liveCode.choosePreviewToTheSide",
"when": "editorLangId in liveCode.supportedLanguageIds",
"when": "editorLangId in liveCode.supportedLanguageIds && config.liveCode.showChoosePreviewToTheSideInEditorTitle",
"group": "navigation"
},
{
Expand All @@ -99,6 +99,11 @@
"type": "object",
"title": "Live Code",
"properties": {
"liveCode.showChoosePreviewToTheSideInEditorTitle": {
"type": "boolean",
"default": false,
"description": "Wether to show the 'Choose Preview to the Side' button in the editor title"
},
"liveCode.renderJSX": {
"type": "boolean",
"default": true,
Expand Down
135 changes: 80 additions & 55 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -37,26 +38,35 @@ const jsLanguageIds = [
'typescriptreact',
]

const runtimeTitleMap: Record<Runtime, string> = {
const runtimeNameMap: Record<Runtime, string> = {
node: 'Node.js',
browser: 'browser',
}

// https://icon-sets.iconify.design/devicon/
// https://icon-sets.iconify.design/devicon-plain/
const runtimeIconMap: Record<Runtime, string> = {
browser: 'images/browser.svg',
node: 'images/node.svg',
}

const distDir = __dirname
// TODO: change to fsPath map
const documentPanelMap = new Map<vscode.TextDocument, vscode.WebviewPanel>()
type RuntimePanelMap = Map<Runtime, vscode.WebviewPanel>
const documentRuntimePanelMap = new Map<vscode.TextDocument, RuntimePanelMap>()
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<nodeVM.RunOpts['workerRef'], void>
workerRef: {current: Worker | null}
}
>()
let installPromise: ReturnType<typeof install>
Expand All @@ -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')},
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand All @@ -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',
Expand All @@ -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}) => {
Expand Down Expand Up @@ -225,16 +243,18 @@ function openPreviewToSide(runtime: Runtime) {
})
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) =>
Expand All @@ -247,17 +267,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
Expand Down Expand Up @@ -325,25 +352,18 @@ function debounce<T extends AnyFunction<void>>(fn: T, wait: number) {
} as T
}

// https://icon-sets.iconify.design/devicon/
// https://icon-sets.iconify.design/devicon-plain/
const iconMap: Record<Runtime, string> = {
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]
)
}

Expand Down Expand Up @@ -385,11 +405,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
)
Expand Down
6 changes: 1 addition & 5 deletions src/extension/getWebviewContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,7 @@ export default function getWebviewContent(appConfig: AppConfig) {
</head>
<body>
<div id="splash">
<l-dot-wave
size="47"
speed="1"
color="#09f"
></l-dot-wave>
<l-ripples size="45" speed="2" color="#09f"></l-ripples>
</div>
<div id="app"></div>
<script type="module" src="${appConfig.entries.preview}"></script>
Expand Down
2 changes: 1 addition & 1 deletion src/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ const App = () => {
justifyContent: 'center',
}}
>
<l-dot-wave size="47" speed="1" color="#09f" />
<l-ripples size="45" speed="2" color="#09f" />
</div>
)
}
Expand Down
7 changes: 4 additions & 3 deletions src/preview/head.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// only import the components are used in the preview

// The bundle size is too large
// search for `<vscode-`
// export * from '@vscode/webview-ui-toolkit/dist/progress-ring'

// search for `<l-dot-`
import {dotWave} from 'ldrs'
// search for `<l-`
import {ripples} from 'ldrs'

dotWave.register()
ripples.register()
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "ESNext",
"target": "ES2020",
"outDir": "out",
"lib": ["ES2020", "DOM"],
Expand Down

0 comments on commit fb1fdf7

Please sign in to comment.