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
This commit is contained in:
Josh Johnson 2017-06-15 23:55:04 -04:00
parent 181233529f
commit a9953b696d
11 changed files with 341 additions and 283 deletions

View file

@ -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<string> {
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);
}
}

View file

@ -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<Node> {
private _onDidChangeTreeData: vsc.EventEmitter<Node | null> = new vsc.EventEmitter<Node | null>();
private _xmlDocument: Document;
constructor(private _context: vsc.ExtensionContext) {
vsc.window.onDidChangeActiveTextEditor((editor) => {
this._refreshTree();
});
vsc.workspace.onDidChangeTextDocument((e) => {
this._refreshTree();
});
}
readonly onDidChangeTreeData: vsc.Event<Node | null> = 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<Node>();
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<Node>();
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();
}
}