From a9953b696d0aaffd897a3730c75c3863c71b995e Mon Sep 17 00:00:00 2001 From: Josh Johnson Date: Thu, 15 Jun 2017 23:55:04 -0400 Subject: [PATCH] Re-Implement XML Tree View Leverages the new "views" API in VS Code to show a tree view in the file explorer sidebar. Clicking a tree node navigates the editor to the line containing the source element/attribute. The tree can be navigates via keyboard by using the UP/DOWN arrow keys to choose nodes and the LEFT/RIGHT arrow keys to collapse/expand nodes. Addresses the following issues: #62 #66 #71 #72 #73 #83 #102 --- README.md | 7 +- package.json | 21 +-- resources/icons/attribute.dark.svg | 35 +++++ resources/icons/attribute.light.svg | 56 +++++++ resources/icons/element.dark.svg | 42 ++++++ resources/icons/element.light.svg | 54 +++++++ src/Commands.ts | 18 +-- src/Extension.ts | 13 +- src/providers/Content.ts | 28 ---- src/providers/XmlTreeView.ts | 128 ++++++++++++++++ src/services/XmlTreeService.ts | 222 ---------------------------- 11 files changed, 341 insertions(+), 283 deletions(-) create mode 100644 resources/icons/attribute.dark.svg create mode 100644 resources/icons/attribute.light.svg create mode 100644 resources/icons/element.dark.svg create mode 100644 resources/icons/element.light.svg delete mode 100644 src/providers/Content.ts create mode 100644 src/providers/XmlTreeView.ts delete mode 100644 src/services/XmlTreeService.ts diff --git a/README.md b/README.md index 4ff06e8..1e2c266 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,9 @@ Detailed release notes are available [here](https://github.com/DotJoshJohnson/vs Run into a bug? Report it [here](https://github.com/DotJoshJohnson/vscode-xml/issues). ## Roadmap -Check out development progress [here](https://github.com/DotJoshJohnson/vscode-xml/projects/1). \ No newline at end of file +Check out development progress [here](https://github.com/DotJoshJohnson/vscode-xml/projects/1). + +## Icon Credits +Icons used in the XML Tree View are used under the Creative Commons 3.0 BY license. +* "Code" icon by Dave Gandy from www.flaticon.com +* "At" icon by FreePik from www.flaticon.com \ No newline at end of file diff --git a/package.json b/package.json index 2d5fc81..cb5b023 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "url": "https://github.com/DotJoshJohnson/vscode-xml/issues" }, "engines": { - "vscode": "^0.10.7", + "vscode": "^1.13.0", "node": "^0.12.0" }, "categories": [ @@ -46,10 +46,6 @@ "command": "xmlTools.executeXQuery", "title": "XML Tools: Execute XQuery" }, - { - "command": "xmlTools.viewXmlTree", - "title": "XML Tools: View XML Tree" - }, { "command": "xmlTools.formatAsXml", "title": "XML Tools: Format as XML" @@ -115,7 +111,15 @@ "scopeName": "source.xquery", "path": "./languages/xquery/xquery.tmLanguage" } - ] + ], + "views": { + "explorer": [ + { + "id": "xmlTreeView", + "name": "XML Document" + } + ] + } }, "activationEvents": [ "onLanguage:xml", @@ -124,12 +128,11 @@ "onCommand:xmlTools.minifyXml", "onCommand:xmlTools.evaluateXPath", "onCommand:xmlTools.executeXQuery", - "onCommand:xmlTools.viewXmlTree", "onCommand:xmlTools.formatAsXml" ], "devDependencies": { - "vscode": "^0.11.8", - "typescript": "^1.6.2", + "vscode": "^1.1.0", + "typescript": "^2.3.4", "gulp": "^3.9.0", "gulp-shell": "^0.5.1" }, diff --git a/resources/icons/attribute.dark.svg b/resources/icons/attribute.dark.svg new file mode 100644 index 0000000..f208dc3 --- /dev/null +++ b/resources/icons/attribute.dark.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/attribute.light.svg b/resources/icons/attribute.light.svg new file mode 100644 index 0000000..2c5fc76 --- /dev/null +++ b/resources/icons/attribute.light.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/element.dark.svg b/resources/icons/element.dark.svg new file mode 100644 index 0000000..45eced5 --- /dev/null +++ b/resources/icons/element.dark.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/element.light.svg b/resources/icons/element.light.svg new file mode 100644 index 0000000..8e6cfd1 --- /dev/null +++ b/resources/icons/element.light.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Commands.ts b/src/Commands.ts index 7a5d6f9..6cb3ef8 100644 --- a/src/Commands.ts +++ b/src/Commands.ts @@ -7,7 +7,6 @@ import { RangeUtil } from './utils/RangeUtil'; import { XmlFormatter } from './services/XmlFormatter'; import { XPathFeatureProvider } from './providers/XPath'; import { XQueryExecutionProvider } from './providers/Execution'; -import { XmlTreeDocumentContentProvider } from './providers/Content'; import { XmlFormattingEditProvider } from './providers/Formatting'; const CFG_SECTION: string = 'xmlTools'; @@ -32,26 +31,13 @@ export class TextEditorCommands { static executeXQuery(editor: vsc.TextEditor, edit: vsc.TextEditorEdit): void { XQueryExecutionProvider.executeXQueryAsync(editor); } - - static async viewXmlTree(editor: vsc.TextEditor, edit: vsc.TextEditorEdit): Promise { - try { - await vsc.commands.executeCommand( - 'vscode.previewHtml', - XmlTreeDocumentContentProvider.buildUri(editor.document.uri), - vsc.ViewColumn.Three); - } - - catch (error) { - vsc.window.showErrorMessage(`The XML Tree could not be created: ${error}`); - } - } static formatAsXml(editor: vsc.TextEditor, edit: vsc.TextEditorEdit): void { let edits: vsc.TextEdit[]; let formattingEditProvider = new XmlFormattingEditProvider(); let formattingOptions: vsc.FormattingOptions = { - insertSpaces: editor.options.insertSpaces, - tabSize: editor.options.tabSize + insertSpaces: (editor.options.insertSpaces as boolean), + tabSize: (editor.options.tabSize as number) }; // if the user has selected text, only format what is selected diff --git a/src/Extension.ts b/src/Extension.ts index 727ccc7..8570b99 100644 --- a/src/Extension.ts +++ b/src/Extension.ts @@ -5,7 +5,7 @@ import { TextEditorCommands } from './Commands'; import { XmlFormattingEditProvider } from './providers/Formatting'; import { XQueryLintingFeatureProvider } from './providers/Linting'; import { XQueryCompletionItemProvider } from './providers/Completion'; -import { XmlTreeDocumentContentProvider } from './providers/Content'; +import { XmlTreeViewDataProvider } from "./providers/XmlTreeView"; export var GlobalState: vsc.Memento; export var WorkspaceState: vsc.Memento; @@ -26,7 +26,6 @@ export function activate(ctx: vsc.ExtensionContext) { vsc.commands.registerTextEditorCommand('xmlTools.minifyXml', TextEditorCommands.minifyXml), vsc.commands.registerTextEditorCommand('xmlTools.evaluateXPath', TextEditorCommands.evaluateXPath), vsc.commands.registerTextEditorCommand('xmlTools.executeXQuery', TextEditorCommands.executeXQuery), - vsc.commands.registerTextEditorCommand('xmlTools.viewXmlTree', TextEditorCommands.viewXmlTree), vsc.commands.registerTextEditorCommand('xmlTools.formatAsXml', TextEditorCommands.formatAsXml) ); @@ -38,16 +37,16 @@ export function activate(ctx: vsc.ExtensionContext) { vsc.languages.registerCompletionItemProvider(LANG_XQUERY, new XQueryCompletionItemProvider(), ':', '$') ); - // register workspace feature providers - ctx.subscriptions.push( - vsc.workspace.registerTextDocumentContentProvider(XmlTreeDocumentContentProvider.SCHEME, new XmlTreeDocumentContentProvider()) - ); - // listen to editor events (for linting) ctx.subscriptions.push( vsc.window.onDidChangeActiveTextEditor(_handleChangeActiveTextEditor), vsc.window.onDidChangeTextEditorSelection(_handleChangeTextEditorSelection) ); + + // add views + ctx.subscriptions.push( + vsc.window.registerTreeDataProvider("xmlTreeView", new XmlTreeViewDataProvider(ctx)) + ); } export function deactivate() { diff --git a/src/providers/Content.ts b/src/providers/Content.ts deleted file mode 100644 index b6a094b..0000000 --- a/src/providers/Content.ts +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -import * as vsc from 'vscode'; -import { XmlTreeService } from '../services/XmlTreeService'; - -export class XmlTreeDocumentContentProvider implements vsc.TextDocumentContentProvider { - - static get SCHEME(): string { - return "xmltree"; - } - - static buildUri(sourceUri: vsc.Uri): vsc.Uri { - let uriStr: string = `xmltree://${encodeURIComponent(sourceUri.toString())}`; - - let uri: vsc.Uri = vsc.Uri.parse(uriStr); - - return uri; - } - - async provideTextDocumentContent(uri: vsc.Uri): Promise { - let sourceUri: vsc.Uri = vsc.Uri.parse(decodeURIComponent(uri.toString().substr(10))); - let document: vsc.TextDocument = await vsc.workspace.openTextDocument(sourceUri); - - let html: string = XmlTreeService.getXmlTreeHtml(document.getText()); - - return Promise.resolve(html); - } -} \ No newline at end of file diff --git a/src/providers/XmlTreeView.ts b/src/providers/XmlTreeView.ts new file mode 100644 index 0000000..ed084e1 --- /dev/null +++ b/src/providers/XmlTreeView.ts @@ -0,0 +1,128 @@ +import * as vsc from "vscode"; +import * as path from "path"; + +let DOMParser = require("xmldom").DOMParser; + +export class XmlTreeViewDataProvider implements vsc.TreeDataProvider { + private _onDidChangeTreeData: vsc.EventEmitter = new vsc.EventEmitter(); + private _xmlDocument: Document; + + constructor(private _context: vsc.ExtensionContext) { + vsc.window.onDidChangeActiveTextEditor((editor) => { + this._refreshTree(); + }); + + vsc.workspace.onDidChangeTextDocument((e) => { + this._refreshTree(); + }); + } + + readonly onDidChangeTreeData: vsc.Event = this._onDidChangeTreeData.event; + + get activeEditor(): vsc.TextEditor | null { + return vsc.window.activeTextEditor || null; + } + + getChildren(element?: Node): Node[] { + if (!this._xmlDocument) { + this._refreshTree(); + } + + if (element) { + return [].concat(this._getChildAttributeArray(element), this._getChildElementArray(element)); + } + + else if (this._xmlDocument) { + return [ this._xmlDocument.lastChild ]; + } + + else { + return []; + } + } + + getTreeItem(element: Node): vsc.TreeItem { + let treeItem = new vsc.TreeItem(element.localName); + + if (this._getChildAttributeArray(element).length > 0) { + treeItem.collapsibleState = vsc.TreeItemCollapsibleState.Collapsed; + } + + if (this._getChildElementArray(element).length > 0) { + treeItem.collapsibleState = vsc.TreeItemCollapsibleState.Collapsed; + } + + treeItem.command = { + command: "revealLine", + title: "", + arguments: [{ + lineNumber: (element as any).lineNumber - 1, + at: "top" + }] + }; + + treeItem.iconPath = this._getIcon(element); + + return treeItem; + } + + private _getChildAttributeArray(node: Node): Node[] { + if (!node.attributes) { + return []; + } + + let array = new Array(); + + for (let i = 0; i < node.attributes.length; i++) { + array.push(node.attributes[i]); + } + + return array; + } + + private _getChildElementArray(node: Node): Node[] { + if (!node.childNodes) { + return []; + } + + let array = new Array(); + + for (let i = 0; i < node.childNodes.length; i++) { + let child = node.childNodes[i]; + + if ((child as any).tagName) { + array.push(child); + } + } + + return array; + } + + private _getIcon(element: Node): any { + let type = "element"; + + if (!(element as any).tagName) { + type = "attribute"; + } + + let icon = { + dark: this._context.asAbsolutePath(path.join("resources", "icons", `${type}.dark.svg`)), + light: this._context.asAbsolutePath(path.join("resources", "icons", `${type}.light.svg`)) + }; + + return icon; + } + + private _refreshTree(): void { + if (!this.activeEditor || this.activeEditor.document.languageId !== "xml") { + this._xmlDocument = null; + this._onDidChangeTreeData.fire(); + return; + } + + let xml = this.activeEditor.document.getText(); + this._xmlDocument = new DOMParser().parseFromString(xml, "text/xml"); + + this._onDidChangeTreeData.fire(); + } +} \ No newline at end of file diff --git a/src/services/XmlTreeService.ts b/src/services/XmlTreeService.ts deleted file mode 100644 index f18c97b..0000000 --- a/src/services/XmlTreeService.ts +++ /dev/null @@ -1,222 +0,0 @@ -'use strict'; - -let DOMParser = require('xmldom').DOMParser; -let fs = require('fs'); - -export class XmlTreeService { - static getXmlTreeHtml(xml: string): string { - let xdoc: Document = new DOMParser().parseFromString(xml, 'text/xml'); - let fontColor: string = XmlTreeService._getRecommendedTextColor(); - let html: string = - ` - - - - XML Tree View - - - - - `; - - html += `
    `; - html += XmlTreeService._processXmlNode(xdoc.lastChild); - html += `
