Repository: microsoft/vscode-edge-devtools
Pull request #239 (2020-11-24 02:27:17)

[Polyfill]: Improve extension settings infrastructure
This PR standardizes and improves the workflow of grabbing extension settings values to use within the DevTools.  We create a global map `Root.Runtime.extensionSettings` that we fill with values on the constructor of Runtime.  Values such as `Root.Runtime.extensionSettings.get('networkEnabled')` or `Root.Runtime.extensionSettings.get('theme')` can be used throughout the DevTools without needing to utilize the InspectorFrontendHost callback each time we need to access these settings.


Patch:
diff --git a/postBuildStep.ts b/postBuildStep.ts
index 495b625..b59f09f 100644
--- a/postBuildStep.ts
+++ b/postBuildStep.ts
@@ -10,15 +10,17 @@ import {
     applyAppendTabPatch,
     applyCommonRevealerPatch,
     applyCommandMenuPatch,
+    applyCreateExtensionSettingsPatch,
+    applyCreateExtensionSettingsLegacyPatch,
     applyDefaultTabPatch,
     applyDrawerTabLocationPatch,
+    applyPortSettingsPatch,
     applyQuickOpenPatch,
     applyInspectorCommonContextMenuPatch,
     applyInspectorCommonCssPatch,
     applyInspectorCommonCssRightToolbarPatch,
     applyInspectorCommonCssTabSliderPatch,
     applyInspectorCommonNetworkPatch,
-    applyMainThemePatch,
     applyMainViewPatch,
     applyPersistRequestBlockingTab,
     applyRemoveBreakOnContextMenuItem,
@@ -95,7 +97,6 @@ async function patchFilesForWebView(toolsOutDir: string) {
         applyInspectorCommonCssTabSliderPatch,
     ]);
     await patchFileForWebViewWrapper("main/main.js", toolsOutDir, [
-        applyMainThemePatch,
         applyMainViewPatch,
     ]);
     await patchFileForWebViewWrapper("elements/elements.js", toolsOutDir, [
@@ -125,8 +126,13 @@ async function patchFilesForWebView(toolsOutDir: string) {
         applyShowRequestBlockingTab,
     ]);
     await patchFileForWebViewWrapper("root/root.js", toolsOutDir, [
+        applyCreateExtensionSettingsPatch,
+        applyPortSettingsPatch,
         applyRuntimeImportScriptPathPrefixPatch,
     ]);
+    await patchFileForWebViewWrapper("root/root-legacy.js", toolsOutDir, [
+        applyCreateExtensionSettingsLegacyPatch,
+    ]);
     await patchFileForWebViewWrapper("quick_open/quick_open.js", toolsOutDir, [
         applyCommandMenuPatch,
         applyQuickOpenPatch,
diff --git a/src/common/webviewEvents.ts b/src/common/webviewEvents.ts
index a58ded9..37e462f 100644
--- a/src/common/webviewEvents.ts
+++ b/src/common/webviewEvents.ts
@@ -2,7 +2,7 @@
 // Licensed under the MIT License.
 
 export type WebviewEvent = "getState" | "getUrl" | "openInEditor" | "ready" | "setState" | "telemetry" | "websocket"
-| "getApprovedTabs" | "getThemes" | "copyText" | "focusEditor" | "focusEditorGroup";
+| "getVscodeSettings" | "copyText" | "focusEditor" | "focusEditorGroup";
 export const webviewEventNames: WebviewEvent[] = [
     "getState",
     "getUrl",
@@ -11,8 +11,7 @@ export const webviewEventNames: WebviewEvent[] = [
     "setState",
     "telemetry",
     "websocket",
-    "getApprovedTabs",
-    "getThemes",
+    "getVscodeSettings",
     "copyText",
     "focusEditor",
     "focusEditorGroup",
diff --git a/src/devtoolsPanel.ts b/src/devtoolsPanel.ts
index 9a4205a..93ad3a0 100644
--- a/src/devtoolsPanel.ts
+++ b/src/devtoolsPanel.ts
@@ -55,8 +55,7 @@ export class DevToolsPanel {
         this.panelSocket.on("websocket", () => this.onSocketMessage());
         this.panelSocket.on("telemetry", (msg) => this.onSocketTelemetry(msg));
         this.panelSocket.on("getState", (msg) => this.onSocketGetState(msg));
-        this.panelSocket.on("getApprovedTabs", (msg) => this.onSocketGetApprovedTabs(msg));
-        this.panelSocket.on("getThemes", (msg) => this.onSocketGetThemes(msg));
+        this.panelSocket.on("getVscodeSettings", (msg) => this.onSocketGetVscodeSettings(msg));
         this.panelSocket.on("setState", (msg) => this.onSocketSetState(msg));
         this.panelSocket.on("getUrl", (msg) => this.onSocketGetUrl(msg));
         this.panelSocket.on("openInEditor", (msg) => this.onSocketOpenInEditor(msg));
@@ -188,16 +187,10 @@ export class DevToolsPanel {
         encodeMessageForChannel((msg) => this.panel.webview.postMessage(msg), "getState", { id, preferences });
     }
 
-    private onSocketGetApprovedTabs(message: string) {
+    private onSocketGetVscodeSettings(message: string) {
         const { id } = JSON.parse(message) as { id: number };
-        encodeMessageForChannel((msg) => this.panel.webview.postMessage(msg), "getApprovedTabs", {
+        encodeMessageForChannel((msg) => this.panel.webview.postMessage(msg), "getVscodeSettings", {
             enableNetwork: SettingsProvider.instance.isNetworkEnabled(),
-            id });
-    }
-
-    private onSocketGetThemes(message: string) {
-        const { id } = JSON.parse(message) as { id: number };
-        encodeMessageForChannel((msg) => this.panel.webview.postMessage(msg), "getThemes", {
             themeString: SettingsProvider.instance.getThemeSettings(),
             id });
     }
diff --git a/src/host/polyfills/simpleView.ts b/src/host/polyfills/simpleView.ts
index b0420f2..84a25dd 100644
--- a/src/host/polyfills/simpleView.ts
+++ b/src/host/polyfills/simpleView.ts
@@ -3,9 +3,7 @@
 import { IDevToolsWindow } from "../host";
 import ToolsHost from "../toolsHost";
 
-declare var InspectorFrontendHost: {
-    InspectorFrontendHostInstance: ToolsHost;
-};
+declare var InspectorFrontendHost: ToolsHost;
 
 interface IRevealable {
     lineNumber: number;
@@ -29,8 +27,59 @@ export function revealInVSCode(revealable: IRevealable | undefined, omitFocus: b
     return Promise.resolve();
 }
 
-export function getApprovedTabs(callback: () => void) {
-    InspectorFrontendHost.InspectorFrontendHostInstance.getApprovedTabs(callback);
+export function getVscodeSettings(callback: (arg0: object) => void) {
+    InspectorFrontendHost.getVscodeSettings(callback);
+}
+
+export function applyCreateExtensionSettingsPatch(content: string) {
+    const pattern = /const experiments=new ExperimentsSupport\(\);/;
+    const match = content.match(pattern);
+    if (match) {
+        const matchedString = match[0];
+        content = content.replace(pattern, `const vscodeSettings={};${matchedString}`);
+    } else {
+        return null;
+    }
+
+    const pattern2 = /experiments:experiments/;
+    const match2 = content.match(pattern2);
+    if (match2) {
+        const matchedString = match2[0];
+        return content.replace(pattern2, `${matchedString}, vscodeSettings:vscodeSettings`);
+    } else {
+        return null;
+    }
+}
+
+export function applyCreateExtensionSettingsLegacyPatch(content: string) {
+    const pattern = /Root\.Runtime\.experiments/g
+    const match = content.match(pattern);
+    if (match) {
+        const matchedString = match[0];
+        return content.replace(pattern, `Root.Runtime.vscodeSettings = RootModule.Runtime.vscodeSettings; ${matchedString}`);
+    } else {
+        return null;
+    }
+}
+
+export function applyPortSettingsPatch(content: string) {
+    const pattern = /static instance/g;
+    const match = content.match(pattern);
+    if (match) {
+        const matchedString = match[0];
+        content = content.replace(pattern, `${getVscodeSettings.toString().slice(9)} ${matchedString}`);
+    } else {
+        return null;
+    }
+
+    const constructorPattern = /this._descriptorsMap={};/g;
+    const constructorMatch = content.match(constructorPattern);
+    if (constructorMatch) {
+        const matchedString = constructorMatch[0];
+        return content.replace(constructorPattern, `${matchedString} this.getVscodeSettings((vscodeSettingsObject) => {Object.assign(vscodeSettings, vscodeSettingsObject);});`);
+    } else {
+        return null;
+    }
 }
 
 export function applyCommonRevealerPatch(content: string) {
@@ -56,19 +105,11 @@ export function applyQuickOpenPatch(content: string) {
 }
 
 export function applyCommandMenuPatch(content: string) {
-    // This patch modifies the available options in the command menu.
-    const functionPattern = /attach\(\){const allCommands/;
-    if (content.match(functionPattern)) {
-        content = content.replace(functionPattern, `${getApprovedTabs.toString().slice(9)} attach(){const allCommands`);
-    } else {
-        return null;
-    }
-
     // pattern intended to match logic of CommandMenu.attach()
     const pattern = /for\(const action of actions\){const category=action[\s\S]+this\._commands\.sort\(commandComparator\);/;
     if(content.match(pattern)) {
-        return content.replace(pattern, `this.getApprovedTabs((networkSettings) => {
-            const networkEnabled = networkSettings.enableNetwork;
+        return content.replace(pattern, `
+            const networkEnabled = Root.Runtime.vscodeSettings.enableNetwork;
             for (const action of actions) {
               const category = action.category();
               if (!category) {
@@ -92,9 +133,7 @@ export function applyCommandMenuPatch(content: string) {
                 this._commands.push(command);
               }
             }
-            this._commands = this._commands.sort(commandComparator);
-            this._refreshCallback();
-          });`);
+            this._commands = this._commands.sort(commandComparator);`);
     } else {
         return null;
     }
@@ -199,16 +238,12 @@ export function applyAppendTabPatch(content: string) {
     if (content.match(injectionPoint)) {
         content = content.replace(
             injectionPoint,
-            // points to the getApprovedTabs, then take the out the "function" from the string (slice(9))
-            `${getApprovedTabs.toString().slice(9)};
-            appendTab(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index) {
+            `appendTab(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index) {
                 let patchedCondition = ${condition};
-                this.getApprovedTabs((approvedTabs)=>{
-                    ${applyEnableNetworkPatch()}
-                    if (!patchedCondition) {
-                        this.appendTabOverride(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index);
-                    }
-                });
+                ${applyEnableNetworkPatch()}
+                if (!patchedCondition) {
+                    this.appendTabOverride(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index);
+                }
             }}
             let EventData;
             const Events`);
@@ -245,7 +280,7 @@ export function applyEnableNetworkPatch(): string {
         return `id !== '${tab}'`;
     }).join(" && ");
 
-    return `if(approvedTabs.enableNetwork) {
+    return `if(Root.Runtime.vscodeSettings.enableNetwork) {
         patchedCondition = patchedCondition && (${networkCondition});
     }`;
 }
@@ -400,47 +435,11 @@ export function applyRemoveNonSupportedRevealContextMenu(content: string) {
     }
 }
 
-export function getThemes(callback: (arg0: object) => void) {
-    InspectorFrontendHost.InspectorFrontendHostInstance.getThemes(callback);
-}
-
 export function applyThemePatch(content: string) {
     // Sets the theme of the DevTools
-    const parameterPattern = /function init\(\)/;
-    if (content.match(parameterPattern)) {
-        content = content.replace(parameterPattern, `function init(theme)`);
-    } else {
-        return null;
-    }
-
     const setPattern = /const settingDescriptor/;
     if (content.match(setPattern)) {
-        return content.replace(setPattern, `if(theme){themeSetting.set(theme);} const settingDescriptor`);
-    } else {
-        return null;
-    }
-}
-
-export function applyMainThemePatch(content: string) {
-    // Sets the theme of the DevTools
-    const injectFunctionsPattern = /async _createAppUI/;
-    if (content.match(injectFunctionsPattern)) {
-        content = content.replace(injectFunctionsPattern, `${getThemes.toString().slice(9)} getThemePromise(){
-            const promise = new Promise(function(resolve){
-                this.getThemes((object)=>{
-                  const theme = object.theme;
-                  resolve(theme);
-                });
-              }.bind(this));
-              return promise;
-        } async _createAppUI`);
-    } else {
-        return null;
-    }
-
-    const createAppPattern = /;init\(\);/;
-    if (content.match(createAppPattern)) {
-        return content.replace(createAppPattern, `;const theme = await this.getThemePromise(); init(theme);`);
+        return content.replace(setPattern, `const theme = Root.Runtime.vscodeSettings.theme;if(theme){themeSetting.set(theme);} const settingDescriptor`);
     } else {
         return null;
     }
diff --git a/src/host/toolsHost.ts b/src/host/toolsHost.ts
index 195197f..34f4785 100644
--- a/src/host/toolsHost.ts
+++ b/src/host/toolsHost.ts
@@ -81,16 +81,10 @@ export default class ToolsHost {
         encodeMessageForChannel((msg) => window.parent.postMessage(msg, "*"), "openInEditor", request);
     }
 
-    public getApprovedTabs(callback: () => void) {
+    public getVscodeSettings(callback: (arg0: object) => void) {
         const id = this.getHostCallbacksNextId++;
         this.getHostCallbacks.set(id, callback);
-        encodeMessageForChannel((msg) => window.parent.postMessage(msg, "*"), "getApprovedTabs", {id});
-    }
-
-    public getThemes(callback: (arg0: object) => void) {
-        const id = this.getHostCallbacksNextId++;
-        this.getHostCallbacks.set(id, callback);
-        encodeMessageForChannel((msg) => window.parent.postMessage(msg, "*"), "getThemes", {id});
+        encodeMessageForChannel((msg) => window.parent.postMessage(msg, "*"), "getVscodeSettings", {id});
     }
 
     public copyText(clipboardData: string) {
@@ -125,40 +119,37 @@ export default class ToolsHost {
                 break;
             }
 
-            case "getApprovedTabs": {
-                const { id, enableNetwork } = JSON.parse(args);
-                this.fireGetHostCallback(id, {
-                        enableNetwork,
-                });
-                break;
-            }
-
-            case "getThemes": {
+            case "getVscodeSettings": {
                 const parsedArgs = JSON.parse(args);
-                const id: number = parsedArgs.id;
-                const themeString: ThemeString = parsedArgs.themeString;
-                let theme;
-                switch(themeString) {
-                    case 'System preference':
-                        theme = 'systemPreferred';
-                        break;
-                    case 'Light':
-                        theme = 'default';
-                        break;
-                    case 'Dark':
-                        theme = 'dark';
-                        break;
-                    default:
-                        theme = null;
-                }
-                this.fireGetHostCallback(id, {
-                    theme,
-                })
+                this.parseVscodeSettingsObject(parsedArgs)
             }
         }
         return true;
     }
 
+    private parseVscodeSettingsObject(vscodeObject: any) {
+        const id: number = vscodeObject.id;
+        const themeString: ThemeString = vscodeObject.themeString;
+        let theme;
+        switch(themeString) {
+            case 'System preference':
+                theme = 'systemPreferred';
+                break;
+            case 'Light':
+                theme = 'default';
+                break;
+            case 'Dark':
+                theme = 'dark';
+                break;
+            default:
+                theme = null;
+        }
+        this.fireGetHostCallback(id, {
+            enableNetwork: vscodeObject.enableNetwork,
+            theme,
+        });
+    }
+
     private sendTelemetry(telemetry: TelemetryData) {
         // Forward the data to the extension
         encodeMessageForChannel((msg) => window.parent.postMessage(msg, "*"), "telemetry", telemetry);


Test patch:
diff --git a/src/devtoolsPanel.test.ts b/src/devtoolsPanel.test.ts
index 0ac8493..2e3a750 100644
--- a/src/devtoolsPanel.test.ts
+++ b/src/devtoolsPanel.test.ts
@@ -467,45 +467,21 @@ describe("devtoolsPanel", () => {
                 expect(mockVsCode.window.showTextDocument).toHaveBeenCalled();
             });
 
-            it("calls getApprovedTabs", async () => {
+            it("calls getVscodeSettings", async () => {
                 const expectedId = { id: 0 };
-                const expectedState = { enableNetwork: true };
+                const expectedState = { enableNetwork: true, themeString: "System preference" };
                 (context.workspaceState.get as jest.Mock).mockReturnValue(expectedState);
 
                 const dtp = await import("./devtoolsPanel");
                 dtp.DevToolsPanel.createOrShow(context, mockTelemetry, "", mockRuntimeConfig);
 
-                hookedEvents.get("getApprovedTabs")!(JSON.stringify(expectedId));
+                hookedEvents.get("getVscodeSettings")!(JSON.stringify(expectedId));
                 expect(mockWebviewEvents.encodeMessageForChannel).toHaveBeenCalledWith(
                     expect.any(Function),
-                    "getApprovedTabs",
-                    {
-                        enableNetwork: expectedState.enableNetwork,
-                        id: expectedId.id,
-                    },
-                );
-
-                // Ensure that the encoded message is actually passed over to the webview
-                const expectedPostedMessage = "encodedMessage";
-                const { callback, thisObj } = getFirstCallback(mockWebviewEvents.encodeMessageForChannel);
-                callback.call(thisObj, expectedPostedMessage);
-                expect(mockPanel.webview.postMessage).toHaveBeenCalledWith(expectedPostedMessage);
-            });
-
-            it("calls getThemes", async () => {
-                const expectedId = { id: 0 };
-                const expectedState = { themeString: "System preference" };
-                (context.workspaceState.get as jest.Mock).mockReturnValue(expectedState);
-
-                const dtp = await import("./devtoolsPanel");
-                dtp.DevToolsPanel.createOrShow(context, mockTelemetry, "", mockRuntimeConfig);
-
-                hookedEvents.get("getThemes")!(JSON.stringify(expectedId));
-                expect(mockWebviewEvents.encodeMessageForChannel).toHaveBeenCalledWith(
-                    expect.any(Function),
-                    "getThemes",
+                    "getVscodeSettings",
                     {
                         themeString: expectedState.themeString,
+                        enableNetwork: expectedState.enableNetwork,
                         id: expectedId.id,
                     },
                 );
diff --git a/src/host/polyfills/simpleView.test.ts b/src/host/polyfills/simpleView.test.ts
index 42a04a4..9b9597b 100644
--- a/src/host/polyfills/simpleView.test.ts
+++ b/src/host/polyfills/simpleView.test.ts
@@ -70,7 +70,7 @@ describe("simpleView", () => {
     it("applyCommandMenuPatch correctly changes attach text for command menu", async () => {
         const filePath = "quick_open/quick_open.js";
         const patch = SimpleView.applyCommandMenuPatch;
-        const expectedStrings = ["this.getApprovedTabs((networkSettings)"];
+        const expectedStrings = ["Root.Runtime.vscodeSettings.enableNetwork;"];
 
         await testPatch(filePath, patch, expectedStrings);
     });
@@ -190,15 +190,7 @@ describe("simpleView", () => {
     it("applyThemePatch correctly modifies themes to use theme parameter", async () => {
         const filePath = "themes/themes.js";
         const patch = SimpleView.applyThemePatch;
-        const expectedStrings = ["function init(theme)", "if(theme){themeSetting.set(theme);}"];
-
-        await testPatch(filePath, patch, expectedStrings);
-    });
-
-    it("applyMainThemePatch correctly modifes main.js to pass in themes from settings", async () => {
-        const filePath = "main/main.js";
-        const patch = SimpleView.applyMainThemePatch;
-        const expectedStrings = ["resolve(theme);", "await this.getThemePromise()"];
+        const expectedStrings = ["Root.Runtime.vscodeSettings.theme;"];
 
         await testPatch(filePath, patch, expectedStrings);
     });
@@ -218,4 +210,28 @@ describe("simpleView", () => {
 
         await testPatch(filePath, patch, expectedStrings);
     });
+
+    it("applyCreateExtensionSettingsPatch correctly changes root.js to include extensionSettings global const", async () => {
+        const filePath = "root/root.js";
+        const patch = SimpleView.applyCreateExtensionSettingsPatch;
+        const expectedStrings = ["const vscodeSettings={}", "vscodeSettings:vscodeSettings"];
+
+        testPatch(filePath, patch, expectedStrings);
+    });
+
+    it("applyCreateExtensionSettingsLegacyPatch correctly changes root-legacy.js to include extensionSettings glbal const", async () => {
+        const filePath = "root/root-legacy.js";
+        const patch = SimpleView.applyCreateExtensionSettingsLegacyPatch;
+        const expectedStrings = ["Root.Runtime.vscodeSettings = RootModule.Runtime.vscodeSettings"];
+
+        testPatch(filePath, patch, expectedStrings);
+    });
+
+    it("applyPortSettingsPatch correctly changes root.js to set extensionSettings map", async () => {
+        const filePath = "root/root.js";
+        const patch = SimpleView.applyPortSettingsPatch;
+        const expectedStrings = ["InspectorFrontendHost.getVscodeSettings(callback);", "this.getVscodeSettings"];
+
+        testPatch(filePath, patch, expectedStrings);
+    });
 });