From d59ba06738eaa5771e114e645f9380002e961ff9 Mon Sep 17 00:00:00 2001 From: sharon wang Date: Mon, 16 Dec 2024 18:19:39 -0500 Subject: [PATCH 01/10] inject webview-events.js script into preview html --- .../resources/scripts_preview.html | 48 +++ .../resources/webview-events.js | 389 ++++++++++++++++++ extensions/positron-proxy/src/htmlProxy.ts | 39 +- .../positron-proxy/src/positronProxy.ts | 86 ++-- extensions/positron-proxy/src/types.ts | 72 ++++ .../browser/positronPreviewServiceImpl.ts | 2 +- .../browser/previewOverlayWebview.ts | 67 ++- .../webview/browser/pre/webview-events.js | 3 + .../contrib/webview/browser/webviewElement.ts | 4 + 9 files changed, 628 insertions(+), 82 deletions(-) create mode 100644 extensions/positron-proxy/resources/scripts_preview.html create mode 100644 extensions/positron-proxy/resources/webview-events.js create mode 100644 extensions/positron-proxy/src/types.ts diff --git a/extensions/positron-proxy/resources/scripts_preview.html b/extensions/positron-proxy/resources/scripts_preview.html new file mode 100644 index 00000000000..c2b3e12712d --- /dev/null +++ b/extensions/positron-proxy/resources/scripts_preview.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + diff --git a/extensions/positron-proxy/resources/webview-events.js b/extensions/positron-proxy/resources/webview-events.js new file mode 100644 index 00000000000..8e637dc27f4 --- /dev/null +++ b/extensions/positron-proxy/resources/webview-events.js @@ -0,0 +1,389 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * This file is derived from the event handlers in the `index.html` file next + * door. Its job is to absorb events from the inner iframe and forward them to + * the host as window messages. + * + * This allows the host to dispatach events that can't be handled natively in + * the frame on Electron, such as copy/cut/paste commands and context menus. + * + * The other side of the communication is in `index-external.html`; it receives + * messages sent from this file and forwards them to the webview host, where + * they are processed and dispatched. + */ + +/** + * Send a message to the host; this simulates the `hostMessaging` object in the + * webview. + */ +const hostMessaging = { + postMessage: (type, data) => { + // OK to be promiscuous here, as this script is only used in an Electron + // webview context we already control. + window.parent.postMessage({ + channel: type, + data: data, + }, '*'); + } +}; + +/** + * Handles a message sent from the host. + */ +const handlePostMessage = (event) => { + // Execute a command in the document if requested + if (event.data.channel === 'execCommand') { + const command = event.data.data; + // Check for special Positron commands. + if (command === 'navigate-back') { + window.history.back(); + return; + } else if (command === 'navigate-forward') { + window.history.forward(); + return; + } else if (command === 'reload-window') { + window.location.reload(); + return; + } + + // Otherwise, execute the command in the document. + document.execCommand(command); + } +}; + +/** + * @param {MouseEvent} event + */ +const handleAuxClick = (event) => { + // Prevent middle clicks opening a broken link in the browser + if (!event?.view?.document) { + return; + } + + if (event.button === 1) { + for (const pathElement of event.composedPath()) { + /** @type {any} */ + const node = pathElement; + if ( + node.tagName && + node.tagName.toLowerCase() === "a" && + node.href + ) { + event.preventDefault(); + return; + } + } + } +}; + +/** + * @param {KeyboardEvent} e + */ +const handleInnerKeydown = (e) => { + // If the keypress would trigger a browser event, such as copy or paste, + // make sure we block the browser from dispatching it. Instead VS Code + // handles these events and will dispatch a copy/paste back to the webview + // if needed + if (isUndoRedo(e) || isPrint(e) || isFindEvent(e) || isSaveEvent(e) || isCopyPasteOrCut(e)) { + e.preventDefault(); + } + hostMessaging.postMessage('did-keydown', { + key: e.key, + keyCode: e.keyCode, + code: e.code, + shiftKey: e.shiftKey, + altKey: e.altKey, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey, + repeat: e.repeat, + }); +}; +/** + * @param {KeyboardEvent} e + */ +const handleInnerKeyup = (e) => { + hostMessaging.postMessage("did-keyup", { + key: e.key, + keyCode: e.keyCode, + code: e.code, + shiftKey: e.shiftKey, + altKey: e.altKey, + ctrlKey: e.ctrlKey, + metaKey: e.metaKey, + repeat: e.repeat, + }); +}; + +/** + * @param {KeyboardEvent} e + * @return {boolean} + */ +function isCopyPasteOrCut(e) { + const hasMeta = e.ctrlKey || e.metaKey; + // 45: keyCode of "Insert" + const shiftInsert = e.shiftKey && e.keyCode === 45; + // 67, 86, 88: keyCode of "C", "V", "X" + return (hasMeta && [67, 86, 88].includes(e.keyCode)) || shiftInsert; +} + +/** + * @param {KeyboardEvent} e + * @return {boolean} + */ +function isUndoRedo(e) { + const hasMeta = e.ctrlKey || e.metaKey; + // 90, 89: keyCode of "Z", "Y" + return hasMeta && [90, 89].includes(e.keyCode); +} + +/** + * @param {KeyboardEvent} e + * @return {boolean} + */ +function isPrint(e) { + const hasMeta = e.ctrlKey || e.metaKey; + // 80: keyCode of "P" + return hasMeta && e.keyCode === 80; +} + +/** + * @param {KeyboardEvent} e + * @return {boolean} + */ +function isFindEvent(e) { + const hasMeta = e.ctrlKey || e.metaKey; + // 70: keyCode of "F" + return hasMeta && e.keyCode === 70; +} + +let isHandlingScroll = false; + +/** + * @param {WheelEvent} event + */ +const handleWheel = (event) => { + if (isHandlingScroll) { + return; + } + + hostMessaging.postMessage("did-scroll-wheel", { + deltaMode: event.deltaMode, + deltaX: event.deltaX, + deltaY: event.deltaY, + deltaZ: event.deltaZ, + detail: event.detail, + type: event.type, + }); +}; + +/** + * @param {Event} event + */ +const handleInnerScroll = (event) => { + if (isHandlingScroll) { + return; + } + + const target = /** @type {HTMLDocument | null} */ (event.target); + const currentTarget = /** @type {Window | null} */ ( + event.currentTarget + ); + if (!currentTarget || !target?.body) { + return; + } + + const progress = currentTarget.scrollY / target.body.clientHeight; + if (isNaN(progress)) { + return; + } + + isHandlingScroll = true; + window.requestAnimationFrame(() => { + try { + hostMessaging.postMessage("did-scroll", { + scrollYPercentage: progress, + }); + } catch (e) { + // noop + } + isHandlingScroll = false; + }); +}; + +function handleInnerDragStartEvent(/** @type {DragEvent} */ e) { + if (e.defaultPrevented) { + // Extension code has already handled this event + return; + } + + if (!e.dataTransfer || e.shiftKey) { + return; + } + + // Only handle drags from outside editor for now + if ( + e.dataTransfer.items.length && + Array.prototype.every.call( + e.dataTransfer.items, + (item) => item.kind === "file", + ) + ) { + hostMessaging.postMessage("drag-start", undefined); + } +} +/** + * @param {KeyboardEvent} e + * @return {boolean} + */ +function isSaveEvent(e) { + const hasMeta = e.ctrlKey || e.metaKey; + // 83: keyCode of "S" + return hasMeta && e.keyCode === 83; +} + +/** + * @param {KeyboardEvent} e + * @return {boolean} + */ +function isCloseTab(e) { + const hasMeta = e.ctrlKey || e.metaKey; + // 87: keyCode of "W" + return hasMeta && e.keyCode === 87; +} + +/** + * @param {KeyboardEvent} e + * @return {boolean} + */ +function isNewWindow(e) { + const hasMeta = e.ctrlKey || e.metaKey; + // 78: keyCode of "N" + return hasMeta && e.keyCode === 78; +} + +/** + * @param {KeyboardEvent} e + * @return {boolean} + */ +function isHelp(e) { + // 112: keyCode of "F1" + return e.keyCode === 112; +} + +/** + * @param {KeyboardEvent} e + * @return {boolean} + */ +function isRefresh(e) { + // 116: keyCode of "F5" + return e.keyCode === 116; +} + +window.addEventListener('message', handlePostMessage); +window.addEventListener('dragenter', handleInnerDragStartEvent); +window.addEventListener('dragover', handleInnerDragStartEvent); +window.addEventListener('scroll', handleInnerScroll); +window.addEventListener('wheel', handleWheel); +window.addEventListener('auxclick', handleAuxClick); +window.addEventListener('keydown', handleInnerKeydown); +window.addEventListener('keyup', handleInnerKeyup); +window.addEventListener('contextmenu', (e) => { + if (e.defaultPrevented) { + // Extension code has already handled this event + return; + } + + e.preventDefault(); + + /** @type { Record} */ + let context = {}; + + /** @type {HTMLElement | null} */ + let el = e.target; + while (true) { + if (!el) { + break; + } + + // Search self/ancestors for the closest context data attribute + el = el.closest("[data-vscode-context]"); + if (!el) { + break; + } + + try { + context = { + ...JSON.parse(el.dataset.vscodeContext), + ...context, + }; + } catch (e) { + console.error( + `Error parsing 'data-vscode-context' as json`, + el, + e, + ); + } + + el = el.parentElement; + } + + hostMessaging.postMessage('did-context-menu', { + clientX: e.clientX, + clientY: e.clientY, + context: context, + }); +}); + +// Ask Positron to open a link instead of handling it internally +function openLinkInHost(link) { + link.addEventListener('click', function (event) { + console.log('########openLinkInHost:', link); + hostMessaging.postMessage('did-click-link', { uri: link.href }); + event.preventDefault(); + return false; + }); +} + +// When the window loads, look for all links and add a click handler to each +// external link (i.e. links that point to a different origin) that will ask +// Positron to open them instead of handling them internally. +window.addEventListener('load', () => { + const links = document.getElementsByTagName('a'); + const origin = window.location.origin; + for (let i = 0; i < links.length; i++) { + const link = links[i]; + if (link.href && !link.href.startsWith(origin)) { + openLinkInHost(link); + } + } + + // Notify the host that the webview has loaded its content + console.log('########did-load-window:', document.title); + hostMessaging.postMessage('did-load-window', { + title: document.title, + }); +}); + +// Override the prompt function to return the default value or 'Untitled' if one isnt provided. +// This is needed because the prompt function is not supported in webviews and the prompt function +// is commonly used by libraries like bokeh to provide names for files to save. The main file save +// dialog that positron shows will already provide the ability to change the file name so we're +// just providing a default value here. +window.prompt = (message, _default) => { + return _default ?? 'Untitled'; +}; + +// Override the window.open function to send a message to the host to open the link instead. +// Save the old window.open function so we can call it after sending the message in case there's +// some other behavior that was depended upon that we're not aware of. +const oldOpen = window.open; +window.open = (url, target, features) => { + const uri = url instanceof URL ? url.href : url; + console.log('########Opening link:', uri); + hostMessaging.postMessage('did-click-link', { uri }); + return oldOpen(uri, target, features); +}; diff --git a/extensions/positron-proxy/src/htmlProxy.ts b/extensions/positron-proxy/src/htmlProxy.ts index 5faa19168fe..055be572f3b 100644 --- a/extensions/positron-proxy/src/htmlProxy.ts +++ b/extensions/positron-proxy/src/htmlProxy.ts @@ -3,13 +3,14 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import express from 'express'; import path = require('path'); import fs = require('fs'); import { Disposable, Uri } from 'vscode'; import { PromiseHandles } from './util'; -import { isAddressInfo } from './positronProxy'; +import { isAddressInfo, ProxyServerHtml } from './types'; /** * HtmlProxyServer class. @@ -44,7 +45,10 @@ export class HtmlProxyServer implements Disposable { * to the URL. * @returns A URL that serves the content at the specified path. */ - public async createHtmlProxy(targetPath: string): Promise { + public async createHtmlProxy( + targetPath: string, + htmlConfig?: ProxyServerHtml + ): Promise { // Wait for the server to be ready. await this._ready.promise; @@ -82,7 +86,36 @@ export class HtmlProxyServer implements Disposable { } // Create a new path entry. - this._app.use(`/${serverPath}`, express.static(targetPath)); + if (vscode.env.uiKind === vscode.UIKind.Web) { + this._app.use(`/${serverPath}`, async (req, res, next) => { + const filePath = path.join(targetPath, req.path); + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + let content = fs.readFileSync(filePath, 'utf8'); + // If there is an HTML configuration, use it to rewrite the content. + if (htmlConfig) { + // Inject the preview style defaults for unstyled preview documents. + content = content.replace( + '', + `\n + ${htmlConfig.styleDefaults + '\n' || ''}` + ); + + // Inject the preview style overrides and script. + content = content.replace( + '', + `${htmlConfig.styleOverrides + '\n' || ''} + ${htmlConfig.script + '\n' || ''} + ` + ); + } + res.send(content); + } else { + next(); + } + }); + } else { + this._app.use(`/${serverPath}`, express.static(targetPath)); + } const address = this._server.address(); if (!isAddressInfo(address)) { throw new Error(`Server address is not available; cannot serve ${targetPath}`); diff --git a/extensions/positron-proxy/src/positronProxy.ts b/extensions/positron-proxy/src/positronProxy.ts index a02809209f5..d27d08dbf14 100644 --- a/extensions/positron-proxy/src/positronProxy.ts +++ b/extensions/positron-proxy/src/positronProxy.ts @@ -7,13 +7,14 @@ import * as vscode from 'vscode'; import fs = require('fs'); import path = require('path'); import express from 'express'; -import { AddressInfo, Server } from 'net'; +import { Server } from 'net'; import { log, ProxyServerStyles } from './extension'; // eslint-disable-next-line no-duplicate-imports import { Disposable, ExtensionContext } from 'vscode'; import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware'; import { HtmlProxyServer } from './htmlProxy'; import { htmlContentRewriter, rewriteUrlsWithProxyPath } from './util'; +import { ContentRewriter, isAddressInfo, MaybeAddressInfo, PendingProxyServer, ProxyServerHtmlConfig, ProxyServerType } from './types'; /** * Constants. @@ -44,43 +45,6 @@ const getStyleElement = (script: string, id: string) => const getScriptElement = (script: string, id: string) => script.match(new RegExp(` - - - `); } diff --git a/src/vs/workbench/contrib/webview/browser/pre/webview-events.js b/src/vs/workbench/contrib/webview/browser/pre/webview-events.js index d739173f2d8..8e637dc27f4 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/webview-events.js +++ b/src/vs/workbench/contrib/webview/browser/pre/webview-events.js @@ -341,6 +341,7 @@ window.addEventListener('contextmenu', (e) => { // Ask Positron to open a link instead of handling it internally function openLinkInHost(link) { link.addEventListener('click', function (event) { + console.log('########openLinkInHost:', link); hostMessaging.postMessage('did-click-link', { uri: link.href }); event.preventDefault(); return false; @@ -361,6 +362,7 @@ window.addEventListener('load', () => { } // Notify the host that the webview has loaded its content + console.log('########did-load-window:', document.title); hostMessaging.postMessage('did-load-window', { title: document.title, }); @@ -381,6 +383,7 @@ window.prompt = (message, _default) => { const oldOpen = window.open; window.open = (url, target, features) => { const uri = url instanceof URL ? url.href : url; + console.log('########Opening link:', uri); hostMessaging.postMessage('did-click-link', { uri }); return oldOpen(uri, target, features); }; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 8e99cc16e3d..ff09ef1bfde 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -199,6 +199,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this._tunnelService )); + console.log('######## WebviewElement#constructor', initInfo); this._element = this._createElement(initInfo.options, initInfo.contentOptions); this._register(this.on('no-csp-found', () => { @@ -206,6 +207,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD })); this._register(this.on('did-click-link', ({ uri }) => { + console.log('######## WebviewElement#onDidClickLink', uri); this._onDidClickLink.fire(uri); })); @@ -344,6 +346,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } if (evt.frameId.processId === this._frameId.processId && evt.frameId.routingId === this._frameId.routingId) { + console.log('######## WebviewElement#onFrameNavigated', evt); // Insert the `webview-events.js` script into the frame await this.injectJavaScript(); @@ -356,6 +359,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } })); this._register(this.on('did-load-window', (data) => { + console.log('######## WebviewElement#onDidLoadWindow', data); this._onDidLoad.fire(data.title); })); // --- End Positron --- From ff31136542478692b808a5c9d98ba7ed729616d6 Mon Sep 17 00:00:00 2001 From: sharon wang Date: Thu, 19 Dec 2024 14:22:51 -0700 Subject: [PATCH 02/10] pass messages from preview iframe to webview --- .../resources/webview-events.js | 18 +++++++------- .../browser/previewOverlayWebview.ts | 24 ++++++------------- .../webview/browser/pre/webview-events.js | 3 --- .../contrib/webview/browser/webviewElement.ts | 19 +++++++++++---- 4 files changed, 31 insertions(+), 33 deletions(-) diff --git a/extensions/positron-proxy/resources/webview-events.js b/extensions/positron-proxy/resources/webview-events.js index 8e637dc27f4..dddf8cf36e4 100644 --- a/extensions/positron-proxy/resources/webview-events.js +++ b/extensions/positron-proxy/resources/webview-events.js @@ -4,16 +4,19 @@ *--------------------------------------------------------------------------------------------*/ /** - * This file is derived from the event handlers in the `index.html` file next - * door. Its job is to absorb events from the inner iframe and forward them to - * the host as window messages. + * This file is derived from the event handlers in the `index.html` file from + * src/vs/workbench/contrib/webview/browser/pre/index.html. Its job is to absorb + * vents from the inner iframe and forward them to the host as window messages. * - * This allows the host to dispatach events that can't be handled natively in - * the frame on Electron, such as copy/cut/paste commands and context menus. + * This allows the host to dispatch events such as copy/cut/paste commands and + * context menus to the webview. * - * The other side of the communication is in `index-external.html`; it receives + * The other side of the communication is in `index.html`; it receives * messages sent from this file and forwards them to the webview host, where * they are processed and dispatched. + * + * The only difference between this file and the original is this comment block. + * Original: src/vs/workbench/contrib/webview/browser/pre/webview-events.js */ /** @@ -341,7 +344,6 @@ window.addEventListener('contextmenu', (e) => { // Ask Positron to open a link instead of handling it internally function openLinkInHost(link) { link.addEventListener('click', function (event) { - console.log('########openLinkInHost:', link); hostMessaging.postMessage('did-click-link', { uri: link.href }); event.preventDefault(); return false; @@ -362,7 +364,6 @@ window.addEventListener('load', () => { } // Notify the host that the webview has loaded its content - console.log('########did-load-window:', document.title); hostMessaging.postMessage('did-load-window', { title: document.title, }); @@ -383,7 +384,6 @@ window.prompt = (message, _default) => { const oldOpen = window.open; window.open = (url, target, features) => { const uri = url instanceof URL ? url.href : url; - console.log('########Opening link:', uri); hostMessaging.postMessage('did-click-link', { uri }); return oldOpen(uri, target, features); }; diff --git a/src/vs/workbench/contrib/positronPreview/browser/previewOverlayWebview.ts b/src/vs/workbench/contrib/positronPreview/browser/previewOverlayWebview.ts index 5bab187b1f4..2992d249334 100644 --- a/src/vs/workbench/contrib/positronPreview/browser/previewOverlayWebview.ts +++ b/src/vs/workbench/contrib/positronPreview/browser/previewOverlayWebview.ts @@ -66,25 +66,15 @@ export class PreviewOverlayWebview extends Disposable { // Listen for messages window.addEventListener('message', message => { - console.log('########preview iframe wrapper received message:', message); - - // If a message is coming from the preview content window, forward it to the - // preview overlay webview. if (message.source === previewContentWindow) { - console.log('########forwarding to preview overlay webview:', message.data); - // // Cannot read properties of undefined (reading 'postMessage') - // window.parent.postMessage({ - // type: message.channel, - // data: message.data - // }); - // // This doesn't work. Don't think this would send the message to the - // // correct layer anyways? - // vscode.postMessage({ - // type: message.channel, - // data: message.data - // }); + // If a message is coming from the preview content window, forward it to the + // preview overlay webview. + vscode.postMessage({ + __positron_preview_message: true, + ...message.data + }); } else { - console.log('########forwarding to iframe:', message.data); + // Forward messages from the preview overlay webview to the preview content window. previewContentWindow.postMessage(message.data, '*'); } }); diff --git a/src/vs/workbench/contrib/webview/browser/pre/webview-events.js b/src/vs/workbench/contrib/webview/browser/pre/webview-events.js index 8e637dc27f4..d739173f2d8 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/webview-events.js +++ b/src/vs/workbench/contrib/webview/browser/pre/webview-events.js @@ -341,7 +341,6 @@ window.addEventListener('contextmenu', (e) => { // Ask Positron to open a link instead of handling it internally function openLinkInHost(link) { link.addEventListener('click', function (event) { - console.log('########openLinkInHost:', link); hostMessaging.postMessage('did-click-link', { uri: link.href }); event.preventDefault(); return false; @@ -362,7 +361,6 @@ window.addEventListener('load', () => { } // Notify the host that the webview has loaded its content - console.log('########did-load-window:', document.title); hostMessaging.postMessage('did-load-window', { title: document.title, }); @@ -383,7 +381,6 @@ window.prompt = (message, _default) => { const oldOpen = window.open; window.open = (url, target, features) => { const uri = url instanceof URL ? url.href : url; - console.log('########Opening link:', uri); hostMessaging.postMessage('did-click-link', { uri }); return oldOpen(uri, target, features); }; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index ff09ef1bfde..062ad2df838 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -199,7 +199,6 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this._tunnelService )); - console.log('######## WebviewElement#constructor', initInfo); this._element = this._createElement(initInfo.options, initInfo.contentOptions); this._register(this.on('no-csp-found', () => { @@ -207,11 +206,25 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD })); this._register(this.on('did-click-link', ({ uri }) => { - console.log('######## WebviewElement#onDidClickLink', uri); this._onDidClickLink.fire(uri); })); this._register(this.on('onmessage', ({ message, transfer }) => { + // --- Start Positron --- + // If the message has the __positron_preview_message flag, we can unwrap it and send it + // directly to the webview instead of processing it as a generic message. This is similar + // to the onmessage handling in src/vs/workbench/contrib/positronHelp/browser/helpEntry.ts + if (message.__positron_preview_message) { + const handlers = this._messageHandlers.get(message.channel); + if (handlers) { + handlers?.forEach(handler => handler(message.data, message)); + return; + } else { + console.log(`No handlers found for Positron Preview message: '${message.channel}'`); + // Fall through to fire the generic message event + } + } + // --- End Positron --- this._onMessage.fire({ message, transfer }); })); @@ -346,7 +359,6 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } if (evt.frameId.processId === this._frameId.processId && evt.frameId.routingId === this._frameId.routingId) { - console.log('######## WebviewElement#onFrameNavigated', evt); // Insert the `webview-events.js` script into the frame await this.injectJavaScript(); @@ -359,7 +371,6 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } })); this._register(this.on('did-load-window', (data) => { - console.log('######## WebviewElement#onDidLoadWindow', data); this._onDidLoad.fire(data.title); })); // --- End Positron --- From 3eb662a3fc2a71e06b583184df00fa35f6df41f8 Mon Sep 17 00:00:00 2001 From: sharon wang Date: Thu, 19 Dec 2024 14:25:01 -0700 Subject: [PATCH 03/10] avoid "require is not defined" error when previewing HTML file --- src/vs/workbench/api/common/positron/extHostPreviewPanels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/positron/extHostPreviewPanels.ts b/src/vs/workbench/api/common/positron/extHostPreviewPanels.ts index 993574e1ba9..cd4a13520c7 100644 --- a/src/vs/workbench/api/common/positron/extHostPreviewPanels.ts +++ b/src/vs/workbench/api/common/positron/extHostPreviewPanels.ts @@ -13,6 +13,7 @@ import { IExtHostWorkspace } from '../extHostWorkspace.js'; import type * as vscode from 'vscode'; import type * as positron from 'positron'; import * as extHostProtocol from './extHost.positron.protocol.js'; +import * as path from '../../../../base/common/path.js'; type IconPath = URI | { readonly light: URI; readonly dark: URI }; @@ -242,7 +243,6 @@ export class ExtHostPreviewPanels implements extHostProtocol.ExtHostPreviewPanel }; this._proxy.$previewHtml(toExtensionData(extension), handle, htmlpath); const webview = this.webviews.$createNewWebview(handle, options, extension); - const path = require('path'); const title = path.basename(htmlpath); const panel = this.createNewPreviewPanel(handle, viewType, title, webview as ExtHostWebview, true); From 26c6fb1743fc4abb2d3bea8acf734a0112b2022f Mon Sep 17 00:00:00 2001 From: sharon wang Date: Thu, 19 Dec 2024 16:50:47 -0700 Subject: [PATCH 04/10] don't prevent default for copy paste or cut events --- extensions/positron-proxy/resources/webview-events.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/extensions/positron-proxy/resources/webview-events.js b/extensions/positron-proxy/resources/webview-events.js index dddf8cf36e4..7e628c5f390 100644 --- a/extensions/positron-proxy/resources/webview-events.js +++ b/extensions/positron-proxy/resources/webview-events.js @@ -15,7 +15,11 @@ * messages sent from this file and forwards them to the webview host, where * they are processed and dispatched. * - * The only difference between this file and the original is this comment block. + * Differences between this file and the original are code-fenced with the comment format: + * // --- Start Positron Proxy Changes --- + * ... + * // --- End Positron Proxy Changes --- + * * Original: src/vs/workbench/contrib/webview/browser/pre/webview-events.js */ @@ -90,8 +94,9 @@ const handleInnerKeydown = (e) => { // If the keypress would trigger a browser event, such as copy or paste, // make sure we block the browser from dispatching it. Instead VS Code // handles these events and will dispatch a copy/paste back to the webview - // if needed - if (isUndoRedo(e) || isPrint(e) || isFindEvent(e) || isSaveEvent(e) || isCopyPasteOrCut(e)) { + // --- Start Positron Proxy Changes --- + if (isUndoRedo(e) || isPrint(e) || isFindEvent(e) || isSaveEvent(e) /*|| isCopyPasteOrCut(e)*/) { + // --- End Positron Proxy Changes --- e.preventDefault(); } hostMessaging.postMessage('did-keydown', { From cac1504c2610800fe9df70ec9e165ec80e77c44b Mon Sep 17 00:00:00 2001 From: sharon wang Date: Fri, 20 Dec 2024 10:23:32 -0700 Subject: [PATCH 05/10] move help resources into `proxyServerHtmlConfigs` --- .../positron-proxy/src/positronProxy.ts | 204 ++++++++++-------- extensions/positron-proxy/src/types.ts | 33 ++- 2 files changed, 144 insertions(+), 93 deletions(-) diff --git a/extensions/positron-proxy/src/positronProxy.ts b/extensions/positron-proxy/src/positronProxy.ts index d27d08dbf14..94c9066cd5e 100644 --- a/extensions/positron-proxy/src/positronProxy.ts +++ b/extensions/positron-proxy/src/positronProxy.ts @@ -14,7 +14,7 @@ import { Disposable, ExtensionContext } from 'vscode'; import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware'; import { HtmlProxyServer } from './htmlProxy'; import { htmlContentRewriter, rewriteUrlsWithProxyPath } from './util'; -import { ContentRewriter, isAddressInfo, MaybeAddressInfo, PendingProxyServer, ProxyServerHtmlConfig, ProxyServerType } from './types'; +import { ContentRewriter, isAddressInfo, MaybeAddressInfo, PendingProxyServer, ProxyServerHtml, ProxyServerHtmlConfig, ProxyServerType } from './types'; /** * Constants. @@ -59,6 +59,7 @@ export class ProxyServer implements Disposable { readonly serverOrigin: string, readonly targetOrigin: string, private readonly server: Server, + readonly serverType: ProxyServerType, ) { } @@ -76,35 +77,10 @@ export class ProxyServer implements Disposable { export class PositronProxy implements Disposable { //#region Private Properties - /** - * Gets or sets a value which indicates whether the resources/scripts_{TYPE}.html files have been loaded. - */ - private _scriptsFileLoaded = false; - /** * Stores the proxy server HTML configurations. */ - private _proxyServerHtmlConfigs: ProxyServerHtmlConfig = {}; - - /** - * Gets or sets the help styles. - */ - private _helpStyles?: ProxyServerStyles; - - /** - * Gets or sets the help style defaults. - */ - private _helpStyleDefaults?: string; - - /** - * Gets or sets the help style overrides. - */ - private _helpStyleOverrides?: string; - - /** - * Gets or sets the help script. - */ - private _helpScript?: string; + private _proxyServerHtmlConfigs: ProxyServerHtmlConfig; /** * Gets or sets the proxy servers, keyed by target origin. @@ -128,26 +104,10 @@ export class PositronProxy implements Disposable { constructor(private readonly context: ExtensionContext) { // Try to load the resources/scripts_{TYPE}.html files and the elements within them. This will either // work or it will not work, but there's not sense in trying it again, if it doesn't. - - // Load the scripts_help.html file for the help proxy server. - try { - // Load the resources/scripts_help.html scripts file. - const scriptsPath = path.join(this.context.extensionPath, 'resources', 'scripts_help.html'); - const scripts = fs.readFileSync(scriptsPath).toString('utf8'); - - // Get the elements from the scripts file. - this._helpStyleDefaults = getStyleElement(scripts, 'help-style-defaults'); - this._helpStyleOverrides = getStyleElement(scripts, 'help-style-overrides'); - this._helpScript = getScriptElement(scripts, 'help-script'); - - // Set the scripts file loaded flag if everything appears to have worked. - this._scriptsFileLoaded = - this._helpStyleDefaults !== undefined && - this._helpStyleOverrides !== undefined && - this._helpScript !== undefined; - } catch (error) { - log.error(`Failed to load the resources/scripts_help.html file: ${JSON.stringify(error)}`); - } + this._proxyServerHtmlConfigs = { + help: this.loadHelpResources(), + preview: this.loadPreviewResources(), + }; } /** @@ -185,11 +145,17 @@ export class PositronProxy implements Disposable { // Build the help vars. let helpVars = ''; - if (this._helpStyles) { + const { + styleDefaults, + styleOverrides, + script: helpScript, + styles: helpStyles + } = this._proxyServerHtmlConfigs.help; + if (helpStyles) { helpVars += '\n'; @@ -203,14 +169,14 @@ export class PositronProxy implements Disposable { '', `\n ${helpVars}\n - ${this._helpStyleDefaults}` + ${styleDefaults}` ); // Inject the help style overrides and the help script. response = response.replace( '', - `${this._helpStyleOverrides}\n - ${this._helpScript}\n + `${styleOverrides}\n + ${helpScript}\n ` ); @@ -219,7 +185,9 @@ export class PositronProxy implements Disposable { // Return the response. return response; - }); + }, + ProxyServerType.Help + ); } /** @@ -258,12 +226,6 @@ export class PositronProxy implements Disposable { this._htmlProxyServer = new HtmlProxyServer(); } - // If we're running in a Web UI, load the preview resources to inject iframe communication - // scripts. Otherwise, we have Electron-specific handling for Desktop. - if (vscode.env.uiKind === vscode.UIKind.Web) { - this.loadPreviewResources(); - } - return this._htmlProxyServer.createHtmlProxy( targetPath, this._proxyServerHtmlConfigs.preview @@ -276,7 +238,7 @@ export class PositronProxy implements Disposable { */ setHelpProxyServerStyles(styles: ProxyServerStyles) { // Set the help styles. - this._helpStyles = styles; + this._proxyServerHtmlConfigs.help.styles = styles; } /** @@ -287,7 +249,7 @@ export class PositronProxy implements Disposable { startHttpProxyServer(targetOrigin: string): Promise { log.debug(`Starting an HTTP proxy server for target: ${targetOrigin}...`); // Start the proxy server. - return this.startProxyServer(targetOrigin, htmlContentRewriter); + return this.startProxyServer(targetOrigin, htmlContentRewriter, ProxyServerType.Preview); } /** @@ -302,38 +264,77 @@ export class PositronProxy implements Disposable { log.debug('Starting a pending HTTP proxy server...'); // Start the proxy server and return the pending proxy server info. The caller will need to // call finishProxySetup to complete the proxy setup. - return this.startNewProxyServer(htmlContentRewriter); + return this.startNewProxyServer(htmlContentRewriter, ProxyServerType.Preview); } //#endregion Public Methods //#region Private Methods - private loadPreviewResources() { - // Load the scripts_preview.html file for the preview proxy server. + /** + * Loads the help HTML resources and constructs the help HTML config. + * @returns The help HTML config or an empty object if something went wrong while loading resources. + */ + private loadHelpResources(): ProxyServerHtml { try { - // Load the resources/scripts_preview.html scripts file. - const scriptsPath = path.join(this.context.extensionPath, 'resources', 'scripts_preview.html'); + // Load the resources/scripts_help.html scripts file. + const scriptsPath = path.join(this.context.extensionPath, 'resources', 'scripts_help.html'); const scripts = fs.readFileSync(scriptsPath).toString('utf8'); - // Get the elements from the scripts file. - this._proxyServerHtmlConfigs.preview = { - styleDefaults: getStyleElement(scripts, 'preview-style-defaults'), - styleOverrides: getStyleElement(scripts, 'preview-style-overrides'), - }; - - // Inject the webview events script. - const scriptEl = getScriptElement(scripts, 'preview-script'); - if (scriptEl) { - const webviewEventsScriptPath = path.join(this.context.extensionPath, 'resources', 'webview-events.js'); - const webviewEventsScript = fs.readFileSync(webviewEventsScriptPath).toString('utf8'); - this._proxyServerHtmlConfigs.preview.script = scriptEl.replace('// webviewEventsScript placeholder', webviewEventsScript); - } - return true; + // Construct the help HTML config. + const helpHtmlConfig = new ProxyServerHtml( + getStyleElement(scripts, 'help-style-defaults'), + getStyleElement(scripts, 'help-style-overrides'), + getScriptElement(scripts, 'help-script') + ); + + // Return the help HTML config. + return helpHtmlConfig; } catch (error) { - log.error(`Failed to load the resources/scripts_preview.html file: ${JSON.stringify(error)}`); + log.error(`Failed to load the resources/scripts_help.html file: ${JSON.stringify(error)}`); } - return false; + + // Return an empty help HTML config. + return new ProxyServerHtml(); + } + + /** + * Loads the preview HTML resources and constructs the preview HTML config when running in the Web. + * @returns The preview HTML config or an empty object if something went wrong while loading resources. + */ + private loadPreviewResources(): ProxyServerHtml { + // Load the preview resources only when running in the Web. + if (vscode.env.uiKind === vscode.UIKind.Web) { + try { + // Load the resources/scripts_preview.html scripts file. + const scriptsPath = path.join(this.context.extensionPath, 'resources', 'scripts_preview.html'); + const scripts = fs.readFileSync(scriptsPath).toString('utf8'); + + // Inject the webview events script. + const scriptEl = getScriptElement(scripts, 'preview-script'); + let previewScript; + if (scriptEl) { + const webviewEventsScriptPath = path.join(this.context.extensionPath, 'resources', 'webview-events.js'); + const webviewEventsScript = fs.readFileSync(webviewEventsScriptPath).toString('utf8'); + previewScript = scriptEl.replace('// webviewEventsScript placeholder', webviewEventsScript); + } + + // Construct the preview HTML config. + const previewHtmlConfig = new ProxyServerHtml( + getStyleElement(scripts, 'preview-style-defaults'), + getStyleElement(scripts, 'preview-style-overrides'), + previewScript, + ); + + // Return the preview HTML config. + return previewHtmlConfig; + } catch (error) { + log.error(`Failed to load the resources/scripts_preview.html file: ${JSON.stringify(error)}`); + } + } + + // Return an empty preview HTML config. + return new ProxyServerHtml(); } /** @@ -342,7 +343,7 @@ export class PositronProxy implements Disposable { * @param contentRewriter The content rewriter. * @returns The server origin, resolved to an external uri if applicable. */ - private async startProxyServer(targetOrigin: string, contentRewriter: ContentRewriter): Promise { + private async startProxyServer(targetOrigin: string, contentRewriter: ContentRewriter, serverType: ProxyServerType): Promise { // See if we have an existing proxy server for target origin. If there is, return the // server origin. const proxyServer = this._proxyServers.get(targetOrigin); @@ -354,7 +355,7 @@ export class PositronProxy implements Disposable { let pendingProxy: PendingProxyServer; try { // We don't have an existing proxy server for the target origin, so start a new one. - pendingProxy = await this.startNewProxyServer(contentRewriter); + pendingProxy = await this.startNewProxyServer(contentRewriter, serverType); } catch (error) { log.error(`Failed to start a proxy server for ${targetOrigin}: ${JSON.stringify(error)}`); throw error; @@ -378,7 +379,7 @@ export class PositronProxy implements Disposable { * This is used to create a server and app that will be used to add middleware later. * @returns The server origin and the proxy path. */ - private async startNewProxyServer(contentRewriter: ContentRewriter): Promise { + private async startNewProxyServer(contentRewriter: ContentRewriter, serverType: ProxyServerType): Promise { // Create the app and start listening on a random port. const app = express(); let address: MaybeAddressInfo; @@ -418,6 +419,7 @@ export class PositronProxy implements Disposable { serverOrigin, externalUri, server, + serverType, app, contentRewriter ); @@ -431,6 +433,7 @@ export class PositronProxy implements Disposable { * @param serverOrigin The server origin. * @param externalUri The external URI. * @param server The server. + * @param serverType The server type. * @param app The express app. * @param contentRewriter The content rewriter. * @returns A promise that resolves when the proxy setup is complete. @@ -440,6 +443,7 @@ export class PositronProxy implements Disposable { serverOrigin: string, externalUri: vscode.Uri, server: Server, + serverType: ProxyServerType, app: express.Express, contentRewriter: ContentRewriter ) { @@ -452,7 +456,8 @@ export class PositronProxy implements Disposable { this._proxyServers.set(targetOrigin, new ProxyServer( serverOrigin, targetOrigin, - server + server, + serverType )); // Add the proxy middleware. @@ -483,7 +488,9 @@ export class PositronProxy implements Disposable { // content rewriter. Also, the scripts must be loaded. const url = req.url; const contentType = proxyRes.headers['content-type']; - if (!url || !contentType || !this._scriptsFileLoaded) { + const serverType = this._proxyServers.get(targetOrigin)?.serverType; + const scriptsLoaded = this.resourcesLoadedForServerType(serverType); + if (!url || !contentType || !scriptsLoaded) { log.trace(`onProxyRes - skipping response processing for ${serverOrigin}${url}`); // Don't process the response. return responseBuffer; @@ -495,5 +502,26 @@ export class PositronProxy implements Disposable { })); } + /** + * Checks if the resources are loaded for the server type. + * @param serverType The server type. + * @returns Whether the scripts are loaded. + */ + private resourcesLoadedForServerType(serverType: ProxyServerType | undefined): boolean { + switch (serverType) { + case ProxyServerType.Help: + return this._proxyServerHtmlConfigs.help.resourcesLoaded(); + case ProxyServerType.Preview: + // Check if the resources are loaded when running in the Web. + if (vscode.env.uiKind === vscode.UIKind.Web) { + return this._proxyServerHtmlConfigs.preview.resourcesLoaded(); + } + return true; + default: + console.log(`Can't check if resources are loaded for unknown server type: ${serverType}`); + return false; + } + } + //#endregion Private Methods } diff --git a/extensions/positron-proxy/src/types.ts b/extensions/positron-proxy/src/types.ts index b4195bf6c76..afad7010640 100644 --- a/extensions/positron-proxy/src/types.ts +++ b/extensions/positron-proxy/src/types.ts @@ -46,21 +46,44 @@ export const isAddressInfo = ( (addressInfo as AddressInfo).port !== undefined; /** - * ProxyServerStyles type. + * ProxyServerHtml class. */ -export type ProxyServerHtml = { - styles?: ProxyServerStyles; +export class ProxyServerHtml { styleDefaults?: string; styleOverrides?: string; script?: string; + styles?: ProxyServerStyles; + + constructor( + styleDefaults?: string, + styleOverrides?: string, + script?: string, + styles?: ProxyServerStyles, + + ) { + this.styleDefaults = styleDefaults; + this.styleOverrides = styleOverrides; + this.script = script; + this.styles = styles; + } + + /** + * Function to check if all resources are loaded. + * @returns true if styles, styleDefaults, and styleOverrides are all defined; otherwise, false. + */ + resourcesLoaded(): boolean { + return this.styleDefaults !== undefined + && this.styleOverrides !== undefined + && this.script !== undefined; + } }; /** * ProxyServerHtmlConfig type. */ export interface ProxyServerHtmlConfig { - help?: ProxyServerHtml; - preview?: ProxyServerHtml; + help: ProxyServerHtml; + preview: ProxyServerHtml; } /** From cc2a7395dcfad03ce445fb7881387d6fc37b6bab Mon Sep 17 00:00:00 2001 From: sharon wang Date: Fri, 20 Dec 2024 13:06:51 -0700 Subject: [PATCH 06/10] move content rewriters to util and add htmlConfig to args --- extensions/positron-proxy/src/htmlProxy.ts | 4 +- .../positron-proxy/src/positronProxy.ts | 70 +++----------- extensions/positron-proxy/src/util.ts | 91 ++++++++++++++++++- 3 files changed, 107 insertions(+), 58 deletions(-) diff --git a/extensions/positron-proxy/src/htmlProxy.ts b/extensions/positron-proxy/src/htmlProxy.ts index 055be572f3b..fdea9d26ed2 100644 --- a/extensions/positron-proxy/src/htmlProxy.ts +++ b/extensions/positron-proxy/src/htmlProxy.ts @@ -104,8 +104,8 @@ export class HtmlProxyServer implements Disposable { content = content.replace( '', `${htmlConfig.styleOverrides + '\n' || ''} - ${htmlConfig.script + '\n' || ''} - ` + ${htmlConfig.script + '\n' || ''} + ` ); } res.send(content); diff --git a/extensions/positron-proxy/src/positronProxy.ts b/extensions/positron-proxy/src/positronProxy.ts index 94c9066cd5e..55b95739265 100644 --- a/extensions/positron-proxy/src/positronProxy.ts +++ b/extensions/positron-proxy/src/positronProxy.ts @@ -13,7 +13,7 @@ import { log, ProxyServerStyles } from './extension'; import { Disposable, ExtensionContext } from 'vscode'; import { createProxyMiddleware, responseInterceptor } from 'http-proxy-middleware'; import { HtmlProxyServer } from './htmlProxy'; -import { htmlContentRewriter, rewriteUrlsWithProxyPath } from './util'; +import { helpContentRewriter, htmlContentRewriter } from './util'; import { ContentRewriter, isAddressInfo, MaybeAddressInfo, PendingProxyServer, ProxyServerHtml, ProxyServerHtmlConfig, ProxyServerType } from './types'; /** @@ -135,59 +135,7 @@ export class PositronProxy implements Disposable { log.debug(`Starting a help proxy server for target: ${targetOrigin}...`); // Start the proxy server. - return this.startProxyServer( - targetOrigin, - async (_serverOrigin, proxyPath, _url, contentType, responseBuffer) => { - // If this isn't 'text/html' content, just return the response buffer. - if (!contentType.includes('text/html')) { - return responseBuffer; - } - - // Build the help vars. - let helpVars = ''; - const { - styleDefaults, - styleOverrides, - script: helpScript, - styles: helpStyles - } = this._proxyServerHtmlConfigs.help; - if (helpStyles) { - helpVars += '\n'; - } - - // Get the response. - let response = responseBuffer.toString('utf8'); - - // Inject the help style defaults for unstyled help documents and the help vars. - response = response.replace( - '', - `\n - ${helpVars}\n - ${styleDefaults}` - ); - - // Inject the help style overrides and the help script. - response = response.replace( - '', - `${styleOverrides}\n - ${helpScript}\n - ` - ); - - // Rewrite the URLs with the proxy path. - response = rewriteUrlsWithProxyPath(response, proxyPath); - - // Return the response. - return response; - }, - ProxyServerType.Help - ); + return this.startProxyServer(targetOrigin, helpContentRewriter, ProxyServerType.Help); } /** @@ -496,8 +444,20 @@ export class PositronProxy implements Disposable { return responseBuffer; } + // Get the HTML configuration. + const htmlConfig = serverType === ProxyServerType.Help + ? this._proxyServerHtmlConfigs.help + : this._proxyServerHtmlConfigs.preview; + // Rewrite the content. - return contentRewriter(serverOrigin, externalUri.path, url, contentType, responseBuffer); + return contentRewriter( + serverOrigin, + externalUri.path, + url, + contentType, + responseBuffer, + htmlConfig + ); }) })); } diff --git a/extensions/positron-proxy/src/util.ts b/extensions/positron-proxy/src/util.ts index 57fd374a5a4..fe02e769481 100644 --- a/extensions/positron-proxy/src/util.ts +++ b/extensions/positron-proxy/src/util.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { ProxyServerHtml } from './types'; /** * PromiseHandles is a class that represents a promise that can be resolved or @@ -33,7 +34,14 @@ export class PromiseHandles { * @param responseBuffer The response buffer. * @returns The rewritten response buffer. */ -export async function htmlContentRewriter(_serverOrigin: string, proxyPath: string, _url: string, contentType: string, responseBuffer: Buffer) { +export async function htmlContentRewriter( + _serverOrigin: string, + proxyPath: string, + _url: string, + contentType: string, + responseBuffer: Buffer, + htmlConfig?: ProxyServerHtml +) { // If this isn't 'text/html' content, just return the response buffer. if (!contentType.includes('text/html')) { return responseBuffer; @@ -42,6 +50,87 @@ export async function htmlContentRewriter(_serverOrigin: string, proxyPath: stri // Get the response. let response = responseBuffer.toString('utf8'); + // If we're running in the web, we need to inject resources for the preview HTML. + if (vscode.env.uiKind === vscode.UIKind.Web && htmlConfig) { + // Inject the preview style defaults for unstyled preview documents. + response = response.replace( + '', + `\n + ${htmlConfig.styleDefaults + '\n' || ''}` + ); + + // Inject the preview style overrides and script. + response = response.replace( + '', + `${htmlConfig.styleOverrides + '\n' || ''} + ${htmlConfig.script + '\n' || ''} + ` + ); + } + + // Rewrite the URLs with the proxy path. + response = rewriteUrlsWithProxyPath(response, proxyPath); + + // Return the response. + return response; +} + +export async function helpContentRewriter( + _serverOrigin: string, + proxyPath: string, + _url: string, + contentType: string, + responseBuffer: Buffer, + htmlConfig?: ProxyServerHtml +) { + // If this isn't 'text/html' content, just return the response buffer. + if (!contentType.includes('text/html')) { + return responseBuffer; + } + + // Get the response. + let response = responseBuffer.toString('utf8'); + + if (htmlConfig) { + // Build the help vars. + let helpVars = ''; + + // Destructure the HTML config. + const { + styleDefaults, + styleOverrides, + script: helpScript, + styles: helpStyles + } = htmlConfig; + + // Inject the help vars. + if (helpStyles) { + helpVars += '\n'; + } + + // Inject the help style defaults for unstyled help documents and the help vars. + response = response.replace( + '', + `\n + ${helpVars}\n + ${styleDefaults}` + ); + + // Inject the help style overrides and the help script. + response = response.replace( + '', + `${styleOverrides}\n + ${helpScript}\n + ` + ); + } + // Rewrite the URLs with the proxy path. response = rewriteUrlsWithProxyPath(response, proxyPath); From fc6fc5516a5377f6434c2af32586267d88c25ab5 Mon Sep 17 00:00:00 2001 From: sharon wang Date: Fri, 20 Dec 2024 17:00:33 -0700 Subject: [PATCH 07/10] get back, forward and refresh nav working in viewer on web --- extensions/positron-proxy/src/htmlProxy.ts | 35 +++++++++++------- extensions/positron-proxy/src/util.ts | 37 ++++++++++++------- .../browser/previewOverlayWebview.ts | 4 +- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/extensions/positron-proxy/src/htmlProxy.ts b/extensions/positron-proxy/src/htmlProxy.ts index fdea9d26ed2..8c1f5dc2bc6 100644 --- a/extensions/positron-proxy/src/htmlProxy.ts +++ b/extensions/positron-proxy/src/htmlProxy.ts @@ -93,20 +93,29 @@ export class HtmlProxyServer implements Disposable { let content = fs.readFileSync(filePath, 'utf8'); // If there is an HTML configuration, use it to rewrite the content. if (htmlConfig) { - // Inject the preview style defaults for unstyled preview documents. - content = content.replace( - '', - `\n - ${htmlConfig.styleDefaults + '\n' || ''}` - ); + // If the response includes a head tag, inject the preview resources into the head tag. + if (content.includes('')) { + // Inject the preview style defaults for unstyled preview documents. + content = content.replace( + '', + ` + ${htmlConfig.styleDefaults || ''}` + ); - // Inject the preview style overrides and script. - content = content.replace( - '', - `${htmlConfig.styleOverrides + '\n' || ''} - ${htmlConfig.script + '\n' || ''} - ` - ); + // Inject the preview style overrides and script. + content = content.replace( + '', + `${htmlConfig.styleOverrides || ''} + ${htmlConfig.script || ''} + ` + ); + } else { + // Otherwise, prepend the HTML content with the preview resources. + content = `${htmlConfig.styleDefaults || ''} + ${htmlConfig.styleOverrides || ''} + ${htmlConfig.script || ''} + ${content}`; + } } res.send(content); } else { diff --git a/extensions/positron-proxy/src/util.ts b/extensions/positron-proxy/src/util.ts index fe02e769481..a80a2aab8a8 100644 --- a/extensions/positron-proxy/src/util.ts +++ b/extensions/positron-proxy/src/util.ts @@ -52,20 +52,29 @@ export async function htmlContentRewriter( // If we're running in the web, we need to inject resources for the preview HTML. if (vscode.env.uiKind === vscode.UIKind.Web && htmlConfig) { - // Inject the preview style defaults for unstyled preview documents. - response = response.replace( - '', - `\n - ${htmlConfig.styleDefaults + '\n' || ''}` - ); - - // Inject the preview style overrides and script. - response = response.replace( - '', - `${htmlConfig.styleOverrides + '\n' || ''} - ${htmlConfig.script + '\n' || ''} - ` - ); + // If the response includes a head tag, inject the preview resources into the head tag. + if (response.includes('')) { + // Inject the preview style defaults for unstyled preview documents. + response = response.replace( + '', + `\n + ${htmlConfig.styleDefaults || ''}` + ); + + // Inject the preview style overrides and script. + response = response.replace( + '', + `${htmlConfig.styleOverrides || ''} + ${htmlConfig.script || ''} + ` + ); + } else { + // Otherwise, prepend the HTML content with the preview resources. + response = `${htmlConfig.styleDefaults || ''} + ${htmlConfig.styleOverrides || ''} + ${htmlConfig.script || ''} + ${response}`; + } } // Rewrite the URLs with the proxy path. diff --git a/src/vs/workbench/contrib/positronPreview/browser/previewOverlayWebview.ts b/src/vs/workbench/contrib/positronPreview/browser/previewOverlayWebview.ts index 2992d249334..d699e658024 100644 --- a/src/vs/workbench/contrib/positronPreview/browser/previewOverlayWebview.ts +++ b/src/vs/workbench/contrib/positronPreview/browser/previewOverlayWebview.ts @@ -66,7 +66,7 @@ export class PreviewOverlayWebview extends Disposable { // Listen for messages window.addEventListener('message', message => { - if (message.source === previewContentWindow) { + if (message.source === previewContentWindow && message.data.channel !== 'execCommand') { // If a message is coming from the preview content window, forward it to the // preview overlay webview. vscode.postMessage({ @@ -75,6 +75,8 @@ export class PreviewOverlayWebview extends Disposable { }); } else { // Forward messages from the preview overlay webview to the preview content window. + // Messages may include commands to navigate back, forward, reload, etc., + // via the 'execCommand' channel. previewContentWindow.postMessage(message.data, '*'); } }); From 9d4b174b2c4c05ed681926a852a4431aabedefb8 Mon Sep 17 00:00:00 2001 From: sharon wang Date: Fri, 20 Dec 2024 17:07:01 -0700 Subject: [PATCH 08/10] move preview resources injection to util file --- extensions/positron-proxy/src/htmlProxy.ts | 26 +-------- extensions/positron-proxy/src/util.ts | 67 ++++++++++++++-------- 2 files changed, 46 insertions(+), 47 deletions(-) diff --git a/extensions/positron-proxy/src/htmlProxy.ts b/extensions/positron-proxy/src/htmlProxy.ts index 8c1f5dc2bc6..7e70a537f6d 100644 --- a/extensions/positron-proxy/src/htmlProxy.ts +++ b/extensions/positron-proxy/src/htmlProxy.ts @@ -9,7 +9,7 @@ import path = require('path'); import fs = require('fs'); import { Disposable, Uri } from 'vscode'; -import { PromiseHandles } from './util'; +import { injectPreviewResources, PromiseHandles } from './util'; import { isAddressInfo, ProxyServerHtml } from './types'; /** @@ -93,29 +93,7 @@ export class HtmlProxyServer implements Disposable { let content = fs.readFileSync(filePath, 'utf8'); // If there is an HTML configuration, use it to rewrite the content. if (htmlConfig) { - // If the response includes a head tag, inject the preview resources into the head tag. - if (content.includes('')) { - // Inject the preview style defaults for unstyled preview documents. - content = content.replace( - '', - ` - ${htmlConfig.styleDefaults || ''}` - ); - - // Inject the preview style overrides and script. - content = content.replace( - '', - `${htmlConfig.styleOverrides || ''} - ${htmlConfig.script || ''} - ` - ); - } else { - // Otherwise, prepend the HTML content with the preview resources. - content = `${htmlConfig.styleDefaults || ''} - ${htmlConfig.styleOverrides || ''} - ${htmlConfig.script || ''} - ${content}`; - } + content = injectPreviewResources(content, htmlConfig); } res.send(content); } else { diff --git a/extensions/positron-proxy/src/util.ts b/extensions/positron-proxy/src/util.ts index a80a2aab8a8..5fdd95f99de 100644 --- a/extensions/positron-proxy/src/util.ts +++ b/extensions/positron-proxy/src/util.ts @@ -52,29 +52,7 @@ export async function htmlContentRewriter( // If we're running in the web, we need to inject resources for the preview HTML. if (vscode.env.uiKind === vscode.UIKind.Web && htmlConfig) { - // If the response includes a head tag, inject the preview resources into the head tag. - if (response.includes('')) { - // Inject the preview style defaults for unstyled preview documents. - response = response.replace( - '', - `\n - ${htmlConfig.styleDefaults || ''}` - ); - - // Inject the preview style overrides and script. - response = response.replace( - '', - `${htmlConfig.styleOverrides || ''} - ${htmlConfig.script || ''} - ` - ); - } else { - // Otherwise, prepend the HTML content with the preview resources. - response = `${htmlConfig.styleDefaults || ''} - ${htmlConfig.styleOverrides || ''} - ${htmlConfig.script || ''} - ${response}`; - } + response = injectPreviewResources(response, htmlConfig); } // Rewrite the URLs with the proxy path. @@ -84,6 +62,49 @@ export async function htmlContentRewriter( return response; } +/** + * Injects the preview resources into the HTML content. + * @param content The HTML content to inject the preview resources into. + * @param htmlConfig The HTML configuration defining the preview resources. + * @returns The content with the preview resources injected. + */ +export function injectPreviewResources(content: string, htmlConfig: ProxyServerHtml) { + // If the response includes a head tag, inject the preview resources into the head tag. + if (content.includes('')) { + // Inject the preview style defaults for unstyled preview documents. + content = content.replace( + '', + `\n + ${htmlConfig.styleDefaults || ''}` + ); + + // Inject the preview style overrides and script. + content = content.replace( + '', + `${htmlConfig.styleOverrides || ''} + ${htmlConfig.script || ''} + ` + ); + } else { + // Otherwise, prepend the HTML content with the preview resources. + content = `${htmlConfig.styleDefaults || ''} + ${htmlConfig.styleOverrides || ''} + ${htmlConfig.script || ''} + ${content}`; + } + return content; +} + +/** + * A content rewriter for help content. Injects the help resources into the help HTML content. + * @param _serverOrigin The server origin. + * @param proxyPath The proxy path. + * @param _url The URL. + * @param contentType The content type. + * @param responseBuffer The response buffer. + * @param htmlConfig The HTML configuration. + * @returns The rewritten response buffer. + */ export async function helpContentRewriter( _serverOrigin: string, proxyPath: string, From 87ae243b5ee9e6cf511cfcfc92ad1ac96a450908 Mon Sep 17 00:00:00 2001 From: sharon wang Date: Mon, 23 Dec 2024 10:19:26 -0700 Subject: [PATCH 09/10] use `handleInnerKeydown` from index.html in web based webview-events.js --- .../resources/webview-events.js | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/extensions/positron-proxy/resources/webview-events.js b/extensions/positron-proxy/resources/webview-events.js index 7e628c5f390..e54654641f0 100644 --- a/extensions/positron-proxy/resources/webview-events.js +++ b/extensions/positron-proxy/resources/webview-events.js @@ -21,6 +21,9 @@ * // --- End Positron Proxy Changes --- * * Original: src/vs/workbench/contrib/webview/browser/pre/webview-events.js + * + * This file is intended for the browser context of Positron, and should not used in the + * Electron context. Please see the original webview-events.js file for the Electron context. */ /** @@ -87,18 +90,27 @@ const handleAuxClick = (event) => { } }; +// --- Start Positron Proxy Changes --- /** + * This is a copy of the handleInnerKeydown function from src/vs/workbench/contrib/webview/browser/pre/index.html, + * with some modifications for Positron in a browser context. * @param {KeyboardEvent} e */ const handleInnerKeydown = (e) => { // If the keypress would trigger a browser event, such as copy or paste, // make sure we block the browser from dispatching it. Instead VS Code // handles these events and will dispatch a copy/paste back to the webview - // --- Start Positron Proxy Changes --- - if (isUndoRedo(e) || isPrint(e) || isFindEvent(e) || isSaveEvent(e) /*|| isCopyPasteOrCut(e)*/) { - // --- End Positron Proxy Changes --- + // if needed + if (isPrint(e) || isFindEvent(e) || isSaveEvent(e)) { + e.preventDefault(); + } else if (isUndoRedo(e) || isCopyPasteOrCut(e)) { + return; // let the browser handle this + } else if (isCloseTab(e) || isNewWindow(e) || isHelp(e) || isRefresh(e)) { + // Prevent Ctrl+W closing window / Ctrl+N opening new window in PWA. + // (No effect in a regular browser tab.) e.preventDefault(); } + hostMessaging.postMessage('did-keydown', { key: e.key, keyCode: e.keyCode, @@ -107,9 +119,11 @@ const handleInnerKeydown = (e) => { altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, - repeat: e.repeat, + repeat: e.repeat }); }; +// --- End Positron Proxy Changes --- + /** * @param {KeyboardEvent} e */ From c163acf13572e2b0a975dbbbb5abc9b6152181e5 Mon Sep 17 00:00:00 2001 From: sharon wang Date: Mon, 23 Dec 2024 10:25:26 -0700 Subject: [PATCH 10/10] reorder if block --- extensions/positron-proxy/src/htmlProxy.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/extensions/positron-proxy/src/htmlProxy.ts b/extensions/positron-proxy/src/htmlProxy.ts index 7e70a537f6d..288086867e4 100644 --- a/extensions/positron-proxy/src/htmlProxy.ts +++ b/extensions/positron-proxy/src/htmlProxy.ts @@ -86,7 +86,10 @@ export class HtmlProxyServer implements Disposable { } // Create a new path entry. - if (vscode.env.uiKind === vscode.UIKind.Web) { + if (vscode.env.uiKind !== vscode.UIKind.Web) { + this._app.use(`/${serverPath}`, express.static(targetPath)); + } else { + // If we're running in the web, we need to inject resources for the preview HTML. this._app.use(`/${serverPath}`, async (req, res, next) => { const filePath = path.join(targetPath, req.path); if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { @@ -100,8 +103,6 @@ export class HtmlProxyServer implements Disposable { next(); } }); - } else { - this._app.use(`/${serverPath}`, express.static(targetPath)); } const address = this._server.address(); if (!isAddressInfo(address)) {