`; - - html += - ` - - - `; - - return html; - } - - private static _processXmlNode(node: Node): string { - let html: string = ''; - - if (node.childNodes) { - html += `
    1. `; - } - - if (node.attributes) { - for (let i = 0; i < node.attributes.length; i++) { - html += `
    2. ${node.attributes.item(i).localName} = '${node.attributes.item(i).value}'
    3. `; - } - } - - if (!node.childNodes && node.textContent) { - html += `
    4. ${node.textContent}
    5. `; - } - - if (node.childNodes) { - for (let i = 0; i < node.childNodes.length; i++) { - html += XmlTreeService._processXmlNode(node.childNodes.item(i)); - } - - html += `
  • `; - } - - return html; - } - - private static _getRecommendedTextColor(): string { - let color = '#AAAAAA'; - let path = ''; - - switch (process.platform) { - case 'darwin': - path = `${process.env.HOME}/Library/Application Support/Code/storage.json`; - break; - - case 'win32': - path = `${process.env.APPDATA}\\Code\\storage.json`; - break; - - default: - path = `${process.env.HOME}/.config/Code/storage.json` - break; - } - - try { - fs.accessSync(path); - - let json = fs.readFileSync(path, 'utf8'); - let storage = JSON.parse(json); - - color = (storage.theme.indexOf('vs-dark') > -1) ? '#FFFFFF' : '#000000'; - } - - catch (error) { } - - return color; - } -} \ No newline at end of file