vNext Rewrite

This commit cleans up and reorganizes the code base, as well as fixes some issues. The intent of the vNext branch is to make the extension more conducive to an open-source environment.
This commit is contained in:
Josh Johnson 2016-01-06 15:55:11 -05:00
parent 1b9caa3265
commit eb43a467c8
16 changed files with 617 additions and 192 deletions

33
src/Commands.ts Normal file
View file

@ -0,0 +1,33 @@
'use strict';
import * as vsc from 'vscode';
import * as ext from './Extension';
import * as xpath from 'xpath';
import { RangeUtil } from './utils/RangeUtil';
import { XmlFormatter } from './services/XmlFormatter';
import { XPathFeatureProvider } from './providers/XPath';
const CFG_SECTION: string = 'xmlTools';
const CFG_REMOVE_COMMENTS: string = 'removeCommentsOnMinify';
export class TextEditorCommands {
static formatXml(editor: vsc.TextEditor, edit: vsc.TextEditorEdit): void {
// alias for editor.action.format
vsc.commands.executeCommand('editor.action.format');
}
static minifyXml(editor: vsc.TextEditor, edit: vsc.TextEditorEdit): void {
let removeComments: boolean = vsc.workspace.getConfiguration(CFG_SECTION).get<boolean>(CFG_REMOVE_COMMENTS, false);
let range: vsc.Range = RangeUtil.getRangeForDocument(editor.document);
let formatter: XmlFormatter = new XmlFormatter();
let xml: string = formatter.minify(editor.document.getText());
edit.replace(range, xml);
}
static evaluateXPath(editor: vsc.TextEditor, edit: vsc.TextEditorEdit): void {
XPathFeatureProvider.evaluateXPathAsync(editor, edit);
}
}

29
src/Extension.ts Normal file
View file

@ -0,0 +1,29 @@
'use strict';
import * as vsc from 'vscode';
import { TextEditorCommands } from './Commands';
import { XmlDocumentFormattingEditProvider, XmlRangeFormattingEditProvider } from './providers/Formatting';
export var GlobalState: vsc.Memento;
export var WorkspaceState: vsc.Memento;
const LANG_XML: string = 'xml';
export function activate(ctx: vsc.ExtensionContext) {
// expose global and workspace state to the entire extension
GlobalState = ctx.globalState;
WorkspaceState = ctx.workspaceState;
// register palette commands
ctx.subscriptions.push(
vsc.commands.registerTextEditorCommand('xmlTools.minifyXml', TextEditorCommands.minifyXml),
vsc.commands.registerTextEditorCommand('xmlTools.formatXml', TextEditorCommands.formatXml),
vsc.commands.registerTextEditorCommand('xmlTools.evaluateXPath', TextEditorCommands.evaluateXPath)
);
// register language feature providers
ctx.subscriptions.push(
vsc.languages.registerDocumentFormattingEditProvider(LANG_XML, new XmlDocumentFormattingEditProvider()),
vsc.languages.registerDocumentRangeFormattingEditProvider(LANG_XML, new XmlRangeFormattingEditProvider())
);
}

View file

@ -1,92 +0,0 @@
'use strict';
import {
Range,
TextEdit,
TextEditor,
TextDocument,
TextEditorEdit,
CancellationToken,
FormattingOptions,
DocumentFormattingEditProvider,
DocumentRangeFormattingEditProvider
} from 'vscode';
import { getRangeForDocument } from '../utils/RangeUtils';
export function linearizeXml(editor: TextEditor, edit: TextEditorEdit): void {
let current = editor.document.getText();
let range = getRangeForDocument(editor.document);
edit.replace(range, _linearizeXml(current));
}
export class XmlDocumentFormattingProvider implements DocumentFormattingEditProvider {
provideDocumentFormattingEdits(document: TextDocument, options: FormattingOptions, token: CancellationToken): TextEdit[] {
let current = document.getText();
let range = getRangeForDocument(document);
return [TextEdit.replace(range, _formatXml(current, options))];
}
}
export class XmlRangeFormattingProvider implements DocumentRangeFormattingEditProvider {
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): TextEdit[] {
let current = document.getText(range);
return [TextEdit.replace(range, _formatXml(current, options))];
}
}
function _strRepeat(source: string, count: number): string {
let output = '';
for (let i = 0; i < count; i++) {
output += source;
}
return output;
}
function _linearizeXml(xml: string): string {
return xml.replace(/>\s{0,}</g, '><');
}
function _formatXml(xml: string, options: FormattingOptions): string {
let tab = _strRepeat(' ', options.tabSize);
let output = '';
let level = 0;
// linearize the xml first for a consistent starting point
xml = _linearizeXml(xml);
// put each tag on its own line
xml = xml.replace(/></g, '>\n<');
// iterate over each line and plug in tabs
let tokens = xml.split('\n');
for (let i = 0; i < tokens.length; i++) {
let line = tokens[i];
// start tags
let startMatch = /<[\w\d]+[^\/]*>/.exec(line);
if (startMatch !== null && startMatch[0] == line) {
output += _strRepeat(tab, level++) + line + '\n';
continue;
}
// close tags
let closeMatch = /<\s*\/\s*[\w\d]+>/.exec(line);
if (closeMatch !== null && closeMatch[0] == line) {
output += _strRepeat(tab, --level) + line + '\n';
}
// one-liners (items that do not affect level)
else {
output += _strRepeat(tab, level) + line + '\n';
}
}
return output;
}

