Skip to content

Commit 455843c

Browse files
Cherry Pick #1919: Fix CustomViews by switching to WebViews (#1923)
* Cherry Pick #1919: Fix CustomViews by switching to WebViews * Fix error in HtmlContentView.ShowContent when no JS/CSS provided (#1925)
1 parent 4404066 commit 455843c

File tree

3 files changed

+190
-30
lines changed

3 files changed

+190
-30
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@
4444
"devDependencies": {
4545
"@types/mocha": "~5.2.6",
4646
"@types/node": "~11.13.7",
47+
"@types/rewire": "^2.5.28",
4748
"mocha": "~5.2.0",
4849
"mocha-junit-reporter": "~1.22.0",
4950
"mocha-multi-reporters": "~1.1.7",
51+
"rewire": "~4.0.1",
5052
"tslint": "~5.16.0",
5153
"typescript": "~3.4.5",
5254
"vsce": "~1.59.0",

src/features/CustomViews.ts

+48-30
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
44

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

910
export class CustomViewsFeature implements IFeature {
@@ -94,13 +95,7 @@ class PowerShellContentProvider implements vscode.TextDocumentContentProvider {
9495

9596
public showView(id: string, viewColumn: vscode.ViewColumn) {
9697
const uriString = this.getUri(id);
97-
const view: CustomView = this.viewIndex[uriString];
98-
99-
vscode.commands.executeCommand(
100-
"vscode.previewHtml",
101-
uriString,
102-
viewColumn,
103-
view.title);
98+
(this.viewIndex[uriString] as HtmlContentView).showContent(viewColumn);
10499
}
105100

106101
public closeView(id: string) {
@@ -164,6 +159,8 @@ class HtmlContentView extends CustomView {
164159
styleSheetPaths: [],
165160
};
166161

162+
private webviewPanel: vscode.WebviewPanel;
163+
167164
constructor(
168165
id: string,
169166
title: string) {
@@ -179,42 +176,63 @@ class HtmlContentView extends CustomView {
179176
}
180177

181178
public getContent(): string {
182-
let styleSrc = "none";
183179
let styleTags = "";
184-
185-
function getNonce(): number {
186-
return Math.floor(Math.random() * 100000) + 100000;
187-
}
188-
189180
if (this.htmlContent.styleSheetPaths &&
190181
this.htmlContent.styleSheetPaths.length > 0) {
191-
styleSrc = "";
192182
this.htmlContent.styleSheetPaths.forEach(
193-
(p) => {
194-
const nonce = getNonce();
195-
styleSrc += `'nonce-${nonce}' `;
196-
styleTags += `<link nonce="${nonce}" href="${p}" rel="stylesheet" type="text/css" />\n`;
183+
(styleSheetPath) => {
184+
styleTags += `<link rel="stylesheet" href="${
185+
styleSheetPath.toString().replace("file://", "vscode-resource://")
186+
}">\n`;
197187
});
198188
}
199189

200-
let scriptSrc = "none";
201190
let scriptTags = "";
202-
203191
if (this.htmlContent.javaScriptPaths &&
204192
this.htmlContent.javaScriptPaths.length > 0) {
205-
scriptSrc = "";
206193
this.htmlContent.javaScriptPaths.forEach(
207-
(p) => {
208-
const nonce = getNonce();
209-
scriptSrc += `'nonce-${nonce}' `;
210-
scriptTags += `<script nonce="${nonce}" src="${p}"></script>\n`;
194+
(javaScriptPath) => {
195+
scriptTags += `<script src="${
196+
javaScriptPath.toString().replace("file://", "vscode-resource://")
197+
}"></script>\n`;
211198
});
212199
}
213200

214201
// Return an HTML page with the specified content
215-
return `<html><head><meta http-equiv="Content-Security-Policy" ` +
216-
`content="default-src 'none'; img-src *; style-src ${styleSrc}; script-src ${scriptSrc};">` +
217-
`${styleTags}</head><body>\n${this.htmlContent.bodyContent}\n${scriptTags}</body></html>`;
202+
return `<html><head>${styleTags}</head><body>\n${this.htmlContent.bodyContent}\n${scriptTags}</body></html>`;
203+
}
204+
205+
public showContent(viewColumn: vscode.ViewColumn): void {
206+
if (this.webviewPanel) {
207+
this.webviewPanel.dispose();
208+
}
209+
210+
let localResourceRoots: vscode.Uri[] = [];
211+
if (this.htmlContent.javaScriptPaths) {
212+
localResourceRoots = localResourceRoots.concat(this.htmlContent.javaScriptPaths.map((p) => {
213+
return vscode.Uri.parse(path.dirname(p));
214+
}));
215+
}
216+
217+
if (this.htmlContent.styleSheetPaths) {
218+
localResourceRoots = localResourceRoots.concat(this.htmlContent.styleSheetPaths.map((p) => {
219+
return vscode.Uri.parse(path.dirname(p));
220+
}));
221+
}
222+
223+
this.webviewPanel = vscode.window.createWebviewPanel(
224+
this.id,
225+
this.title,
226+
viewColumn,
227+
{
228+
enableScripts: true,
229+
enableFindWidget: true,
230+
enableCommandUris: true,
231+
retainContextWhenHidden: true,
232+
localResourceRoots,
233+
});
234+
this.webviewPanel.webview.html = this.getContent();
235+
this.webviewPanel.reveal(viewColumn);
218236
}
219237
}
220238

test/features/CustomViews.test.ts

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import * as assert from "assert";
6+
import fs = require("fs");
7+
import path = require("path");
8+
import rewire = require("rewire");
9+
import vscode = require("vscode");
10+
11+
// Setup types that are not exported.
12+
const customViews = rewire("../../src/features/CustomViews");
13+
const htmlContentViewClass = customViews.__get__("HtmlContentView");
14+
const HtmlContentView: typeof htmlContentViewClass = htmlContentViewClass;
15+
16+
// interfaces for tests
17+
interface ITestFile {
18+
fileName: string;
19+
content: string;
20+
}
21+
22+
interface IHtmlContentViewTestCase {
23+
name: string;
24+
htmlContent: string;
25+
javaScriptFiles: ITestFile[];
26+
cssFiles: ITestFile[];
27+
expectedHtmlString: string;
28+
}
29+
30+
function convertToVSCodeResourceScheme(filePath: string): string {
31+
return vscode.Uri.file(filePath).toString().replace("file://", "vscode-resource://");
32+
}
33+
34+
suite("CustomViews tests", () => {
35+
const testCases: IHtmlContentViewTestCase[] = [
36+
// Basic test that has no js or css.
37+
{
38+
name: "Basic",
39+
htmlContent: "hello",
40+
javaScriptFiles: [],
41+
cssFiles: [],
42+
expectedHtmlString: `<html><head></head><body>
43+
hello
44+
</body></html>`,
45+
},
46+
47+
// A test that adds a js file.
48+
{
49+
name: "With JavaScript file",
50+
htmlContent: "hello",
51+
javaScriptFiles: [
52+
{
53+
fileName: "testCustomViews.js",
54+
content: "console.log('asdf');",
55+
},
56+
],
57+
cssFiles: [],
58+
expectedHtmlString: `<html><head></head><body>
59+
hello
60+
<script src="${convertToVSCodeResourceScheme(path.join(__dirname, "testCustomViews.js"))}"></script>
61+
</body></html>`,
62+
},
63+
64+
// A test that adds a js file in the current directory, and the parent directory.
65+
{
66+
name: "With 2 JavaScript files in two different locations",
67+
htmlContent: "hello",
68+
javaScriptFiles: [
69+
{
70+
fileName: "testCustomViews.js",
71+
content: "console.log('asdf');",
72+
},
73+
{
74+
fileName: "../testCustomViews.js",
75+
content: "console.log('asdf');",
76+
},
77+
],
78+
cssFiles: [],
79+
expectedHtmlString: `<html><head></head><body>
80+
hello
81+
<script src="${convertToVSCodeResourceScheme(path.join(__dirname, "testCustomViews.js"))}"></script>
82+
<script src="${convertToVSCodeResourceScheme(path.join(__dirname, "../testCustomViews.js"))}"></script>
83+
</body></html>`,
84+
},
85+
86+
// A test that adds a js file and a css file.
87+
{
88+
name: "With JavaScript and CSS file",
89+
htmlContent: "hello",
90+
javaScriptFiles: [
91+
{
92+
fileName: "testCustomViews.js",
93+
content: "console.log('asdf');",
94+
},
95+
],
96+
cssFiles: [
97+
{
98+
fileName: "testCustomViews.css",
99+
content: "body: { background-color: green; }",
100+
},
101+
],
102+
expectedHtmlString: `<html><head><link rel="stylesheet" href="${
103+
convertToVSCodeResourceScheme(path.join(__dirname, "testCustomViews.css"))}">
104+
</head><body>
105+
hello
106+
<script src="${convertToVSCodeResourceScheme(path.join(__dirname, "testCustomViews.js"))}"></script>
107+
</body></html>`,
108+
},
109+
];
110+
111+
for (const testCase of testCases) {
112+
test(`Can create an HtmlContentView and get its content - ${testCase.name}`, () => {
113+
const htmlContentView = new HtmlContentView();
114+
115+
const jsPaths = testCase.javaScriptFiles.map((jsFile) => {
116+
const jsPath: string = path.join(__dirname, jsFile.fileName);
117+
fs.writeFileSync(jsPath, jsFile.content);
118+
return vscode.Uri.file(jsPath).toString();
119+
});
120+
121+
const cssPaths = testCase.cssFiles.map((cssFile) => {
122+
const cssPath: string = path.join(__dirname, cssFile.fileName);
123+
fs.writeFileSync(cssPath, cssFile.content);
124+
return vscode.Uri.file(cssPath).toString();
125+
});
126+
127+
htmlContentView.htmlContent = {
128+
bodyContent: testCase.htmlContent,
129+
javaScriptPaths: jsPaths,
130+
styleSheetPaths: cssPaths,
131+
};
132+
try {
133+
assert.equal(htmlContentView.getContent(), testCase.expectedHtmlString);
134+
} finally {
135+
jsPaths.forEach((jsPath) => fs.unlinkSync(vscode.Uri.parse(jsPath).fsPath));
136+
cssPaths.forEach((cssPath) => fs.unlinkSync(vscode.Uri.parse(cssPath).fsPath));
137+
}
138+
});
139+
}
140+
});

0 commit comments

Comments
 (0)