Skip to content

Commit

Permalink
Cherry Pick #1919: Fix CustomViews by switching to WebViews (#1923)
Browse files Browse the repository at this point in the history
* Cherry Pick #1919: Fix CustomViews by switching to WebViews

* Fix error in HtmlContentView.ShowContent when no JS/CSS provided (#1925)
  • Loading branch information
TylerLeonhardt authored Apr 29, 2019
1 parent 4404066 commit 455843c
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 30 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@
"devDependencies": {
"@types/mocha": "~5.2.6",
"@types/node": "~11.13.7",
"@types/rewire": "^2.5.28",
"mocha": "~5.2.0",
"mocha-junit-reporter": "~1.22.0",
"mocha-multi-reporters": "~1.1.7",
"rewire": "~4.0.1",
"tslint": "~5.16.0",
"typescript": "~3.4.5",
"vsce": "~1.59.0",
Expand Down
78 changes: 48 additions & 30 deletions src/features/CustomViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import vscode = require("vscode");
import { LanguageClient, NotificationType, RequestType } from "vscode-languageclient";
import * as path from "path";
import * as vscode from "vscode";
import { LanguageClient, RequestType } from "vscode-languageclient";
import { IFeature } from "../feature";

export class CustomViewsFeature implements IFeature {
Expand Down Expand Up @@ -94,13 +95,7 @@ class PowerShellContentProvider implements vscode.TextDocumentContentProvider {

public showView(id: string, viewColumn: vscode.ViewColumn) {
const uriString = this.getUri(id);
const view: CustomView = this.viewIndex[uriString];

vscode.commands.executeCommand(
"vscode.previewHtml",
uriString,
viewColumn,
view.title);
(this.viewIndex[uriString] as HtmlContentView).showContent(viewColumn);
}

public closeView(id: string) {
Expand Down Expand Up @@ -164,6 +159,8 @@ class HtmlContentView extends CustomView {
styleSheetPaths: [],
};

private webviewPanel: vscode.WebviewPanel;

constructor(
id: string,
title: string) {
Expand All @@ -179,42 +176,63 @@ class HtmlContentView extends CustomView {
}

public getContent(): string {
let styleSrc = "none";
let styleTags = "";

function getNonce(): number {
return Math.floor(Math.random() * 100000) + 100000;
}

if (this.htmlContent.styleSheetPaths &&
this.htmlContent.styleSheetPaths.length > 0) {
styleSrc = "";
this.htmlContent.styleSheetPaths.forEach(
(p) => {
const nonce = getNonce();
styleSrc += `'nonce-${nonce}' `;
styleTags += `<link nonce="${nonce}" href="${p}" rel="stylesheet" type="text/css" />\n`;
(styleSheetPath) => {
styleTags += `<link rel="stylesheet" href="${
styleSheetPath.toString().replace("file://", "vscode-resource://")
}">\n`;
});
}

let scriptSrc = "none";
let scriptTags = "";

if (this.htmlContent.javaScriptPaths &&
this.htmlContent.javaScriptPaths.length > 0) {
scriptSrc = "";
this.htmlContent.javaScriptPaths.forEach(
(p) => {
const nonce = getNonce();
scriptSrc += `'nonce-${nonce}' `;
scriptTags += `<script nonce="${nonce}" src="${p}"></script>\n`;
(javaScriptPath) => {
scriptTags += `<script src="${
javaScriptPath.toString().replace("file://", "vscode-resource://")
}"></script>\n`;
});
}

// Return an HTML page with the specified content
return `<html><head><meta http-equiv="Content-Security-Policy" ` +
`content="default-src 'none'; img-src *; style-src ${styleSrc}; script-src ${scriptSrc};">` +
`${styleTags}</head><body>\n${this.htmlContent.bodyContent}\n${scriptTags}</body></html>`;
return `<html><head>${styleTags}</head><body>\n${this.htmlContent.bodyContent}\n${scriptTags}</body></html>`;
}

public showContent(viewColumn: vscode.ViewColumn): void {
if (this.webviewPanel) {
this.webviewPanel.dispose();
}

let localResourceRoots: vscode.Uri[] = [];
if (this.htmlContent.javaScriptPaths) {
localResourceRoots = localResourceRoots.concat(this.htmlContent.javaScriptPaths.map((p) => {
return vscode.Uri.parse(path.dirname(p));
}));
}

if (this.htmlContent.styleSheetPaths) {
localResourceRoots = localResourceRoots.concat(this.htmlContent.styleSheetPaths.map((p) => {
return vscode.Uri.parse(path.dirname(p));
}));
}

this.webviewPanel = vscode.window.createWebviewPanel(
this.id,
this.title,
viewColumn,
{
enableScripts: true,
enableFindWidget: true,
enableCommandUris: true,
retainContextWhenHidden: true,
localResourceRoots,
});
this.webviewPanel.webview.html = this.getContent();
this.webviewPanel.reveal(viewColumn);
}
}

Expand Down
140 changes: 140 additions & 0 deletions test/features/CustomViews.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import * as assert from "assert";
import fs = require("fs");
import path = require("path");
import rewire = require("rewire");
import vscode = require("vscode");

// Setup types that are not exported.
const customViews = rewire("../../src/features/CustomViews");
const htmlContentViewClass = customViews.__get__("HtmlContentView");
const HtmlContentView: typeof htmlContentViewClass = htmlContentViewClass;

// interfaces for tests
interface ITestFile {
fileName: string;
content: string;
}

interface IHtmlContentViewTestCase {
name: string;
htmlContent: string;
javaScriptFiles: ITestFile[];
cssFiles: ITestFile[];
expectedHtmlString: string;
}

function convertToVSCodeResourceScheme(filePath: string): string {
return vscode.Uri.file(filePath).toString().replace("file://", "vscode-resource://");
}

suite("CustomViews tests", () => {
const testCases: IHtmlContentViewTestCase[] = [
// Basic test that has no js or css.
{
name: "Basic",
htmlContent: "hello",
javaScriptFiles: [],
cssFiles: [],
expectedHtmlString: `<html><head></head><body>
hello
</body></html>`,
},

// A test that adds a js file.
{
name: "With JavaScript file",
htmlContent: "hello",
javaScriptFiles: [
{
fileName: "testCustomViews.js",
content: "console.log('asdf');",
},
],
cssFiles: [],
expectedHtmlString: `<html><head></head><body>
hello
<script src="${convertToVSCodeResourceScheme(path.join(__dirname, "testCustomViews.js"))}"></script>
</body></html>`,
},

// A test that adds a js file in the current directory, and the parent directory.
{
name: "With 2 JavaScript files in two different locations",
htmlContent: "hello",
javaScriptFiles: [
{
fileName: "testCustomViews.js",
content: "console.log('asdf');",
},
{
fileName: "../testCustomViews.js",
content: "console.log('asdf');",
},
],
cssFiles: [],
expectedHtmlString: `<html><head></head><body>
hello
<script src="${convertToVSCodeResourceScheme(path.join(__dirname, "testCustomViews.js"))}"></script>
<script src="${convertToVSCodeResourceScheme(path.join(__dirname, "../testCustomViews.js"))}"></script>
</body></html>`,
},

// A test that adds a js file and a css file.
{
name: "With JavaScript and CSS file",
htmlContent: "hello",
javaScriptFiles: [
{
fileName: "testCustomViews.js",
content: "console.log('asdf');",
},
],
cssFiles: [
{
fileName: "testCustomViews.css",
content: "body: { background-color: green; }",
},
],
expectedHtmlString: `<html><head><link rel="stylesheet" href="${
convertToVSCodeResourceScheme(path.join(__dirname, "testCustomViews.css"))}">
</head><body>
hello
<script src="${convertToVSCodeResourceScheme(path.join(__dirname, "testCustomViews.js"))}"></script>
</body></html>`,
},
];

for (const testCase of testCases) {
test(`Can create an HtmlContentView and get its content - ${testCase.name}`, () => {
const htmlContentView = new HtmlContentView();

const jsPaths = testCase.javaScriptFiles.map((jsFile) => {
const jsPath: string = path.join(__dirname, jsFile.fileName);
fs.writeFileSync(jsPath, jsFile.content);
return vscode.Uri.file(jsPath).toString();
});

const cssPaths = testCase.cssFiles.map((cssFile) => {
const cssPath: string = path.join(__dirname, cssFile.fileName);
fs.writeFileSync(cssPath, cssFile.content);
return vscode.Uri.file(cssPath).toString();
});

htmlContentView.htmlContent = {
bodyContent: testCase.htmlContent,
javaScriptPaths: jsPaths,
styleSheetPaths: cssPaths,
};
try {
assert.equal(htmlContentView.getContent(), testCase.expectedHtmlString);
} finally {
jsPaths.forEach((jsPath) => fs.unlinkSync(vscode.Uri.parse(jsPath).fsPath));
cssPaths.forEach((cssPath) => fs.unlinkSync(vscode.Uri.parse(cssPath).fsPath));
}
});
}
});

0 comments on commit 455843c

Please sign in to comment.