Add Get Current XPath

This commit also moves some shared code to a common class.

Issue: #85
This commit is contained in:
Josh Johnson 2018-05-29 21:38:11 -04:00
parent af2060dc38
commit 6a92fa96f2
10 changed files with 223 additions and 84 deletions

View File

@ -55,6 +55,10 @@
"command": "xmlTools.formatAsXml",
"title": "XML Tools: Format as XML"
},
{
"command": "xmlTools.getCurrentXPath",
"title": "XML Tools: Get Current XPath"
},
{
"command": "xmlTools.minifyXml",
"title": "XML Tools: Minify XML"
@ -195,6 +199,10 @@
"command": "xmlTools.executeXQuery",
"when": "editorLangId == xquery"
},
{
"command": "xmlTools.getCurrentXPath",
"when": "editorLangId == xml"
},
{
"command": "xmlTools.minifyXml",
"when": "editorLangId == xml"

View File

@ -2,3 +2,4 @@ export * from "./configuration";
export * from "./create-document-selector";
export * from "./extension-state";
export * from "./native-commands";
export * from "./xml-traverser";

129
src/common/xml-traverser.ts Normal file
View File

@ -0,0 +1,129 @@
import { Position } from "vscode";
import { DOMParser } from "xmldom";
export class XmlTraverser {
constructor(private _xmlDocument: Document) { }
get xmlDocument(): Document {
return this._xmlDocument;
}
set xmlDocument(value: Document) {
this._xmlDocument = value;
}
getChildAttributeArray(node: Element): any[] {
if (!node.attributes) {
return [];
}
const array = new Array<any>();
for (let i = 0; i < node.attributes.length; i++) {
array.push(node.attributes[i]);
}
return array;
}
getChildElementArray(node: Node): any[] {
if (!node.childNodes) {
return [];
}
const array = new Array<any>();
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (this.isElement(child)) {
array.push(child);
}
}
return array;
}
getElementAtPosition(position: Position): Element {
const node = this.getNodeAtPosition(position);
return this.getNearestElementAncestor(node);
}
getNearestElementAncestor(node: Node): Element {
if (!this.isElement) {
return this.getNearestElementAncestor(node.parentNode);
}
return <Element>node;
}
getNodeAtPosition(position: Position): Node {
return this._getNodeAtPositionCore(position, this._xmlDocument.documentElement);
}
getSiblings(node: Node): Node[] {
return [...this.getChildAttributeArray(<Element>node.parentNode), ...this.getChildElementArray(node.parentNode)];
}
hasSimilarSiblings(node: Node): boolean {
if (!node || !node.parentNode || !this.isElement(node)) {
return false;
}
const siblings = this.getChildElementArray(<Element>node.parentNode);
return (siblings.filter(x => x.tagName === (node as Element).tagName).length > 1);
}
isElement(node: Node): boolean {
return (!!node && !!(node as Element).tagName);
}
private _getNodeAtPositionCore(position: Position, contextNode: Node): Node {
if (!contextNode) {
return undefined;
}
const lineNumber = (contextNode as any).lineNumber;
const columnNumber = (contextNode as any).columnNumber;
const columnRange = [columnNumber, (columnNumber + (this._getNodeWidthInCharacters(contextNode) - 1))];
// for some reason, xmldom sets the column number for attributes to the "="
if (!this.isElement(contextNode)) {
columnRange[0] = (columnRange[0] - contextNode.nodeName.length);
}
if (lineNumber === (position.line + 1) && ((position.character + 1) >= columnRange[0] && (position.character + 1) < columnRange[1])) {
return contextNode;
}
if (this.isElement(contextNode)) {
const children = [...this.getChildAttributeArray(<Element>contextNode), ...this.getChildElementArray(contextNode)];
let result: Node;
for (let i = 0; i < children.length; i++) {
const child = children[i];
result = this._getNodeAtPositionCore(position, child);
if (result) {
return result;
}
}
}
return undefined;
}
private _getNodeWidthInCharacters(node: Node) {
if (this.isElement(node)) {
return (node.nodeName.length + 2);
}
else {
return (node.nodeName.length + node.nodeValue.length + 3);
}
}
}

View File