View file

@ -1,50 +0,0 @@
'use strict';
import { window, TextEditor, TextEditorEdit, OutputChannel, ViewColumn, workspace } from 'vscode';
let xpath = require('xpath');
let dom = require('xmldom').DOMParser;
let resultChannel: OutputChannel = null;
export var lastXPath: string;
export function evaluateXPath(editor: TextEditor, edit: TextEditorEdit): void {
let isPersistant = workspace.getConfiguration().has('xmlTools.persistXPathQuery') && workspace.getConfiguration('xmlTools').get<boolean>('persistXPathQuery') === true
window.showInputBox({
placeHolder: 'XPath Query',
prompt: 'Please enter an XPath query to evaluate.',
value: isPersistant ? lastXPath : ''
}).then((query) => {
if (query === undefined) return;
let xml = editor.document.getText();
let doc = new dom().parseFromString(xml);
try {
var nodes = xpath.select(query, doc);
}
catch (ex) {
window.showErrorMessage(ex);
return;
}
lastXPath = query;
if (nodes === null || nodes === undefined || nodes.length == 0) {
window.showInformationMessage('Your XPath query returned no results.');
return;
}
if (resultChannel === null) resultChannel = window.createOutputChannel('XPath Evaluation Results');
resultChannel.clear();
nodes.forEach((node) => {
resultChannel.appendLine(`${node.localName}: ${node.firstChild.data}`);
});
resultChannel.show(ViewColumn.Three);
});
}

View file

@ -1,20 +0,0 @@
'use strict';
import { commands, languages, ExtensionContext } from 'vscode';
import { linearizeXml, XmlDocumentFormattingProvider, XmlRangeFormattingProvider } from './features/xmlFormatting';
import { evaluateXPath } from './features/xmlXPathEngine';
export function activate(ctx: ExtensionContext) {
// register palette commands
ctx.subscriptions.push(commands.registerTextEditorCommand('xmltools.linearizeXml', linearizeXml));
ctx.subscriptions.push(commands.registerTextEditorCommand('xmltools.evaluateXPath', evaluateXPath));
// alias for editor.action.format
ctx.subscriptions.push(commands.registerTextEditorCommand('xmlTools.formatXml', () => {
commands.executeCommand('editor.action.format');
}));
// register formatting providers
ctx.subscriptions.push(languages.registerDocumentFormattingEditProvider('xml', new XmlDocumentFormattingProvider()));
ctx.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider('xml', new XmlRangeFormattingProvider()));
}

View file

@ -0,0 +1,24 @@
'use strict';
import * as vsc from 'vscode';
import { RangeUtil } from '../utils/RangeUtil';
import { XmlFormatter } from '../services/XmlFormatter';
export class XmlDocumentFormattingEditProvider implements vsc.DocumentFormattingEditProvider {
provideDocumentFormattingEdits(document: vsc.TextDocument, options: vsc.FormattingOptions): vsc.TextEdit[] {
let range = RangeUtil.getRangeForDocument(document);
let formatter = new XmlFormatter(options.insertSpaces, options.tabSize);
let xml = formatter.format(document.getText());
return [ vsc.TextEdit.replace(range, xml) ];
}
}
export class XmlRangeFormattingEditProvider implements vsc.DocumentRangeFormattingEditProvider {
provideDocumentRangeFormattingEdits(document: vsc.TextDocument, range: vsc.Range, options: vsc.FormattingOptions): vsc.TextEdit[] {
let formatter = new XmlFormatter(options.insertSpaces, options.tabSize);
let xml = formatter.format(document.getText());
return [ vsc.TextEdit.replace(range, xml) ];
}
}

