From 98c40e513e06226b81891a3a9b45a1e2634c9106 Mon Sep 17 00:00:00 2001 From: Brian Fitzpatrick Date: Fri, 29 Jan 2021 09:42:07 -0700 Subject: [PATCH] FUSETOOLS2-725 - Removing single window limit on didact (#365) * FUSETOOLS2-725 - Removing single window limit on didact * updates to support single tutorial window * fix for title changes * fix issue with disposed webview when saving column * adding first cut of new didact panel and manager * first solid pass with most things working * working through all tests and clean-up * adding tests and updates after first round of feedback * persist uri for easier identification * also reuse old panels (based on uri) to avoid opening a new window each time * add test to ensure that if we open the same didact URI twice we only end up with one panel * combining if conditions Signed-off-by: Brian Fitzpatrick --- CHANGELOG.md | 4 + demos/asciidoc/didact-demo.didact.adoc | 3 +- demos/markdown/didact-demo.didact.md | 3 +- media/main.js | 37 +- package-lock.json | 5 + package.json | 28 +- resources/didactCompletionCatalog.json | 12 - src/didactManager.ts | 121 ++++ src/didactPanel.ts | 423 ++++++++++++++ src/didactPanelSerializer.ts | 44 ++ src/didactWebView.ts | 526 ------------------ src/doublyLinkedList.ts | 82 --- src/extension.ts | 20 +- src/extensionFunctions.ts | 157 ++---- src/history.ts | 58 -- src/test/data/didactForReload.didact.md | 7 + src/test/suite/commandHandler.test.ts | 55 +- src/test/suite/didact.test.ts | 41 +- ...actWebView.test.ts => didactPanel.test.ts} | 93 +++- .../didactUriCompletionItemProvider.test.ts | 2 +- src/test/suite/extensionFunctions.test.ts | 54 -- 21 files changed, 796 insertions(+), 979 deletions(-) create mode 100644 src/didactManager.ts create mode 100644 src/didactPanel.ts create mode 100644 src/didactPanelSerializer.ts delete mode 100644 src/didactWebView.ts delete mode 100644 src/doublyLinkedList.ts delete mode 100644 src/history.ts create mode 100644 src/test/data/didactForReload.didact.md rename src/test/suite/{didactWebView.test.ts => didactPanel.test.ts} (52%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f225936..ef5fbab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to the "vscode-didact" extension will be documented in this ## 0.2.1 - Switching from xmldom to node-html-parser for heading parsing +- remove history functions +- open a new didact window with each tutorial +- persist html state between workspace sessions +- remove old webview implementation ## 0.2.0 diff --git a/demos/asciidoc/didact-demo.didact.adoc b/demos/asciidoc/didact-demo.didact.adoc index 2a2c5da1..112e078d 100644 --- a/demos/asciidoc/didact-demo.didact.adoc +++ b/demos/asciidoc/didact-demo.didact.adoc @@ -98,8 +98,7 @@ Asciidoc doesn't seem to support bringing in native HTML. But you can bring in t ## Limitations -* We can only open one Didact file at a time at the moment and there isn't the concept of a tutorial "history" to step forward or back through yet. -* Didact, because it utilizes the VS Code Webview, is limited to what files it can access. For example, local image files must exist in the same folder as the didact file or in a child folder. +* Didact, beacause it utilizes the VS Code Webview, is limited to what files it can access. For example, local image files must exist in the same folder as the didact file or in a child folder. # Ideas or want to contribute? diff --git a/demos/markdown/didact-demo.didact.md b/demos/markdown/didact-demo.didact.md index 277206ea..dce82369 100644 --- a/demos/markdown/didact-demo.didact.md +++ b/demos/markdown/didact-demo.didact.md @@ -111,8 +111,7 @@ And you can even bring in tables... ## Limitations -* We can only open one Didact file at a time at the moment and there isn't the concept of a tutorial "history" to step forward or back through yet. -* Didact, because it utilizes the VS Code Webview, is limited to what files it can access. For example, local image files must exist in the same folder as the didact file or in a child folder. +* Didact, beacause it utilizes the VS Code Webview, is limited to what files it can access. For example, local image files must exist in the same folder as the didact file or in a child folder. # Ideas or want to contribute? diff --git a/media/main.js b/media/main.js index d22a7a56..7fd109cb 100644 --- a/media/main.js +++ b/media/main.js @@ -22,6 +22,17 @@ function () { //connect to the vscode api const vscode = acquireVsCodeApi(); + const oldState = vscode.getState(); + let oldBody = oldState ? oldState.oldbody : ''; + if (oldBody) { + const textFromBase64 = window.btoa(oldBody); + document.body = decodeURI(textFromBase64); + } + let oldTitle = oldState ? oldState.oldTitle : ''; + if (oldTitle) { + this.title = oldTitle; + } + document.body.addEventListener('click', event => { let node = event && event.target; while (node) { @@ -41,6 +52,7 @@ function () { element.checked = true; if (document.body) { vscode.postMessage({ command: 'update', text: document.body }); + updateState(); } } @@ -70,17 +82,25 @@ function () { return elements; } + function updateState(passedUri) { + const textToCache = '' + '\n' + document.documentElement.outerHTML; + const encodedText = encodeURI(textToCache); + const textToBase64 = window.btoa(encodedText); + vscode.setState( { oldBody: textToBase64, oldTitle : this.title, oldUri : passedUri }); + } + // Handle messages sent from the extension to the webview window.addEventListener('message', event => { const message = event.data; // The json data that the extension sent const json = JSON.parse(message); - switch (json.command) { - case 'requirementCheck': - const requirementName = json.requirementName; - const isAvailable = json.result; + const requirementName = json.requirementName; + const isAvailable = json.result; + const passedUri = json.oldUri; - let element = document.getElementById(requirementName); + let element = document.getElementById(requirementName); + switch (json.command) { + case 'requirementCheck': // add check for adoc div/p requirement label if (element.tagName.toLowerCase() === 'div' && element.childNodes.length > 0) { let list = element.getElementsByTagName('em'); @@ -101,7 +121,9 @@ function () { } } console.log(`${requirementName} is available: ${isAvailable}`); + updateState(); break; + case 'allRequirementCheck': var links = collectElements("a"); for (let index = 0; index < links.length; index++) { @@ -115,6 +137,11 @@ function () { } } } + updateState(); + break; + + case 'setState': + updateState(passedUri); break; } }); diff --git a/package-lock.json b/package-lock.json index 0f3fb32d..366311be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -648,6 +648,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" + }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", diff --git a/package.json b/package.json index b6c717c2..d73aba42 100644 --- a/package.json +++ b/package.json @@ -109,21 +109,6 @@ "command": "vscode.didact.verifyCommands", "title": "Validate Didact File", "when": "resourceFilename =~ /[.](didact)[.](md|adoc)$/" - }, - { - "command": "vscode.didact.historyBack", - "title": "Didact: Show Previous Entry in the Didact History", - "when": "didact.webview" - }, - { - "command": "vscode.didact.historyForward", - "title": "Didact: Show Next Entry in the Didact History", - "when": "didact.webview" - }, - { - "command": "vscode.didact.clearHistory", - "title": "Didact: Clear History", - "when": "didact.webview" } ], "keybindings": [ @@ -132,18 +117,6 @@ "key": "ctrl+shift+v", "mac": "cmd+shift+v", "when": "editorFocus && resourceFilename =~ /[.](didact)[.](md|adoc)$/" - }, - { - "command": "vscode.didact.historyBack", - "key": "alt+down", - "mac": "alt+down", - "when": "didact.webview" - }, - { - "command": "vscode.didact.historyForward", - "key": "alt+up", - "mac": "alt+up", - "when": "didact.webview" } ], "menus": { @@ -244,6 +217,7 @@ "@types/markdown-it": "^12.0.1", "@types/request-promise": "^4.1.47", "asciidoctor": "^2.2.1", + "base-64": "^1.0.0", "download": "^8.0.0", "glob": "^7.1.6", "markdown-it": "^12.0.4", diff --git a/resources/didactCompletionCatalog.json b/resources/didactCompletionCatalog.json index 206bef5c..3c4a0d59 100644 --- a/resources/didactCompletionCatalog.json +++ b/resources/didactCompletionCatalog.json @@ -4,10 +4,6 @@ "fullCommandId":"didact.tutorials.focus", "documentation": "Focuses VS Code on the Didact Tutorials view" }, - { - "fullCommandId":"vscode.didact.clearHistory", - "documentation": "Clears the Didact tutorial history" - }, { "fullCommandId":"vscode.didact.cliCommandSuccessful", "parms": ["Requirement-Label", "URLEncoded-Command-to-Execute"], @@ -42,14 +38,6 @@ "parms": ["Requirement-Label", "Full-Extension-ID"], "documentation": "Simple check to see if the extension Id is installed in the user workspace" }, - { - "fullCommandId":"vscode.didact.historyBack", - "documentation": "Move back one entry in the Didact tutorial history" - }, - { - "fullCommandId":"vscode.didact.historyForward", - "documentation": "Move forward one entry in the Didact tutorial history" - }, { "fullCommandId":"vscode.didact.openNamedOutputChannel", "parms": ["Output-Channel-Name"], diff --git a/src/didactManager.ts b/src/didactManager.ts new file mode 100644 index 00000000..4f024e5e --- /dev/null +++ b/src/didactManager.ts @@ -0,0 +1,121 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; +import {ExtensionContext, Uri, ViewColumn} from 'vscode'; +import { DidactPanel } from './didactPanel'; +import { isAsciiDoc } from './extensionFunctions'; + +export const DEFAULT_TITLE_VALUE = `Didact Tutorial`; +export const VIEW_TYPE = 'didact'; + +export class DidactManager { + + // singleton instance + private static _instance: DidactManager; + private _panels: DidactPanel[] = []; + private context : ExtensionContext | undefined = undefined; + + private constructor() { + // empty + } + + public static get Instance() : DidactManager { + return this._instance || (this._instance = new this()); + } + + public async create(didactUri : Uri, column? : ViewColumn) : Promise { + if (didactUri) { + let panel : DidactPanel | undefined; + let reload = false; + if(this.getByUri(didactUri)) { + panel = this.getByUri(didactUri); + column = panel?.getColumn(); + panel?._panel?.dispose(); + reload = true; + } + panel = new DidactPanel(didactUri); + if (panel) { + if (!column) { + column = ViewColumn.Active; + } + panel.initWebviewPanel(column, didactUri); + panel.setDidactUriPath(didactUri); + panel.setIsAsciiDoc(isAsciiDoc()); + panel.handleEvents(); + await panel.configure(reload); + } + return panel; + } + return undefined; + } + + public add(panel: DidactPanel): void { + if (panel) { + this._panels.push(panel); + } + } + + public remove(panel: DidactPanel): void { + if (panel) { + const found = this._panels.indexOf(panel); + if (found >= 0) { + this._panels.splice(found, 1); + } + } + } + + public active(): DidactPanel | undefined { + return this._panels.find(p => p.visible); + } + + public resetVisibility() : void { + this._panels.forEach(p => p.visible = false); + } + + public setContext(ctxt : ExtensionContext): void { + this.context = ctxt; + } + + public getExtensionPath() : string | undefined { + if (this.context) { + return this.context.extensionPath; + } + return undefined; + } + + // for test purposes + public countPanels() : number { + return this._panels.length; + } + + public getByUri(testUri: Uri): DidactPanel | undefined { + let returnPanel = undefined; + for (let index = 0; index < this._panels.length; index++) { + const p = this._panels[index]; + const originalUri = p.getDidactUriPath(); + if (testUri && originalUri && originalUri.toString() === testUri.toString()) { + returnPanel = p; + break; + } + } + return returnPanel; + } +} + +// export preview manager singleton +export const didactManager = DidactManager.Instance; diff --git a/src/didactPanel.ts b/src/didactPanel.ts new file mode 100644 index 00000000..977bc28c --- /dev/null +++ b/src/didactPanel.ts @@ -0,0 +1,423 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as vscode from 'vscode'; +import * as extensionFunctions from './extensionFunctions'; +import * as path from 'path'; +import { ViewColumn } from 'vscode'; +import { DIDACT_DEFAULT_URL } from './utils'; +import { DEFAULT_TITLE_VALUE, didactManager, VIEW_TYPE } from './didactManager'; +import * as commandHandler from './commandHandler'; + +export class DidactPanel { + + // public for testing purposes + public _panel: vscode.WebviewPanel | undefined; + + private _disposables: vscode.Disposable[] = []; + private currentHtml : string | undefined = undefined; + private didactUriPath : vscode.Uri | undefined = undefined; + private defaultTitle = DEFAULT_TITLE_VALUE; + private isAsciiDoc = false; + private _disposed = false; + public visible = false; + + public constructor(uri?: vscode.Uri ) { + this.didactUriPath = uri; + didactManager.add(this); + } + + public initWebviewPanel(viewColumn: ViewColumn, inpath?: vscode.Uri | undefined): DidactPanel | undefined { + const extPath = didactManager.getExtensionPath(); + if (!extPath) { + console.error(`Error: Extension context not set on Didact manager`); + return undefined; + } + + // Otherwise, create a new panel. + const localResourceRoots = [vscode.Uri.file(path.resolve(extPath, 'media'))]; + if (inpath) { + const dirName = path.dirname(inpath.fsPath); + localResourceRoots.push(vscode.Uri.file(dirName)); + } + + const localIconPath = vscode.Uri.file(path.resolve(extPath, 'icon/logo.svg')); + const iconDirPath = path.dirname(localIconPath.fsPath); + localResourceRoots.push(vscode.Uri.file(iconDirPath)); + + const panel = vscode.window.createWebviewPanel( + VIEW_TYPE, this.defaultTitle, viewColumn, + { + // Enable javascript in the webview + enableScripts: true, + + // And restrict the webview to only loading content from known directories + localResourceRoots: localResourceRoots, + + // persist the state + retainContextWhenHidden: true + } + ); + panel.iconPath = localIconPath; + + return this.attachWebviewPanel(panel); + } + + public attachWebviewPanel(webviewPanel: vscode.WebviewPanel): DidactPanel { + this._panel = webviewPanel; + this.setVisible(webviewPanel.active); + this._panel.onDidDispose(() => { + this.dispose(); + }, this, this._disposables); + return this; + } + + private setVisible(flag: boolean) { + didactManager.resetVisibility(); + this.visible = flag; + } + + public static revive(context: vscode.ExtensionContext, webviewPanel: vscode.WebviewPanel, oldBody? : string, oldUri? : string): DidactPanel { + didactManager.setContext(context); + + let panel : DidactPanel; + if (oldUri) { + const toUri = vscode.Uri.parse(oldUri); + panel = new DidactPanel(toUri); + } else { + panel = new DidactPanel(); + } + panel.attachWebviewPanel(webviewPanel); + panel.handleEvents(); + panel.configure(); + if (oldBody) { + panel.setHtml(oldBody); + } + return panel; + } + + public handleEvents() : void { + this._panel?.webview.onDidReceiveMessage( + async message => { + console.log(message); + switch (message.command) { + case 'update': + if (message.text) { + this.currentHtml = message.text; + } + return; + case 'link': + if (message.text) { + try { + await commandHandler.processInputs(message.text, didactManager.getExtensionPath()); + } catch (error) { + vscode.window.showErrorMessage(`Didact was unable to call commands: ${message.text}: ${error}`); + } + } + return; + } + }, + null, + this._disposables + ); + + this._panel?.onDidChangeViewState( async (e) => { + this.setVisible(e.webviewPanel.active); + await this.sendSetStateMessage(); + }); + } + + public async sendSetStateMessage() : Promise { + if (!this._panel || this._disposed) { + return; + } + const sendCommand = `"command": "setState"`; + let sendUri = undefined; + if (this.didactUriPath) { + const encodedUri = encodeURI(this.didactUriPath.toString()); + sendUri = `"oldUri" : "${encodedUri}"`; + } + + let jsonMsg = `{ ${sendCommand} }`; + if (sendUri) { + jsonMsg = `{ ${sendCommand}, ${sendUri} }`; + } + this._panel.webview.postMessage(jsonMsg); + } + + async configure(flag = false): Promise { + this._update(flag); + await this.sendSetStateMessage(); + } + + public setHtml(html : string) : void { + if (this._panel) { + this._panel.webview.html = html; + } + } + + public getCurrentHTML() : string | undefined { + return this._panel?.webview.html; + } + + public getCurrentTitle() : string | undefined { + return this._panel?.title; + } + + public getDidactUriPath(): vscode.Uri | undefined { + return this.didactUriPath; + } + + public setIsAsciiDoc(flag : boolean): void { + this.isAsciiDoc = flag; + } + + // public for testing purposes + public getDidactDefaultTitle() : string | undefined { + return this.defaultTitle; + } + + public setDidactUriPath(inpath : vscode.Uri | undefined): void { + this.didactUriPath = inpath; + if (inpath) { + const tempFilename = path.basename(inpath.fsPath); + this.defaultTitle = tempFilename; + } + this._update(true); + } + + private async _update(flag: boolean) { + if (flag) { // reset based on vscode link + const content = await extensionFunctions.getWebviewContent(); + if (content) { + this.currentHtml = this.wrapDidactContent(content); + } + } + if (this.currentHtml) { + if (this._panel && this._panel.webview && this._panel.active) { + this._panel.webview.html = this.currentHtml; + const firstHeading : string | undefined = this.getFirstHeadingText(); + if (firstHeading && firstHeading.trim().length > 0) { + this.defaultTitle = firstHeading; + } + this._panel.title = this.defaultTitle; + } + } + await this.sendSetStateMessage(); + } + + wrapDidactContent(didactHtml: string | undefined) : string | undefined { + if (!didactHtml || this._disposed) { + return; + } + const nonce = this.getNonce(); + + // Base uri to support images + const didactUri : vscode.Uri = this.didactUriPath as vscode.Uri; + + let uriBaseHref = undefined; + if (didactUri && this._panel) { + try { + const didactUriPath = path.dirname(didactUri.fsPath); + const uriBase = this._panel.webview.asWebviewUri(vscode.Uri.file(didactUriPath)).toString(); + uriBaseHref = ``; + } catch (error) { + console.error(error); + } + } + + const extPath = didactManager.getExtensionPath(); + if (!extPath) { + console.error(`Error: Extension context not set on Didact manager`); + return undefined; + } + + // Local path to main script run in the webview + const scriptPathOnDisk = vscode.Uri.file( + path.resolve(extPath, 'media', 'main.js') + ); + + // And the uri we use to load this script in the webview + const scriptUri = scriptPathOnDisk.with({ scheme: 'vscode-resource' }); + + // the cssUri is our path to the stylesheet included in the security policy + const cssPathOnDisk = vscode.Uri.file( + path.resolve(extPath, 'media', 'webviewslim.css') + ); + const cssUri = cssPathOnDisk.with({ scheme: 'vscode-resource' }); + + // this css holds our overrides for both asciidoc and markdown html + const cssUriHtml = ``; + + // process the stylesheet details for asciidoc or markdown-based didact files + const stylesheetHtml = this.produceStylesheetHTML(cssUriHtml); + + const extensionHandle = vscode.extensions.getExtension(extensionFunctions.EXTENSION_ID); + let didactVersionLabel = 'Didact'; + if (extensionHandle) { + const didactVersion = extensionHandle.packageJSON.version; + if (didactVersion) { + didactVersionLabel += ` ${didactVersion}`; + } + } + + let cspSrc = undefined; + if (this._panel) { + cspSrc = this._panel.webview.cspSource; + } else { + console.error(`Error: Content Security Policy not set on webview`); + return undefined; + } + + let metaHeader = ` + + `; + if (uriBaseHref) { + metaHeader += `\n${uriBaseHref}\n`; + } + + const completedHtml = ` + + + ${metaHeader} + Didact Tutorial` + + stylesheetHtml + + ` + + +
` + + didactHtml + + `
+
${didactVersionLabel}
+ - - -
` - + didactHtml + - `
-
${didactVersionLabel}
-