@ -2,6 +2,7 @@ export namespace commands {
export const evaluateXPath = "xmlTools.evaluateXPath";
export const executeXQuery = "xmlTools.executeXQuery";
export const formatAsXml = "xmlTools.formatAsXml";
export const getCurrentXPath = "xmlTools.getCurrentXPath";
export const minifyXml = "xmlTools.minifyXml";
}

View File

@ -9,7 +9,7 @@ import { XmlFormatterFactory, XmlFormattingEditProvider } from "./formatting";
import { formatAsXml, minifyXml } from "./formatting/commands";
import { XQueryLinter } from "./linting";
import { XmlTreeDataProvider } from "./tree-view";
import { evaluateXPath } from "./xpath/commands";
import { evaluateXPath, getCurrentXPath } from "./xpath/commands";
import { executeXQuery } from "./xquery-execution/commands";
import * as constants from "./constants";
@ -61,7 +61,8 @@ export function activate(context: ExtensionContext) {
/* XPath Features */
context.subscriptions.push(
commands.registerTextEditorCommand(constants.commands.evaluateXPath, evaluateXPath)
commands.registerTextEditorCommand(constants.commands.evaluateXPath, evaluateXPath),
commands.registerTextEditorCommand(constants.commands.getCurrentXPath, getCurrentXPath)
);
/* XQuery Features */

View File

@ -7,12 +7,13 @@ import {
import * as path from "path";
import { DOMParser } from "xmldom";
import { Configuration, NativeCommands } from "../common";
import { Configuration, NativeCommands, XmlTraverser } from "../common";
import * as constants from "../constants";
export class XmlTreeDataProvider implements TreeDataProvider<any> {
private _onDidChangeTreeData: EventEmitter<any> = new EventEmitter<any>();
private _xmlDocument: Document;
private _xmlTraverser: XmlTraverser;
constructor(private _context: ExtensionContext) {
window.onDidChangeActiveTextEditor(() => {
@ -38,13 +39,13 @@ export class XmlTreeDataProvider implements TreeDataProvider<any> {
const treeItem = new TreeItem(element.localName);
if (!this._isElement(element)) {
if (!this._xmlTraverser.isElement(element)) {
treeItem.label = `${element.localName} = "${element.nodeValue}"`;
}
else if (enableMetadata) {
const childAttributes = this._getChildAttributeArray(<Element>element);
const childElements = this._getChildElementArray(<Element>element);
const childAttributes = this._xmlTraverser.getChildAttributeArray(<Element>element);
const childElements = this._xmlTraverser.getChildElementArray(<Element>element);
const totalChildren = (childAttributes.length + childElements.length);
if (totalChildren > 0) {
@ -64,7 +65,7 @@ export class XmlTreeDataProvider implements TreeDataProvider<any> {
treeItem.label += ")";
}
if (this._hasSimilarSiblings(<Element>element) && enableSync) {
if (this._xmlTraverser.hasSimilarSiblings(<Element>element) && enableSync) {
treeItem.label += ` [line ${(element as any).lineNumber}]`;
}
}
@ -88,8 +89,8 @@ export class XmlTreeDataProvider implements TreeDataProvider<any> {
this._refreshTree();
}
if (this._isElement(element)) {
return [].concat(this._getChildAttributeArray(<Element>element), this._getChildElementArray(<Element>element));
if (this._xmlTraverser.isElement(element)) {
return [].concat(this._xmlTraverser.getChildAttributeArray(<Element>element), this._xmlTraverser.getChildElementArray(<Element>element));
}
else if (this._xmlDocument) {
@ -102,78 +103,21 @@ export class XmlTreeDataProvider implements TreeDataProvider<any> {
}
getParent(element: Node): Node {
if (!element || !element.parentNode || !element.parentNode.parentNode) {
if ((!element || !element.parentNode || !element.parentNode.parentNode) && !(element as any).ownerElement) {
return undefined;
}
return element.parentNode;
return element.parentNode || (element as any).ownerElement;
}
getNodeAtPosition(position: Position): Node {
return this._getNodeAtPositionCore(position, this._xmlDocument.documentElement);
}
private _getNodeAtPositionCore(position: Position, contextElement: Element): Node {
if (!contextElement) {
return undefined;
}
if (((contextElement as any).lineNumber - 1) === position.line) {
return contextElement;
}
const children = this._getChildElementArray(<Element>contextElement);
let result: Node;
for (let i = 0; i < children.length; i++) {
const child = children[i];
result = this._getNodeAtPositionCore(position, child);
if (result) {
return result;
}
}
return undefined;
}
private _getChildAttributeArray(node: Element): any[] {
if (!node.attributes) {
return [];
}
const array = new Array<any>();
for (let i = 0; i < node.attributes.length; i++) {
array.push(node.attributes[i]);
}
return array;
}
private _getChildElementArray(node: Element): any[] {
if (!node.childNodes) {
return [];
}
const array = new Array<any>();
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (this._isElement(child)) {
array.push(child);
}
}
return array;
return this._xmlTraverser.getNodeAtPosition(position);
}
private _getIcon(element: Node): any {
let type = "element";
if (!this._isElement(element)) {
if (!this._xmlTraverser.isElement(element)) {
type = "attribute";
}
@ -185,20 +129,6 @@ export class XmlTreeDataProvider implements TreeDataProvider<any> {
return icon;
}
private _hasSimilarSiblings(element: Element): boolean {
if (!element || !element.parentNode) {
return false;
}
const siblings = this._getChildElementArray(<Element>element.parentNode);
return (siblings.filter(x => x.tagName === element.tagName).length > 1);
}
private _isElement(node: Node): boolean {
return (!!node && !!(node as Element).tagName);
}
private _refreshTree(): void {
if (!this.activeEditor || this.activeEditor.document.languageId !== constants.languageIds.xml) {
NativeCommands.setContext(constants.contextKeys.xmlTreeViewEnabled, false);
@ -227,6 +157,11 @@ export class XmlTreeDataProvider implements TreeDataProvider<any> {
this._xmlDocument = new DOMParser().parseFromString("<InvalidDocument />", "text/xml");
}
finally {
this._xmlTraverser = this._xmlTraverser || new XmlTraverser(this._xmlDocument);
this._xmlTraverser.xmlDocument = this._xmlDocument;
}
this._onDidChangeTreeData.fire();
}

View File

@ -0,0 +1,21 @@
import { window } from "vscode";
import { TextEditor, TextEditorEdit } from "vscode";
import { DOMParser } from "xmldom";
import { XPathBuilder } from "../xpath-builder";
export function getCurrentXPath(editor: TextEditor, edit: TextEditorEdit): void {
if (!editor.selection) {
window.showInformationMessage("Please put your cursor in an element or attribute name.");
return;
}
const document = new DOMParser().parseFromString(editor.document.getText());
const xpath = new XPathBuilder(document).build(editor.selection.start);
window.showInputBox({
value: xpath,
valueSelection: undefined
});
}

View File

@ -1 +1,2 @@
export * from "./evaluateXPath";
export * from "./getCurrentXPath";

View File

@ -1 +1,2 @@
export * from "./xpath-builder";
export * from "./xpath-evaluator";

View File

@ -0,0 +1,41 @@
import { Position } from "vscode";
import { DOMParser } from "xmldom";
import { XmlTraverser } from "../common";
export class XPathBuilder {
private _xmlTraverser: XmlTraverser;
constructor(private _xmlDocument: Document) {
this._xmlTraverser = new XmlTraverser(this._xmlDocument);
}
build(position: Position): string {
const selectedNode = this._xmlTraverser.getNodeAtPosition(position);
return this._buildCore(selectedNode);
}
private _buildCore(selectedNode: Node): string {
if (selectedNode === this._xmlDocument.documentElement) {
return `/${selectedNode.nodeName}`;
}
if (!this._xmlTraverser.isElement(selectedNode)) {
return `${this._buildCore((selectedNode as any).ownerElement)}/@${selectedNode.nodeName}`;
}
else if (this._xmlTraverser.hasSimilarSiblings(selectedNode)) {
const siblings = this._xmlTraverser.getSiblings(selectedNode);
const xPathIndex = (siblings.indexOf(selectedNode) + 1);
return `${this._buildCore(selectedNode.parentNode)}/${selectedNode.nodeName}[${xPathIndex}]`;
}
else {
return `${this._buildCore(selectedNode.parentNode)}/${selectedNode.nodeName}`;
}
}
}