107
src/providers/XPath.ts Normal file
View file

@ -0,0 +1,107 @@
'use strict';
import * as vsc from 'vscode';
import * as ext from '../Extension';
import { XPathEvaluator } from '../services/XPathEvaluator';
const CFG_SECTION: string = 'xmlTools';
const CFG_PERSIST_QUERY: string = 'persistXPathQuery';
const MEM_QUERY_HISTORY: string = 'xpathQueryHistory';
const MEM_QUERY_LAST: string = 'xPathQueryLast';
const OUTPUT_CHANNEL: string = 'XPath Results';
export class XPathFeatureProvider {
static async evaluateXPathAsync(editor: vsc.TextEditor, edit: vsc.TextEditorEdit): Promise<void> {
// if there is no workspace, we will track queries in the global Memento
let memento: vsc.Memento = ext.WorkspaceState || ext.GlobalState;
// get the xpath persistence setting
let persistQueries: boolean = vsc.workspace.getConfiguration(CFG_SECTION).get<boolean>(CFG_PERSIST_QUERY, true);
// get the last query if there is one for this document
// if not, try pulling the last query ran, regardless of document
// NOTE: if the user has focus on the output channel when opening the xquery prompt, the channel is the "active" document
let history: HistoricQuery[] = memento.get<HistoricQuery[]>(MEM_QUERY_HISTORY, new Array<HistoricQuery>());
let globalLastQuery: string = memento.get<string>(MEM_QUERY_LAST, '');
let lastQuery: HistoricQuery = history.find((item: HistoricQuery) => {
if (item.uri == editor.document.uri.toString()) {
return true;
}
return false;
});
// set the inital display value and prompt the user
let query: string = '';
if (persistQueries) {
if (lastQuery) {
query = lastQuery.query;
}
else {
query = globalLastQuery;
}
}
query = await vsc.window.showInputBox({
placeHolder: 'XPath Query',
prompt: 'Please enter an XPath query to evaluate.',
value: query
});
// showInputBox() will return undefined if the user dimissed the prompt
if (query) {
// run the query
let xml: string = editor.document.getText();
let nodes: Node[] = XPathEvaluator.evaluate(query, xml);
// show the results to the user
let outputChannel: vsc.OutputChannel = vsc.window.createOutputChannel(OUTPUT_CHANNEL);
outputChannel.clear();
outputChannel.appendLine(`XPath Query: ${query}`);
outputChannel.append('');
nodes.forEach((node: Node) => {
outputChannel.appendLine(`${node.localName}: ${node.textContent}`);
});
outputChannel.show(vsc.ViewColumn.Three);
// if persistence is enabled, save the query for later
if (persistQueries) {
lastQuery = new HistoricQuery(editor.document.uri.toString(), query);
let affectedIndex: number = -1;
history = history.map<HistoricQuery>((item: HistoricQuery, index: number) => {
if (item.uri == lastQuery.uri) {
item.query = query;
affectedIndex = index;
}
return item;
});
if (affectedIndex == -1) {
history.push(lastQuery);
}
memento.update(MEM_QUERY_HISTORY, history);
memento.update(MEM_QUERY_LAST, query);
}
}
}
}
class HistoricQuery {
constructor(uri: string, query: string) {
this.uri = uri;
this.query = query;
}
uri: string;
query: string;
}

View file

@ -0,0 +1,23 @@
'use strict';
import * as xpath from 'xpath';
let DOMParser = require('xmldom').DOMParser;
export class XPathEvaluator {
static evaluate(query: string, xml: string): Node[] {
let nodes: Node[] = new Array<Node>();
let xdoc: Document = new DOMParser().parseFromString(xml, 'text/xml');
let resolver: xpath.XPathNSResolver = xpath.createNSResolver(xdoc);
let expression: xpath.XPathExpression = xpath.createExpression(query, resolver);
let result: xpath.XPathResult = expression.evaluate(xdoc, xpath.XPathResult.ORDERED_NODE_ITERATOR_TYPE);
let node: Node;
while (node = result.iterateNext()) {
nodes.push(node);
}
return nodes;
}
}

View file

@ -0,0 +1,122 @@
'use strict';
// Based on pretty-data (https://github.com/vkiryukhin/pretty-data)
export class XmlFormatter {
constructor(preferSpaces?: boolean, tabSize?: number, newLine?: string) {
if (typeof preferSpaces === 'undefined') {
preferSpaces = false;
}
tabSize = tabSize || 4;
newLine = newLine || '\n';
this.newLine = newLine || '\n';
this.indentPattern = (preferSpaces) ? ' '.repeat(tabSize) : '\t';
}
newLine: string;
indentPattern: string;
format(xml: string): string {
xml = this.minify(xml, false);
let parts: string[] = xml
.replace(/</g,"~::~<")
.replace(/xmlns\:/g,"~::~xmlns:")
.replace(/xmlns\=/g,"~::~xmlns=")
.split('~::~');
let inComment: boolean = false;
let level: number = 0;
let output: string = '';
for (let i = 0; i < parts.length; i++) {
// <!
if (parts[i].search(/<!/) > -1) {
output += this._getIndent(level, parts[i]);
inComment = true;
// end <!
if (parts[i].search(/-->/) > -1 || parts[i].search(/\]>/) > -1 || parts[i].search(/!DOCTYPE/) > -1) {
inComment = false;
}
}
// end <!
else if (parts[i].search(/-->/) > -1 || parts[i].search(/\]>/) > -1) {
output += parts[i];
inComment = false;
}
// <elm></elm>
else if (/^<\w/.test(parts[i - 1]) && /^<\/\w/.test(parts[i])
&& /^<[\w:\-\.\,]+/.exec(parts[i - 1])[0] == /^<\/[\w:\-\.\,]+/.exec(parts[i])[0].replace('/', '')) {
output += parts[i];
if (!inComment) level--;
}
// <elm>
else if (parts[i].search(/<\w/) > -1 && parts[i].search(/<\//) == -1 && parts[i].search(/\/>/) == -1) {
output = (!inComment) ? output += this._getIndent(level++, parts[i]) : output += parts[i];
}
// <elm>...</elm>
else if (parts[i].search(/<\w/) > -1 && parts[i].search(/<\//) > -1) {
output = (!inComment) ? output += this._getIndent(level, parts[i]) : output += parts[i];
}
// </elm>
else if (parts[i].search(/<\//) > -1) {
output = (!inComment) ? output += this._getIndent(--level, parts[i]) : output += parts[i];
}
// <elm />
else if (parts[i].search(/\/>/) > -1) {
output = (!inComment) ? output += this._getIndent(level, parts[i]) : output += parts[i];
}
// <?xml ... ?>
else if (parts[i].search(/<\?/) > -1) {
output += this._getIndent(level, parts[i]);
}
// xmlns
else if (parts[i].search(/xmlns\:/) > -1 || parts[i].search(/xmlns\=/) > -1) {
output += this._getIndent(level, parts[i]);
}
else {
output += parts[i];
}
}
// remove leading newline
if (output[0] == this.newLine) {
output = output.slice(1);
}
else if (output.substring(0, 1) == this.newLine) {
output = output.slice(2);
}
return output;
}
minify(xml: string, removeComments?: boolean): string {
if (typeof removeComments === 'undefined') {
removeComments = false;
}
xml = (removeComments) ? xml.replace(/\<![ \r\n\t]*(--([^\-]|[\r\n]|-[^\-])*--[ \r\n\t]*)\>/g, '') : xml;
xml = xml.replace(/>\s{0,}</g, '><');
return xml;
}
private _getIndent(level: number, trailingValue?: string): string {
trailingValue = trailingValue || '';
return `${this.newLine}${this.indentPattern.repeat(level)}${trailingValue}`;
}
}

13
src/utils/RangeUtil.ts Normal file
View file

@ -0,0 +1,13 @@
'use strict';
import * as vsc from 'vscode';
export class RangeUtil {
static getRangeForDocument(document: vsc.TextDocument): vsc.Range {
let lastLineIndex = (document.lineCount - 1);
let range = new vsc.Range(new vsc.Position(0, 0), new vsc.Position(lastLineIndex, Number.MAX_VALUE));
range = document.validateRange(range);
return range;
}
}

View file

@ -1,12 +0,0 @@
'use strict';
import { TextDocument, Range, Position } from 'vscode';
export function getRangeForDocument(document: TextDocument): Range {
let lastLineIndex = (document.lineCount - 1);
let range = new Range(new Position(0, 0), new Position(lastLineIndex, Number.MAX_VALUE));
range = document.validateRange(range);
return range;
}