diff --git a/src/autocompletionprovider.ts b/src/autocompletionprovider.ts index 1592e80..7acb303 100644 --- a/src/autocompletionprovider.ts +++ b/src/autocompletionprovider.ts @@ -1,140 +1,123 @@ -import * as vscode from 'vscode'; -import { languageId } from './extension'; -import { XmlSchemaPropertiesArray } from './types'; import XmlSimpleParser from './helpers/xmlsimpleparser'; +import { Updater } from '../util/Updater'; -export default class AutoCompletionProvider implements vscode.Disposable { +export default class AutoCompletionProvider extends Updater implements monaco.IDisposable { - private documentListener: vscode.Disposable; - private static maxLineChars = 1024; - private static maxLines = 8096; - private delayCount: number = 0; - private documentEvent: vscode.TextDocumentChangeEvent; + constructor(languageId: string) { + super(languageId, 0); + } - constructor(protected extensionContext: vscode.ExtensionContext, protected schemaPropertiesArray: XmlSchemaPropertiesArray) { - this.documentListener = vscode.workspace.onDidChangeTextDocument(async (evnt) => - this.triggerDelayedAutoCompletion(evnt), this, this.extensionContext.subscriptions); - } + private static maxLineChars = 1024; + private static maxLines = 8096; - public dispose() { - this.documentListener.dispose(); - } + async doUpdate(resource: monaco.Uri, languageId: string, documentEvent: monaco.editor.IModelContentChangedEvent): Promise { + if (!documentEvent) + return null; + const document = monaco.editor.getModel(resource) as IMdlnModel; + if (!document || !document.editor) + return null; - private async triggerDelayedAutoCompletion(documentEvent: vscode.TextDocumentChangeEvent, timeout: number = 250): Promise { - - if (this.delayCount > 0) { - this.delayCount = timeout; - this.documentEvent = documentEvent; - return; - } - this.delayCount = timeout; - this.documentEvent = documentEvent; - - const tick = 100; - - while (this.delayCount > 0) { - await new Promise(resolve => setTimeout(resolve, tick)); - this.delayCount -= tick; - } - - this.triggerAutoCompletion(this.documentEvent); - } - - private async triggerAutoCompletion(documentEvent: vscode.TextDocumentChangeEvent): Promise { - const activeTextEditor = vscode.window.activeTextEditor; - const document = documentEvent.document; - const inputChange = documentEvent.contentChanges[0]; - if (document.languageId !== languageId - || documentEvent.contentChanges.length !== 1 - || !inputChange.range.isSingleLine - || (inputChange.text && inputChange.text.indexOf("\n") >= 0) - || activeTextEditor === undefined - || document.lineCount > AutoCompletionProvider.maxLines - || activeTextEditor.document.uri.toString() !== document.uri.toString()) { - return; - } - - const changeLine = inputChange.range.end.line; - const wholeLineRange = document.lineAt(changeLine).range; - const wholeLineText = document.getText(document.lineAt(inputChange.range.end.line).range); - - let linePosition = inputChange.range.start.character + inputChange.text.length; - - if (wholeLineText.length >= AutoCompletionProvider.maxLineChars) { - return; - } - - const scope = await XmlSimpleParser.getScopeForPosition(`${wholeLineText}\n`, linePosition); - - if (--linePosition < 0) { - // NOTE: automatic acions require info about previous char - return; - } - - const before = wholeLineText.substring(0, linePosition); - const after = wholeLineText.substring(linePosition); - - if (!(scope.context && scope.context !== "text" && scope.tagName)) { - // NOTE: unknown scope - return; - } - - if (before.substr(before.lastIndexOf("<"), 2) === ""); - const nextTagStartPostion = after.indexOf("<"); - const nextTagEndingPostion = nextTagStartPostion >= 0 ? after.indexOf(">", nextTagStartPostion) : -1; - const invalidTagStartPostion = nextTagEndingPostion >= 0 ? after.indexOf("<", nextTagEndingPostion) : -1; - - let resultText: string = ""; - - if (after.substr(closeCurrentTagIndex - 1).startsWith(`/>`) && closeCurrentTagIndex === 1) { - - resultText = wholeLineText.substring(0, linePosition + nextTagStartPostion) + `` + wholeLineText.substring(linePosition + nextTagEndingPostion + 1); - - } else if (after.substr(closeCurrentTagIndex - 1, 2) !== "/>" && invalidTagStartPostion < 0) { - - if (nextTagStartPostion >= 0 && after[nextTagStartPostion + 1] === "/") { - - resultText = wholeLineText.substring(0, linePosition + nextTagStartPostion) + `` + wholeLineText.substring(linePosition + nextTagEndingPostion + 1); - } - else if (nextTagStartPostion < 0) { - resultText = wholeLineText.substring(0, linePosition + closeCurrentTagIndex + 1) + `` + wholeLineText.substring(linePosition + closeCurrentTagIndex + 1); - } - } - - if (!resultText || resultText.trim() === wholeLineText.trim()) { - return; - } - - resultText = resultText.trimRight(); - - if (!await XmlSimpleParser.checkXml(`${resultText}`)) { - // NOTE: Single line must be ok, one element in line - return; - } - - let documentContent = document.getText(); - - documentContent = documentContent.split("\n") - .map((l, i) => (i === changeLine) ? resultText : l) - .join("\n"); - - if (!await XmlSimpleParser.checkXml(documentContent)) { - // NOTE: Check whole document - return; - } - - await activeTextEditor.edit((builder) => { - builder.replace( - new vscode.Range( - wholeLineRange.start, - wholeLineRange.end), - resultText); - }, { undoStopAfter: false, undoStopBefore: false }); - } + const inputChange = documentEvent.changes[0]; + + if (document.getModeId() !== languageId + || documentEvent.changes.length !== 1 + || inputChange.range.startLineNumber - inputChange.range.endLineNumber !== 0 + || (inputChange.text && inputChange.text.indexOf("\n") >= 0) + || document.getLineCount() > AutoCompletionProvider.maxLines) { + return null; + } + + const changeLine = inputChange.range.endLineNumber; + const wholeLineRange = new monaco.Range(changeLine, 1, changeLine, document.getLineLength(changeLine) + 1); + const wholeLineText = document.getLineContent(inputChange.range.startLineNumber); + + let linePosition = (inputChange.range.startColumn - 1) + (inputChange.text.length - 1); + + if (wholeLineText.length >= AutoCompletionProvider.maxLineChars) { + return null; + } + + const scope = await XmlSimpleParser.getScopeForPosition(`${wholeLineText}\n`, linePosition); + + if (--linePosition < 0) { + // NOTE: automatic acions require info about previous char + return null; + } + + const before = wholeLineText.substring(0, linePosition); + const after = wholeLineText.substring(linePosition); + + if (!(scope.context && scope.context !== "text" && scope.tagName)) { + // NOTE: unknown scope + return null; + } + + if (before.substr(before.lastIndexOf("<"), 2) === "") <= 0) { + return null; + } + + // NOTE: auto-change is available only for single tag enclosed in one line + const closeCurrentTagIndex = after.indexOf(">"); + const nextTagStartPosition = after.indexOf("<"); + const nextTagEndingPosition = nextTagStartPosition >= 0 ? after.indexOf(">", nextTagStartPosition) : -1; + const invalidTagStartPosition = nextTagEndingPosition >= 0 ? after.indexOf("<", nextTagEndingPosition) : -1; + + let resultText: string = ""; + + if (after.substr(closeCurrentTagIndex - 1).startsWith(`/>`) && closeCurrentTagIndex === 1) { + + resultText = wholeLineText.substring(0, linePosition + nextTagStartPosition) + `` + wholeLineText.substring(linePosition + nextTagEndingPosition + 1); + + } else if (after.substr(closeCurrentTagIndex - 1, 2) !== "/>" && invalidTagStartPosition < 0) { + + if (nextTagStartPosition >= 0 && after[nextTagStartPosition + 1] === "/") { + + resultText = wholeLineText.substring(0, linePosition + nextTagStartPosition) + `` + wholeLineText.substring(linePosition + nextTagEndingPosition + 1); + } + else if (nextTagStartPosition < 0) { + resultText = wholeLineText.substring(0, linePosition + closeCurrentTagIndex + 1) + `` + wholeLineText.substring(linePosition + closeCurrentTagIndex + 1); + } + } + + if (!resultText || resultText.trim() === wholeLineText.trim()) { + return null; + } + + resultText = resultText.trimEnd(); + + if (!await XmlSimpleParser.checkXml(`${resultText}`)) { + // NOTE: Single line must be ok, one element in line + //console.log("bad xml 1: " + resultText); + return null; + } + + let documentContent = document.getValue(); + + documentContent = documentContent.split("\n") + .map((l, i) => (i === changeLine - 1) ? resultText : l) + .join("\n"); + + if (!await XmlSimpleParser.checkXml(documentContent)) { + // NOTE: Check whole document + //console.log("bad xml 2"); + return null; + } + + document.pushEditOperations([], [{ + forceMoveMarkers: false, + range: wholeLineRange, + text: resultText + } as monaco.editor.IIdentifiedSingleEditOperation], null); + + if (document.editor) { + document.editor.setPosition({ + lineNumber: inputChange.range.startLineNumber, + column: inputChange.range.startColumn + 1 + }); + } + } } \ No newline at end of file diff --git a/src/completionitemprovider.ts b/src/completionitemprovider.ts index 480f7d0..3e28c5c 100644 --- a/src/completionitemprovider.ts +++ b/src/completionitemprovider.ts @@ -1,60 +1,172 @@ -import * as vscode from 'vscode'; -import { XmlSchemaPropertiesArray, CompletionString } from './types'; -import { globalSettings } from './extension'; +import { CompletionString, XsdType, CsType } from './types'; +import { CompletionAdapterBase } from '../base/CompletionAdapterBase'; import XmlSimpleParser from './helpers/xmlsimpleparser'; +import XsdSettings, { ISnippet } from './helpers/settings'; -export default class XmlCompletionItemProvider implements vscode.CompletionItemProvider { +export class XmlCompletionItemProvider extends CompletionAdapterBase implements monaco.languages.CompletionItemProvider { - constructor(protected extensionContext: vscode.ExtensionContext, protected schemaPropertiesArray: XmlSchemaPropertiesArray) { + public get triggerCharacters(): string[] { + return ['<', ' ', '=']; } - async provideCompletionItems(textDocument: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, _context: vscode.CompletionContext): Promise { - let documentContent = textDocument.getText(); - let offset = textDocument.offsetAt(position); - let xsdFileUris = (await XmlSimpleParser.getSchemaXsdUris(documentContent, globalSettings.schemaMapping)) - .map(u => vscode.Uri.parse(u)); + async provideCompletionItems(textDocument: monaco.editor.ITextModel, position: monaco.Position, context: monaco.languages.CompletionContext, token: monaco.CancellationToken) + : Promise { - let nsMap = await XmlSimpleParser.getNamespaceMapping(documentContent); + let documentContent = textDocument.getValue(); - let scope = await XmlSimpleParser.getScopeForPosition(documentContent, offset); + let xsdFileUris = (await XmlSimpleParser.getSchemaXsdUris(documentContent, + XsdSettings.schemaMapping.filter(sm => !sm.noComplete))).map(u => monaco.Uri.parse(u)); + await XsdSettings.prepare(xsdFileUris); + + let localNsMap = await XmlSimpleParser.getNamespaceMapping(documentContent); + let scope = await XmlSimpleParser.getScopeForPosition(documentContent, textDocument.getOffsetAt(position)); let resultTexts: CompletionString[]; + let addTagSnippet = false, addAttribSnippet = false; + + const schemaProperties = XsdSettings.schemaPropertiesArray.filterUris(xsdFileUris); + const parentTagName = scope.parentTagName ? scope.parentTagName.substring(scope.parentTagName.indexOf(":") + 1) : null; + + let addSnippets: ISnippet[] = []; + + function addRootSnippet() { + XsdSettings.schemaMapping.forEach(sm => { + if (sm.rootSnippets) { + addSnippets = addSnippets.concat(sm.rootSnippets); + } + }); + } if (token.isCancellationRequested) { resultTexts = []; - - } else if (scope.context === "text") { + } + else if (scope.context === "text") { resultTexts = []; - - } else if (scope.tagName === undefined) { + addTagSnippet = true; + } + else if (scope.tagName === undefined) { resultTexts = []; + addRootSnippet(); + addTagSnippet = true; + } + else if (scope.context === "element" /*&& scope.tagName.indexOf(".") < 0*/) { + const parentTag = schemaProperties + .map(sp => sp.tagCollection + .filter(e => e.visible && e.tag.name === parentTagName)) + .reduce((prev, next) => prev.concat(next), [])[0]; - } else if (scope.context === "element" && scope.tagName.indexOf(".") < 0) { - resultTexts = this.schemaPropertiesArray - .filterUris(xsdFileUris) - .map(sp => sp.tagCollection.filter(e => e.visible).map(e => sp.tagCollection.fixNs(e.tag, nsMap))) + resultTexts = schemaProperties + .map(sp => sp.tagCollection.loadTagEx(parentTag ? parentTag.tag.name : null, localNsMap) + .map(tag => sp.tagCollection.fixNs(tag, localNsMap, sp))) .reduce((prev, next) => prev.concat(next), []) .sort() - .filter((v, i, a) => a.findIndex(e => e.name === v.name && e.comment === v.comment ) === i); + .filter((v, i, a) => a.findIndex(e => e.name === v.name && e.comment === v.comment) === i); + + if (parentTag) { + let arr = scope.parentTagName.split(":"); + const schema = XsdSettings.schemaMapping.filter(sm => sm.xmlns === (arr.length === 2 ? localNsMap.nsToUri.get(arr[0]) : localNsMap.nsToUri.get("")))[0]; + if (schema && schema.snippets) { + addSnippets = schema.snippets.filter(s => !s.parentTags || s.parentTags.indexOf(parentTagName) > -1); + } + } + else if (!scope.parentTagName) { + addRootSnippet(); + } + } + else if (scope.context === "attribute" && scope.tagName) { + let p: number; + const parentTagNs = scope.tagName && (p = scope.tagName.indexOf(":")) > -1 ? scope.tagName.substring(0, p) : null; - } else if (scope.context !== undefined) { - resultTexts = this.schemaPropertiesArray - .filterUris(xsdFileUris) - .map(sp => sp.tagCollection.loadAttributesEx(scope.tagName ? scope.tagName.replace(".", "") : undefined, nsMap).map(s => sp.tagCollection.fixNs(s, nsMap))) + resultTexts = schemaProperties + .map(sp => sp.tagCollection.loadAttributesEx(scope.tagName /*? scope.tagName.replace(".", "") : undefined*/, localNsMap, sp.namespace) + .map(attr => sp.tagCollection.fixNs(attr, localNsMap, sp, parentTagNs))) .reduce((prev, next) => prev.concat(next), []) .sort() - .filter((v, i, a) => a.findIndex(e => e.name === v.name && e.comment === v.comment ) === i); + .filter((v, i, a) => a.findIndex(e => e.name === v.name && e.comment === v.comment) === i); - } else { + const isAttribValue = textDocument.getValueInRange(new monaco.Range(position.lineNumber, position.column - 1, position.lineNumber, position.column)) === "="; + if (isAttribValue) { + addAttribSnippet = true; + const attribName = textDocument.getWordAtPosition(new monaco.Position(position.lineNumber, position.column - 3)); + if (attribName && attribName.word) { + const attrib = resultTexts.filter(r => r.name === attribName.word)[0]; + if (attrib && attrib.values) { + addAttribSnippet = false; + resultTexts = attrib.values; + } + } + if (addAttribSnippet) { + resultTexts = []; + } + } + } + else { resultTexts = []; + addTagSnippet = true; } - return resultTexts - .map(t => { - let ci = new vscode.CompletionItem(t.name, vscode.CompletionItemKind.Snippet); - ci.detail = scope.context; - ci.documentation = t.comment; - return ci; - }); + let complete: monaco.languages.CompletionList = { + suggestions: resultTexts + .map(t => { + const ci: monaco.languages.CompletionItem = { + kind: t.type === CsType.Element ? monaco.languages.CompletionItemKind.Class : monaco.languages.CompletionItemKind.Property, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + label: t.name, + detail: t.comment, + documentation: t.documentation, + insertText: t.type === CsType.Element ? `${t.name}>$0` : + t.type === CsType.AttributeValue ? `"${t.name}" $0` : t.name, + range: null + }; + return ci; + }), + incomplete: false + } + + if (addTagSnippet) { + complete.suggestions.push( + { + kind: monaco.languages.CompletionItemKind.Constructor, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + label: "quick tag", + insertText: "<${1:name}>$0", + detail: "Inserts a full tag - press TAB to type content", + sortText: " ", range: null + }); + } + else if (addAttribSnippet) { + complete.suggestions.push( + { + kind: monaco.languages.CompletionItemKind.Constructor, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + label: "attribute", + insertText: `"$0"`, + detail: "An empty attribute", + sortText: " ", range: null + }); + } + + if (addSnippets.length) { + addSnippets.forEach(s => complete.suggestions.push( + { + kind: monaco.languages.CompletionItemKind.Snippet, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + label: s.label, + insertText: s.insertText, + detail: s.detail, + documentation: s.documentation, + sortText: s.label, range: null + })); + } + + if (this.snippets) { + complete.suggestions = complete.suggestions.concat(this.snippets); + } + + this.consolidateItems(complete.suggestions, textDocument, position); + return complete; } + + registerSnippets() { } + } \ No newline at end of file diff --git a/src/helpers/settings.ts b/src/helpers/settings.ts new file mode 100644 index 0000000..a75e598 --- /dev/null +++ b/src/helpers/settings.ts @@ -0,0 +1,72 @@ +import { XmlSchemaPropertiesArray, XmlSchemaProperties } from "../types"; +import XsdParser from "./xsdparser"; +import { Server } from "../../util/Server"; + +export interface ISnippet { + parentTags: string[], + label: string; + insertText: string; + detail?: string; + documentation?: string; +} + +export interface IXsdSetting { + xmlns: string; + xsdUri: string; + strict: boolean; + noComplete?: boolean; + snippets?: ISnippet[]; + rootSnippets?: ISnippet[]; +} + +export default class XsdSettings { + + static schemaPropertiesArray: XmlSchemaPropertiesArray; + static schemaMapping: IXsdSetting[]; + + public static init() { + if (XsdSettings.schemaPropertiesArray) + return; + require(["sax"], () => { + //console.log("sax loaded") + }); + + XsdSettings.schemaPropertiesArray = new XmlSchemaPropertiesArray(); + /* + // SAMPLE: Set `schemaMapping` via load settings + XsdSettings.schemaMapping = [ + { xmlns: "http://myschemas.net/myscheme", xsdUri: "http://myschemas.net/myscheme.xsd", strict: true, + rootSnippets: [ ... like snippets but for all xml docs in in the root - to insert root element ] + snippets: [{ + label: "mysnip", + parentTags: ["must-be-child-of-this"], + insertText: "my:tag attr=\"${1:\\the value}\">\n\t$0\n", + documentation: "Sample snippet" + }]}]; + */ + } + + public static async prepare(xsdFileUris: monaco.Uri[]) { + // first load all + for (let xsdUri of xsdFileUris) { + let schemaProperties = XsdSettings.schemaPropertiesArray.filterUris([xsdUri])[0]; + if (!schemaProperties) { + schemaProperties = { schemaUri: xsdUri, xsdContent: null, tagCollection: null } as XmlSchemaProperties; + schemaProperties.namespace = XsdSettings.schemaMapping.filter(sm => sm.xsdUri === xsdUri.toString())[0].xmlns; + + //schemaProperties.xsdContent = await XsdLoader.loadSchemaContentsFromUri(xsdUri.toString(true)); + schemaProperties.xsdContent = await Server.loadContentFromUri(xsdUri.toString(true)); + + XsdSettings.schemaPropertiesArray.push(schemaProperties); + } + } + // ... then parse them + for (var i = 0; i < XsdSettings.schemaPropertiesArray.length; i++) { + if (!XsdSettings.schemaPropertiesArray[i].tagCollection) { + await XsdParser.getSchemaTagsAndAttributes(XsdSettings.schemaPropertiesArray[i]); + } + } + } + + +} diff --git a/src/helpers/xmlsimpleparser.ts b/src/helpers/xmlsimpleparser.ts index 7278998..f159422 100644 --- a/src/helpers/xmlsimpleparser.ts +++ b/src/helpers/xmlsimpleparser.ts @@ -1,194 +1,257 @@ -import { XmlTagCollection, XmlDiagnosticData, XmlScope } from '../types'; +import { XmlDiagnosticData, XmlScope, XmlSchemaProperties, XmlTag } from '../types'; +import XsdSettings from './settings'; +import { IAttributes } from './xsdparser'; + +export interface INsMap { + uriToNs: Map, + nsToUri: Map +} export default class XmlSimpleParser { - public static getXmlDiagnosticData(xmlContent: string, xsdTags: XmlTagCollection, nsMap: Map, strict: boolean = true): Promise { - const sax = require("sax"); + public static getXmlDiagnosticData(xmlContent: string, schemaProperties: XmlSchemaProperties[], nsMap: INsMap): XmlDiagnosticData[] { + const sax = /*require("sax");*/ (window as any).sax; const parser = sax.parser(true); - return new Promise( - (resolve) => { - let result: XmlDiagnosticData[] = []; - - parser.onerror = () => { - if (undefined === result.find(e => e.line === parser.line)) { - result.push({ - line: parser.line, - column: parser.column, - message: parser.error.message, - severity: strict ? "error" : "warning" - }); - } - parser.resume(); - }; - - parser.onopentag = (tagData: { name: string, isSelfClosing: boolean, attributes: Map }) => { - - let nodeNameSplitted: Array = tagData.name.split('.'); - - if (xsdTags.loadTagEx(nodeNameSplitted[0], nsMap) !== undefined) { - let schemaTagAttributes = xsdTags.loadAttributesEx(nodeNameSplitted[0], nsMap); - nodeNameSplitted.shift(); - Object.keys(tagData.attributes).concat(nodeNameSplitted).forEach((a: string) => { - if (schemaTagAttributes.findIndex(sta => sta.name === a) < 0 && a.indexOf(":!") < 0 && a !== "xmlns") { - result.push({ - line: parser.line, - column: parser.column, - message: `Unknown xml attribute '${a}' for tag '${tagData.name}'`, severity: strict ? "info" : "hint" - }); - } - }); - } - else if (tagData.name.indexOf(":!") < 0) { - result.push({ - line: parser.line, - column: parser.column, - message: `Unknown xml tag '${tagData.name}'`, - severity: strict ? "info" : "hint" - }); - } - }; + let result: XmlDiagnosticData[] = []; + const tagStack: string[] = []; + let stackIndex = -1, startLine = 0, startColumn = 0; + const attribsRange: Map = new Map(); + + parser.onerror = () => { + if (!result.find(e => e.range.startLineNumber === parser.line)) { + result.push({ + range: new monaco.Range(parser.line, parser.column, parser.line, parser.column), + message: parser.error.message, + severity: "error" + } as XmlDiagnosticData); + } + parser.resume(); + }; + + parser.onattribute = (tag: { name: string, value: string }) => { + attribsRange.set(tag.name, { + startLineNumber: parser.line, + startColumn: parser.column - tag.name.length - tag.value.length - 2, + endLineNumber: parser.line, + endColumn: parser.column + 1 + }); + }; + + parser.onopentagstart = (tag: { name: string }) => { + tagStack[++stackIndex] = tag.name; + attribsRange.clear(); + startLine = parser.line; + startColumn = parser.column; + }; + + parser.onclosetag = () => { + stackIndex--; + }; + + if (schemaProperties) { + parser.onopentag = (tagData: { name: string, isSelfClosing: boolean, attributes: IAttributes }) => { + let parentTagName = stackIndex == 0 ? null : tagStack[stackIndex - 1]; + parentTagName = parentTagName ? parentTagName.substring(parentTagName.indexOf(":") + 1) : null; + + const parentTag = schemaProperties + .map(sp => ({ + tag: sp.tagCollection.filter(e => e.visible && e.tag.name === parentTagName)[0], + foundInSp: sp + })) + .filter(t => t.tag) + .reduce((prev, next) => prev.concat(next), [])[0]; + + const strict = parentTag && XsdSettings.schemaMapping.find(m => m.xsdUri === parentTag.foundInSp.schemaUri.toString() && m.strict === true) ? true : false; + + const allowedTags = schemaProperties + .map(sp => sp.tagCollection.loadTagEx(parentTag ? parentTag.tag.name : null, nsMap) + .map(tag => sp.tagCollection.fixNs(tag, nsMap, sp))) + .reduce((prev, next) => prev.concat(next), []) + .filter(t => t.name == tagData.name); + + if (allowedTags.length) { + const findTagName = tagData.name.substring(tagData.name.indexOf(":") + 1); + + let ft: XmlTag, foundInSp: XmlSchemaProperties; + const xsdTags = schemaProperties.filter(sp => { + let _ft = sp.tagCollection.filter(t => t.tag.name === findTagName)[0] + if (_ft) { + ft = _ft; + foundInSp = sp; + return true; + } + return false; + }); - parser.onend = () => { - resolve(result); - }; + let schemaTagAttributes = xsdTags[0].tagCollection.loadAttributesEx(ft.tag.name, nsMap, null); + + Object.keys(tagData.attributes).forEach((a: string) => { + const attrib = schemaTagAttributes.find(sta => sta.name === a); + if (!attrib && a.indexOf(":!") < 0 + && !a.startsWith("xmlns") && !a.startsWith("data-")) { + const pos = attribsRange.get(a); + result.push({ + range: pos, + message: `Unknown xml attribute '${a}' for tag '${tagData.name}'`, + severity: strict ? "info" : "hint" + }); + } + else if (attrib && attrib.values && attrib.values.findIndex(a => a.name === tagData.attributes[attrib.name]) < 0) { + const pos = attribsRange.get(a); + result.push({ + range: new monaco.Range(pos.startLineNumber, pos.startColumn + a.length + 1, pos.endLineNumber, pos.endColumn), + message: `Unknown xml attribute value '${a}' for attribute '${attrib.name}' in tag '${tagData.name}'`, + severity: strict ? "info" : "hint" + }); + } + }); + } + else if (tagData.name.indexOf(":!") < 0) { + result.push({ + range: new monaco.Range(startLine, startColumn - tagData.name.length - 1, parser.line, parser.column + 1), + message: `Unknown xml tag '${tagData.name}'`, + severity: strict ? "info" : "hint" + }); + } + }; + } - parser.write(xmlContent).close(); - }); + parser.write(xmlContent).close(); + return result; } - public static getSchemaXsdUris(xmlContent: string, schemaMapping: { xmlns: string, xsdUri: string }[]): Promise { - const sax = require("sax"); + public static getSchemaXsdUris(xmlContent: string, schemaMapping: { xmlns: string, xsdUri: string }[]): string[] { + const sax = /*require("sax");*/ (window as any).sax; const parser = sax.parser(true); - return new Promise( - (resolve) => { - let result: string[] = []; - - parser.onerror = () => { - parser.resume(); - }; - - parser.onattribute = (attr: any) => { - if (attr.name.endsWith(":schemaLocation")) { - result.push(...attr.value.split(/\s+/)); - } else if (attr.name === "xmlns") { - let newUriStrings = schemaMapping - .filter(m => m.xmlns === attr.value) - .map(m => m.xsdUri.split(/\s+/)) - .reduce((prev, next) => prev.concat(next), []); - result.push(...newUriStrings); - } else if (attr.name.startsWith("xmlns:")) { - let newUriStrings = schemaMapping - .filter(m => m.xmlns === attr.value) - .map(m => m.xsdUri.split(/\s+/)) - .reduce((prev, next) => prev.concat(next), []); - result.push(...newUriStrings); - } - }; - - parser.onend = () => { - resolve(result.filter((v, i, a) => a.indexOf(v) === i)); - }; - - parser.write(xmlContent).close(); - }); + const result: string[] = []; + + parser.onerror = () => { + parser.resume(); + }; + + parser.onattribute = (attr: any) => { + if (attr.name.endsWith(":schemaLocation")) { + result.push(...attr.value.split(/\s+/)); + } else if (attr.name === "xmlns") { + let newUriStrings = schemaMapping + .filter(m => m.xmlns === attr.value) + .map(m => m.xsdUri.split(/\s+/)) + .reduce((prev, next) => prev.concat(next), []); + result.push(...newUriStrings); + } else if (attr.name.startsWith("xmlns:")) { + let newUriStrings = schemaMapping + .filter(m => m.xmlns === attr.value) + .map(m => m.xsdUri.split(/\s+/)) + .reduce((prev, next) => prev.concat(next), []); + result.push(...newUriStrings); + } + }; + + parser.write(xmlContent).close(); + + return result.filter((v, i, a) => a.indexOf(v) === i); } - public static getNamespaceMapping(xmlContent: string): Promise> { - const sax = require("sax"); + public static getNamespaceMapping(xmlContent: string): INsMap { + const sax = /*require("sax");*/ (window as any).sax; const parser = sax.parser(true); - return new Promise>( - (resolve) => { - let result: Map = new Map(); - - parser.onerror = () => { - parser.resume(); - }; - - parser.onattribute = (attr: any) => { - if (attr.name.startsWith("xmlns:")) { - result.set(attr.value, attr.name.substring("xmlns:".length)); - } - }; - - parser.onend = () => { - resolve(result); - }; - - parser.write(xmlContent).close(); - }); + const uriToNs = new Map(); + const nsToUri = new Map(); + + parser.onerror = () => { + parser.resume(); + }; + + parser.onattribute = (attr: any) => { + if (attr.name === "xmlns") { + uriToNs.set(attr.value, ""); + nsToUri.set("", attr.value); + } + else if (attr.name.startsWith("xmlns:")) { + const ns = attr.name.substring("xmlns:".length); + uriToNs.set(attr.value, ns); + nsToUri.set(ns, attr.value); + } + }; + + parser.write(xmlContent).close(); + return { uriToNs: uriToNs, nsToUri: nsToUri }; } - public static getScopeForPosition(xmlContent: string, offset: number): Promise { - const sax = require("sax"); + public static getScopeForPosition(xmlContent: string, offset: number): XmlScope { + const sax = /*require("sax");*/ (window as any).sax; const parser = sax.parser(true); - return new Promise( - (resolve) => { - let result: XmlScope; - let previousStartTagPosition = 0; - let updatePosition = () => { - - if ((parser.position >= offset) && !result) { - - let content = xmlContent.substring(previousStartTagPosition, offset); - content = content.lastIndexOf("<") >= 0 ? content.substring(content.lastIndexOf("<")) : content; - - let normalizedContent = content.concat(" ").replace("/", "").replace("\t", " ").replace("\n", " ").replace("\r", " "); - let tagName = content.substring(1, normalizedContent.indexOf(" ")); - - result = { tagName: /^[a-zA-Z0-9_:\.\-]*$/.test(tagName) ? tagName : undefined, context: undefined }; - - if (content.lastIndexOf(">") >= content.lastIndexOf("<")) { - result.context = "text"; - } else { - let lastTagText = content.substring(content.lastIndexOf("<")); - if (!/\s/.test(lastTagText)) { - result.context = "element"; - } else if ((lastTagText.split(`"`).length % 2) !== 0) { - result.context = "attribute"; - } - } - } - - previousStartTagPosition = parser.startTagPosition - 1; - }; - - parser.onerror = () => { - parser.resume(); - }; + const tagStack: string[] = []; + let stackIndex = -1; - parser.ontext = (_t: any) => { - updatePosition(); - }; + let result: XmlScope; + let previousStartTagPosition = 0; + let updatePosition = () => { + if ((parser.position >= offset) && !result) { + let content = xmlContent.substring(previousStartTagPosition, offset); + content = content.lastIndexOf("<") >= 0 ? content.substring(content.lastIndexOf("<")) : content; - parser.onopentagstart = () => { - updatePosition(); - }; + let normalizedContent = content.concat(" ").replace("/", "").replace("\t", " ").replace("\n", " ").replace("\r", " "); + let tagName = content.substring(1, normalizedContent.indexOf(" ")); - parser.onattribute = () => { - updatePosition(); + result = { + tagName: /^[\ { - updatePosition(); - }; - - parser.onend = () => { - if (result === undefined) { - result = { tagName: undefined, context: undefined }; + if (content.lastIndexOf(">") >= content.lastIndexOf("<")) { + result.context = "text"; + } else { + let lastTagText = content.substring(content.lastIndexOf("<")); + if (!/\s/.test(lastTagText)) { + result.context = "element"; + } else if ((lastTagText.split(`"`).length % 2) !== 0) { + result.context = "attribute"; } - resolve(result); - }; - - parser.write(xmlContent).close(); - }); + } + } + previousStartTagPosition = parser.startTagPosition - 1; + }; + + parser.onerror = () => { + parser.resume(); + }; + + parser.ontext = () => { + updatePosition(); + }; + + parser.onopentagstart = (tag: { name: string }) => { + tagStack[++stackIndex] = tag.name; + updatePosition(); + }; + + parser.onattribute = () => { + updatePosition(); + }; + + parser.onclosetag = () => { + stackIndex--; + updatePosition(); + }; + + parser.onend = () => { + if (result === undefined) { + result = { tagName: undefined, parentTagName: undefined, context: undefined }; + } + + }; + + parser.write(xmlContent).close(); + return result; } public static checkXml(xmlContent: string): Promise { - const sax = require("sax"); + const sax = /*require("sax");*/ (window as any).sax; const parser = sax.parser(true); let result: boolean = true; @@ -208,7 +271,7 @@ export default class XmlSimpleParser { } public static formatXml(xmlContent: string, indentationString: string, eol: string, formattingStyle: "singleLineAttributes" | "multiLineAttributes" | "fileSizeOptimized"): Promise { - const sax = require("sax"); + const sax = /*require("sax");*/ (window as any).sax; const parser = sax.parser(true); let result: string[] = []; @@ -222,13 +285,6 @@ export default class XmlSimpleParser { ? eol + Array(xmlDepthPath.length).fill(indentationString).join("") : ""; - let getEncodedText = (t: string) : string => - t.replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - return new Promise( (resolve) => { @@ -236,11 +292,11 @@ export default class XmlSimpleParser { parser.resume(); }; - parser.ontext = (t) => { - result.push(/^\s*$/.test(t) ? `` : getEncodedText(`${t}`)); + parser.ontext = (t: string) => { + result.push(/^\s*$/.test(t) ? `` : `${t}`); }; - parser.ondoctype = (t) => { + parser.ondoctype = (t: string) => { result.push(`${eol}`); }; @@ -248,11 +304,11 @@ export default class XmlSimpleParser { result.push(`${eol}`); }; - parser.onsgmldeclaration = (t) => { + parser.onsgmldeclaration = (t: string) => { result.push(`${eol}`); }; - parser.onopentag = (tagData: { name: string, isSelfClosing: boolean, attributes: Map }) => { + parser.onopentag = (tagData: { name: string, isSelfClosing: boolean, attributes: IAttributes }) => { let argString: string[] = [""]; for (let arg in tagData.attributes) { argString.push(` ${arg}="${tagData.attributes[arg]}"`); @@ -272,7 +328,7 @@ export default class XmlSimpleParser { }); }; - parser.onclosetag = (t) => { + parser.onclosetag = (t: string) => { let tag = xmlDepthPath.pop(); if (tag && !tag.selfClosing) { @@ -280,7 +336,7 @@ export default class XmlSimpleParser { } }; - parser.oncomment = (t) => { + parser.oncomment = (t: string) => { result.push(``); }; @@ -288,7 +344,7 @@ export default class XmlSimpleParser { result.push(`${eol} { + parser.oncdata = (t: string) => { result.push(t); }; diff --git a/src/helpers/xsdparser.ts b/src/helpers/xsdparser.ts index 091c298..036625a 100644 --- a/src/helpers/xsdparser.ts +++ b/src/helpers/xsdparser.ts @@ -1,149 +1,189 @@ -import { XmlTagCollection, CompletionString } from '../types'; +import { XmlTagCollection, CompletionString, XmlSchemaProperties, XmlTag, XsdType, CsType } from '../types'; +import XsdSettings from './settings'; -export default class XsdParser { - - public static getSchemaTagsAndAttributes(xsdContent: string, xsdUri: string): Promise { - const sax = require("sax"); - const parser = sax.parser(true); - - let getCompletionString = (name: string, comment?: string) => new CompletionString(name, comment, xsdUri, parser.line, parser.column); +interface IXsdStackEntry { + tag: string, + attrName: string; + attrValue: string; +} - return new Promise( - (resolve) => { - let result: XmlTagCollection = new XmlTagCollection(); - let xmlDepthPath: { tag: string, resultTagName: string }[] = []; +export interface IAttributes { + [index: string]: string; +} - parser.onopentag = (tagData: { name: string, isSelfClosing: boolean, attributes: Map }) => { +export default class XsdParser { - xmlDepthPath.push({ - tag: tagData.name, - resultTagName: tagData.attributes["name"] - }); + static boolValues: CompletionString[] = [new CompletionString("true", CsType.AttributeValue), + new CompletionString("false", CsType.AttributeValue)]; - if (tagData.name.endsWith(":schema")) { - Object.keys(tagData.attributes).forEach((k) => - { - if (k.startsWith("xmlns:")) - { - result.setNsMap(k.substring("xmlns:".length), tagData.attributes[k]); - } - }); - } + public static getSchemaTagsAndAttributes(schemaProperties: XmlSchemaProperties, prefix: string = null, fromSchemaProperties: XmlSchemaProperties = null): void { + const sax = /*require("sax");*/ (window as any).sax; + const parser = sax.parser(true); - if (tagData.name.endsWith(":element") && tagData.attributes["name"] !== undefined) { - result.push({ - tag: getCompletionString(tagData.attributes["name"]), - base: [tagData.attributes["type"]], - attributes: [], - visible: true - }); - } + if (!schemaProperties.tagCollection) { + schemaProperties.tagCollection = new XmlTagCollection(); + } + const result = schemaProperties.tagCollection; + + let xmlDepthPath: IXsdStackEntry[] = []; + //let defaultQualified: boolean; + + function addPrefix(name: string): string { + if (!prefix || !name || name.indexOf(":") > -1) + return name; + return prefix + ":" + name; + } + + function getNamedStack(): IXsdStackEntry[] { + return xmlDepthPath.slice() + .reverse() + .filter(e => e.attrName); + } + + parser.onopentag = (tagData: { name: string, isSelfClosing: boolean, attributes: IAttributes }) => { + const xsAttrName = addPrefix(tagData.attributes["name"]); + const xsAttrRef = tagData.attributes["ref"]; + + xmlDepthPath.push({ + tag: tagData.name, + attrName: xsAttrName, + attrValue: tagData.attributes["value"] + }); - if (tagData.name.endsWith(":complexType") && tagData.attributes["name"] !== undefined) { - result.push({ - tag: getCompletionString(tagData.attributes["name"]), - base: [], - attributes: [], - visible: false - }); + if (tagData.name.endsWith(":schema")) { + if (fromSchemaProperties) + return; + Object.keys(tagData.attributes).forEach((k) => { + if (k.startsWith("xmlns:")) { + result.setNsMap(k.substring("xmlns:".length), tagData.attributes[k]); } - - if (tagData.name.endsWith(":attributeGroup") && tagData.attributes["name"] !== undefined) { - result.push({ - tag: getCompletionString(tagData.attributes["name"]), - base: [], - attributes: [], - visible: false + }); + //defaultQualified = tagData.attributes["elementFormDefault"] === "qualified"; + } + else if (tagData.name.endsWith(":element") && xsAttrName) { + const xsAttrType = tagData.attributes["type"]; + const elm = XmlTag.createElement(xsAttrName, xsAttrType ? [addPrefix(xsAttrType)] : []); + result.push(elm); + //formQualified: tagData.attributes["form"] === "qualified" || (defaultQualified && !tagData.attributes["form"]), + + // Elements under element + const currentResultTag = xmlDepthPath.slice(0, xmlDepthPath.length - 1) + .reverse() + .filter(e => e.attrName)[0]; + + if (currentResultTag) { + result.filter(e => e.tag.name === addPrefix(currentResultTag.attrName)) + .forEach(e => e.childElements.push(elm)); + } + } + else if ((tagData.name.endsWith(":group") || tagData.name.endsWith(":element")) && xsAttrRef) { + result.filter(e => e.tag.name === addPrefix(getNamedStack()[0].attrName)) + .forEach(e => e.baseElements.push(addPrefix(xsAttrRef))); + } + else if ((tagData.name.endsWith(":complexType") || tagData.name.endsWith(":group")) && xsAttrName) { + result.push(XmlTag.createElementRef(xsAttrName)); + } + else if (tagData.name.endsWith(":attributeGroup") && xsAttrName) { + result.push(XmlTag.createAttributeGroup(xsAttrName)); + } + else if (tagData.name.endsWith(":attribute") && xsAttrName) { + const xsAttrType = tagData.attributes["type"]; + result.filter(e => e.tag.name === getNamedStack()[1].attrName) + .forEach(e => e.attributes.push(new CompletionString(xsAttrName, CsType.Attribute, null, + xsAttrType && xsAttrType.endsWith(":boolean") ? XsdParser.boolValues : null))); + } + else if (tagData.name.endsWith(":extension") && tagData.attributes["base"]) { + result.filter(e => e.tag.name === getNamedStack()[0].attrName) + .forEach(e => e.baseAttributes.push(addPrefix(tagData.attributes["base"]))); + } + else if (tagData.name.endsWith(":attributeGroup") && xsAttrRef) { + result.filter(e => e.tag.name === getNamedStack()[0].attrName) + .forEach(e => e.baseAttributes.push(addPrefix(xsAttrRef))); + } + else if (tagData.name.endsWith(":import") && tagData.attributes["namespace"]) { + const importSchemaProperties = XsdSettings.schemaPropertiesArray.filter(sp => sp.namespace === tagData.attributes["namespace"])[0]; + + if (!importSchemaProperties) { + console.log("import setting not found: " + tagData.attributes["namespace"]); + return; + } + if (prefix) { + //console.log("only first import level supported"); + return; + } + const importPrefix = result.prefixMap.get(importSchemaProperties.namespace); + if (!importPrefix) { + console.log("import ns not found: " + tagData.attributes["namespace"]); + return; + } + //console.log("IMPORTING " + importPrefix + ": " + importSchemaProperties.namespace); + XsdParser.getSchemaTagsAndAttributes(schemaProperties, importPrefix, importSchemaProperties); + } + }; + + parser.onclosetag = (name: string) => { + let popped = xmlDepthPath.pop(); + if (popped && popped.tag !== name) { + console.warn("XSD open/close tag consistency error."); + } + }; + + parser.ontext = (t: string) => { + if (/\S/.test(t)) { + let stack = xmlDepthPath.slice().reverse(); + + if (!stack.find(e => e.tag.endsWith(":documentation"))) { + return; + } + let currentCommentTarget = stack.filter(e => e.attrName)[0]; + if (!currentCommentTarget) { + return; + } + if (currentCommentTarget.tag.endsWith(":element")) { + result + .filter(e => e.xsdType === XsdType.Element && e.tag.name === currentCommentTarget.attrName) + .forEach(e => e.tag.comment = t.trim()); + } + else if (currentCommentTarget.tag.endsWith(":complexType")) { + result + .filter(e => e.xsdType === XsdType.ElementRef && e.tag.name === currentCommentTarget.attrName) + .forEach(e => e.tag.comment = t.trim()); + result + .filter(e => e. + baseElements.findIndex(t => t === currentCommentTarget.attrName) > -1) + .forEach(e => e.tag.comment = t.trim()); + } + else if (currentCommentTarget.tag.endsWith(":attribute")) { + const attribs = result.filter(t => t.tag.name === getNamedStack()[1].attrName) + .map(e => e.attributes) + .reduce((prev, next) => prev.concat(next), []) + .filter(e => currentCommentTarget && e.name === currentCommentTarget.attrName); + + const currentEnum = stack.filter(e => e.tag.endsWith(":enumeration"))[0]; + if (currentEnum) { + attribs.forEach(e => { + if (!e.documentation) { + e.comment += ". Has value restrictions! See scrollable list below:" + e.documentation = { value: "" } + e.values = []; + } + e.documentation.value += `\n- **${currentEnum.attrValue}** : ${t.replace(/\s+\n*/g, " ")}`; + e.values.push(new CompletionString(currentEnum.attrValue, CsType.AttributeValue, t.trim())); }); + return; } - if (tagData.name.endsWith(":attribute") && tagData.attributes["name"] !== undefined) { - let currentResultTag = xmlDepthPath - .slice() - .reverse() - .filter(e => e.resultTagName !== undefined)[1]; - result - .filter(e => e.tag.name === currentResultTag.resultTagName) - .forEach(e => e.attributes.push(getCompletionString(tagData.attributes["name"]))); - } - - if (tagData.name.endsWith(":extension") && tagData.attributes["base"] !== undefined) { - let currentResultTag = xmlDepthPath - .slice() - .reverse() - .filter(e => e.resultTagName !== undefined)[0]; - - result - .filter(e => e.tag.name === currentResultTag.resultTagName) - .forEach(e => e.base.push(tagData.attributes["base"])); - } + attribs.forEach(e => e.comment = t.trim()); + } + } + }; - if (tagData.name.endsWith(":attributeGroup") && tagData.attributes["ref"] !== undefined) { - let currentResultTag = xmlDepthPath - .slice() - .reverse() - .filter(e => e.resultTagName !== undefined)[0]; - - result - .filter(e => e.tag.name === currentResultTag.resultTagName) - .forEach(e => e.base.push(tagData.attributes["ref"])); - } - - if (tagData.name.endsWith(":import") && tagData.attributes["schemaLocation"] !== undefined) { - // TODO: handle this somehow, possibly separate methood to be called: - // importFiles.push(tagData.attributes["schemaLocation"]); - } - }; + parser.onend = () => { + if (xmlDepthPath.length !== 0) { + console.warn("XSD open/close tag consistency error (end)."); + } + }; - parser.onclosetag = (name: string) => { - let popped = xmlDepthPath.pop(); - if (popped !== undefined && popped.tag !== name) { - console.warn("XSD open/close tag consistency error."); - } - }; - - parser.ontext = (t: string) => { - if (/\S/.test(t)) { - let stack = xmlDepthPath - .slice() - .reverse(); - - if (!stack.find(e => e.tag.endsWith(":documentation"))) { - return; - } - - let currentCommentTarget = - stack - .filter(e => e.resultTagName !== undefined)[0]; - - if (!currentCommentTarget) { - return; - } - - if (currentCommentTarget.tag.endsWith(":element")) { - result - .filter(e => currentCommentTarget && e.tag.name === currentCommentTarget.resultTagName) - .forEach(e => e.tag.comment = t.trim()); - } - else if (currentCommentTarget.tag.endsWith(":attribute")) { - result - .map(e => e.attributes) - .reduce((prev, next) => prev.concat(next), []) - .filter(e => currentCommentTarget && e.name === currentCommentTarget.resultTagName) - .forEach(e => e.comment = t.trim()); - } - } - }; - - parser.onend = () => { - if (xmlDepthPath.length !== 0) { - console.warn("XSD open/close tag consistency error (end)."); - } - - resolve(result); - }; - - parser.write(xsdContent).close(); - }); + parser.write((fromSchemaProperties || schemaProperties).xsdContent).close(); } } \ No newline at end of file diff --git a/src/linterprovider.ts b/src/linterprovider.ts index 7298ca6..1186fbf 100644 --- a/src/linterprovider.ts +++ b/src/linterprovider.ts @@ -1,124 +1,63 @@ -import * as vscode from 'vscode'; -import { languageId, globalSettings } from './extension'; -import { XmlSchemaProperties, XmlTagCollection, XmlSchemaPropertiesArray, XmlDiagnosticData } from './types'; -import XsdParser from './helpers/xsdparser'; -import XsdCachedLoader from './helpers/xsdcachedloader'; +import { XmlDiagnosticData } from './types'; import XmlSimpleParser from './helpers/xmlsimpleparser'; - -export default class XmlLinterProvider implements vscode.Disposable { - - private documentListener: vscode.Disposable; - private diagnosticCollection: vscode.DiagnosticCollection; - private delayCount: number = 0; - private textDocument: vscode.TextDocument; - - constructor(protected extensionContext: vscode.ExtensionContext, protected schemaPropertiesArray: XmlSchemaPropertiesArray) { - this.schemaPropertiesArray = schemaPropertiesArray; - this.diagnosticCollection = vscode.languages.createDiagnosticCollection(); - - this.documentListener = vscode.workspace.onDidChangeTextDocument(evnt => - this.triggerDelayedLint(evnt.document), this, this.extensionContext.subscriptions); - - vscode.workspace.onDidOpenTextDocument(doc => - this.triggerDelayedLint(doc, 100), this, extensionContext.subscriptions); - - vscode.workspace.onDidCloseTextDocument(doc => - this.cleanupDocument(doc), null, extensionContext.subscriptions); - } - - public dispose() { - this.documentListener.dispose(); - this.diagnosticCollection.clear(); - } - - private cleanupDocument(textDocument: vscode.TextDocument): void { - this.diagnosticCollection.delete(textDocument.uri); - } - - private async triggerDelayedLint(textDocument: vscode.TextDocument, timeout: number = 2000): Promise { - if (this.delayCount > 0) { - this.delayCount = timeout; - this.textDocument = textDocument; - return; - } - this.delayCount = timeout; - this.textDocument = textDocument; - - const tick = 100; - - while (this.delayCount > 0) { - await new Promise(resolve => setTimeout(resolve, tick)); - this.delayCount -= tick; - } - - this.triggerLint(this.textDocument); - } - - private async triggerLint(textDocument: vscode.TextDocument): Promise { - - if (textDocument.languageId !== languageId) { - return; - } - - let diagnostics: Array = new Array(); - try { - let documentContent = textDocument.getText(); - - let xsdFileUris = (await XmlSimpleParser.getSchemaXsdUris(documentContent, globalSettings.schemaMapping)) - .map(u => vscode.Uri.parse(u)) - .filter((v, i, a) => a.findIndex(u => u.toString() === v.toString()) === i); - - let nsMap = await XmlSimpleParser.getNamespaceMapping(documentContent); - - const text = textDocument.getText(); - - for (let xsdUri of xsdFileUris) { - let schemaProperties = this.schemaPropertiesArray - .filterUris([xsdUri])[0]; - - if (schemaProperties === undefined) { - schemaProperties = { schemaUri: xsdUri, xsdContent: ``, tagCollection: new XmlTagCollection() } as XmlSchemaProperties; - - try { - let xsdUriString = xsdUri.toString(true); - schemaProperties.xsdContent = await XsdCachedLoader.loadSchemaContentsFromUri(xsdUriString); - schemaProperties.tagCollection = await XsdParser.getSchemaTagsAndAttributes(schemaProperties.xsdContent, xsdUriString); - vscode.window.showInformationMessage(`Loaded ...${xsdUri.toString().substr(xsdUri.path.length - 16)}`); - } - catch (err) { - vscode.window.showErrorMessage(err.toString()); - } finally { - this.schemaPropertiesArray.push(schemaProperties); - } - } - - const strict = !globalSettings.schemaMapping.find(m => m.xsdUri === xsdUri.toString() && m.strict === false); - let diagnosticResults = await XmlSimpleParser.getXmlDiagnosticData(text, schemaProperties.tagCollection, nsMap, strict); - - diagnostics.push(this.getDiagnosticArray(diagnosticResults)); - } - - if (xsdFileUris.length === 0) { - const planXmlCheckResults = await XmlSimpleParser.getXmlDiagnosticData(text, new XmlTagCollection(), nsMap, false); - diagnostics.push(this.getDiagnosticArray(planXmlCheckResults)); - } - - this.diagnosticCollection.set(textDocument.uri, diagnostics - .reduce((prev, next) => prev.filter(dp => next.find(dn => dn.range.start.compareTo(dp.range.start) === 0)))); - } - catch (err) { - vscode.window.showErrorMessage(err.toString()); - } - } - - private getDiagnosticArray(data: XmlDiagnosticData[]): vscode.Diagnostic[] { - return data.map(r => { - let position = new vscode.Position(r.line, r.column); - let severity = (r.severity === "error") ? vscode.DiagnosticSeverity.Error : - (r.severity === "warning") ? vscode.DiagnosticSeverity.Warning : - (r.severity === "info") ? vscode.DiagnosticSeverity.Information : - vscode.DiagnosticSeverity.Hint; - return new vscode.Diagnostic(new vscode.Range(position, position), r.message, severity); - }); - } +import { Updater } from '../util/Updater'; +import XsdSettings from './helpers/settings'; + +export default class XmlLinterProvider extends Updater implements monaco.IDisposable { + + constructor(languageId: string, changedDelay: number = 2000) { + super(languageId, changedDelay); + } + + async doUpdate(resource: monaco.Uri, languageId: string): Promise { + const textDocument = monaco.editor.getModel(resource); + if (!textDocument || (textDocument.getModeId() !== "xml" && textDocument.getModeId() !== "html")) { + return null; + } + + let documentContent = textDocument.getValue(); + + let xsdFileUris = (XmlSimpleParser.getSchemaXsdUris(documentContent, + XsdSettings.schemaMapping.filter(sm => !sm.noComplete))).map(u => monaco.Uri.parse(u)); + + await XsdSettings.prepare(xsdFileUris); + + let nsMap = await XmlSimpleParser.getNamespaceMapping(documentContent); + + const markers: Array = []; + + if (xsdFileUris.length === 0) { + const planXmlCheckResults = XmlSimpleParser.getXmlDiagnosticData(documentContent, null, nsMap); + markers.push(this.getDiagnosticArray(planXmlCheckResults)); + } + else { + let schemaProperties = XsdSettings.schemaPropertiesArray.filterUris(xsdFileUris); + let diagnosticResults = XmlSimpleParser.getXmlDiagnosticData(documentContent, schemaProperties, nsMap); + markers.push(this.getDiagnosticArray(diagnosticResults)); + } + + monaco.editor.setModelMarkers(textDocument, "xml-lint", + markers.reduce((prev, next) => + prev.filter(dp => next.find(dn => dn.startLineNumber === dp.startLineNumber && dn.startColumn === dp.startColumn)))); + } + + private getDiagnosticArray(data: XmlDiagnosticData[]): monaco.editor.IMarkerData[] { + return data.map(r => { + return { + message: r.message, + severity: (r.severity === "error") ? monaco.MarkerSeverity.Error : + (r.severity === "warning") ? monaco.MarkerSeverity.Warning : + (r.severity === "info") ? monaco.MarkerSeverity.Info : + monaco.MarkerSeverity.Hint, + startLineNumber: r.range.startLineNumber + 1, + startColumn: r.range.startColumn, + endLineNumber: r.range.endLineNumber + 1, + endColumn: r.range.endColumn, + } as monaco.editor.IMarkerData; + }); + } + + public dispose(): void { + super.dispose(); + } } \ No newline at end of file diff --git a/src/tsconfig.json b/src/tsconfig.json index 96853da..aa7824f 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -3,7 +3,7 @@ "module": "commonjs", "target": "es2018", - "noImplicitAny": false, + "noImplicitAny": true, "removeComments": true, "noUnusedLocals": true, "noImplicitThis": true, diff --git a/src/types.ts b/src/types.ts index 5e4ff3c..ea1c86e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,123 +1,207 @@ -import * as vscode from 'vscode'; +import { INsMap } from "./helpers/xmlsimpleparser"; -export class XmlCompleteSettings { - schemaMapping: { xmlns: string, xsdUri: string, strict: boolean }[]; - formattingStyle: "singleLineAttributes" | "multiLineAttributes" | "fileSizeOptimized"; +//export class XmlCompleteSettings { +// schemaMapping: { xmlns: string, xsdUri: string, strict: boolean }[]; +// formattingStyle: "singleLineAttributes" | "multiLineAttributes" | "fileSizeOptimized"; +//} + + +export const enum CsType { + Element = 1, + Attribute = 2, + AttributeValue = 3, } export class CompletionString { + public documentation?: monaco.IMarkdownString; + + constructor(public name: string, public type: CsType, public comment?: string, public values?: CompletionString[]) { + } - constructor(public name: string, public comment?: string, public definitionUri?:string, public definitionLine?:number, public definitionColumn?:number) { + public newWithName?(name: string): CompletionString { + const clone = Object.create(this); + clone.name = name; + return clone; } } +export const enum XsdType { + Unknown = 0, + Element = 1, + ElementRef = 2, + AttributeGroup = 3, +} + export class XmlTag { + + private constructor(tag: string) { + this.tag = new CompletionString(tag, CsType.Element); + } + + static createElement(tag: string, baseElements: string[]): XmlTag { + const x = new XmlTag(tag); + x.baseElements = baseElements; + x.visible = true; + x.xsdType = XsdType.Element; + return x; + } + + static createElementRef(tag: string): XmlTag { + const x = new XmlTag(tag); + x.xsdType = XsdType.ElementRef; + return x; + } + + static createAttributeGroup(tag: string): XmlTag { + const x = new XmlTag(tag); + x.xsdType = XsdType.AttributeGroup; + return x; + } + tag: CompletionString; - base: string[]; - attributes: Array; - visible: boolean; + baseAttributes?: string[] = []; + attributes?: CompletionString[] = []; + visible?: boolean = false; + + formQualified?: boolean; + xsdType?: XsdType; + baseElements?: string[] = []; + childElements?: XmlTag[] = []; } export class XmlTagCollection extends Array { - private nsMap: Map = new Map(); + private nsMap: Map; + prefixMap: Map; - setNsMap(xsdNsTag:string, xsdNsStr:string) { - this.nsMap.set(xsdNsTag, xsdNsStr); + constructor() { + super(); + this.nsMap = new Map(); + this.prefixMap = new Map(); } - loadAttributesEx(tagName: string | undefined, localXmlMapping: Map): CompletionString[] { - let result: CompletionString[] = []; - if (tagName !== undefined) { - let fixedNames = this.fixNsReverse(tagName, localXmlMapping); - fixedNames.forEach(fixn => { - result.push(...this.loadAttributes(fixn)); - }); - } - - return result; + setNsMap(xsdNsTag: string, xsdNsStr: string) { + this.nsMap.set(xsdNsTag, xsdNsStr); + this.prefixMap.set(xsdNsStr, xsdNsTag); } - loadTagEx(tagName: string | undefined, localXmlMapping: Map): CompletionString | undefined { - let result = undefined; - if (tagName !== undefined) { - let fixedNames = this.fixNsReverse(tagName, localXmlMapping); - let element =this.find(e => fixedNames.includes(e.tag.name)) - if (element !== undefined) { - return element.tag; + loadAttributesEx(tagName: string, localNsMap: INsMap, namespace: string): CompletionString[] { + let result: CompletionString[] = []; + if (tagName) { + let arr = tagName.split(":"); + if (arr.length === 2) { + if (localNsMap.uriToNs.get(namespace) !== arr[0]) { + return result; + } + this.loadAttributes(arr[1], result); + } + else { + this.loadAttributes(tagName, result); } } - return result; } - loadAttributes(tagName: string | undefined, handledNames: string[] = []): CompletionString[] { + loadAttributes(tagName: string, result: CompletionString[], recursiveCheck: string[] = [], recursiveCall: number = 0) { + //console.log("--".repeat(recursiveCall) + " a: " + tagName); + const currentTags = this.filter(e => e.tag.name === tagName); + if (!currentTags.length) + return; + + result.push(...currentTags.map(e => e.attributes).reduce((prev, next) => prev.concat(next), [])); + + currentTags.forEach(e => + e.baseAttributes.filter(b => !currentTags.map(t => t.tag.name).includes(b) && !recursiveCheck.includes(b)) + .forEach(b => { + recursiveCheck.push(b); + this.loadAttributes(b, result, recursiveCheck, recursiveCall + 1); + })); + + currentTags.filter(t => t.xsdType === XsdType.AttributeGroup || (recursiveCall === 0 && t.xsdType === XsdType.Element)) + .forEach(e => + e.baseElements.filter(b => !currentTags.map(t => t.tag.name).includes(b) && !recursiveCheck.includes(b)) + .forEach(b => { + recursiveCheck.push(b); + this.loadAttributes(b, result, recursiveCheck, recursiveCall + 1); + })); + } - let tagNameCompare = (a: string, b: string) => a === b || a === b.substring(b.indexOf(":")+1); + loadTagEx(tagName: string | undefined, localNsMap: INsMap): CompletionString[] { let result: CompletionString[] = []; - if (tagName !== undefined) { - handledNames.push(tagName); - let currentTags = this.filter(e => tagNameCompare(e.tag.name, tagName)); - if (currentTags.length > 0) { - result.push(...currentTags.map(e => e.attributes).reduce((prev, next) => prev.concat(next), [])); - currentTags.forEach(e => { - e.base.filter(b => !handledNames.includes(b)) - .forEach(b => result.push(...this.loadAttributes(b))) - }); - } + if (tagName) { + this.loadTags(tagName, result); + } + else { + return this.filter(e => e.visible).map(e => e.tag); } return result; } - fixNs(xsdString: CompletionString, localXmlMapping: Map): CompletionString { - let arr = xsdString.name.split(":"); - if (arr.length === 2 && this.nsMap.has(arr[0]) && localXmlMapping.has(this.nsMap[arr[0]])) - { - return new CompletionString (localXmlMapping[this.nsMap[arr[0]]] + ":" + arr[1], xsdString.comment, xsdString.definitionUri, xsdString.definitionLine, xsdString.definitionColumn); - } - return xsdString; + loadTags(tagName: string, result: CompletionString[], recursiveCheck: string[] = [], recursiveCall: number = 0) { + // NOTE: Does not support restriction/difference between same element name under different parents!! need complete'ish tree for that + //console.log("--".repeat(recursiveCall) + " e: " + tagName); + const currentTags = this.filter(e => e.tag.name === tagName); + if (!currentTags.length) + return; + + if (recursiveCall > 0) + currentTags.filter(t => t.visible).forEach(t => result.push(t.tag)); + + result.push(...currentTags.map(e => e.childElements.map(t => t.tag)).reduce((prev, next) => prev.concat(next), [])); + + currentTags.filter(t => t.xsdType === XsdType.ElementRef || (recursiveCall === 0 && t.xsdType === XsdType.Element)) + .forEach(e => + e.baseElements.filter(b => b && !currentTags.map(t => t.tag.name).includes(b) && !recursiveCheck.includes(b)) + .forEach(b => { + recursiveCheck.push(b); + this.loadTags(b, result, recursiveCheck, recursiveCall + 1); + })); } - fixNsReverse(xmlString: string, localXmlMapping: Map): Array { - let arr = xmlString.split(":"); - let xmlStrings = new Array(); - - localXmlMapping.forEach((v, k) => { - if (v === arr[0]) { - this.nsMap.forEach((v2, k2) => { - if (v2 == k) { - xmlStrings.push(k2 + ":" + arr[1]); - } - }); + completeNsTagName(tagName: string, localNsMap: INsMap, nsUri: string, parentTagNs: string): string { + let arr = tagName.split(":"); + if (arr.length === 2) { + if (this.nsMap.has(arr[0]) && localNsMap.uriToNs.has(this.nsMap.get(arr[0]))) { + const ns = localNsMap.uriToNs.get(this.nsMap.get(arr[0])); + return ns === "" || parentTagNs === ns ? arr[1] : ns + ":" + arr[1]; } - }); - xmlStrings.push(arr[arr.length-1]); + } + else if (arr.length === 1 && nsUri && localNsMap.uriToNs.has(nsUri)) { + const ns = localNsMap.uriToNs.get(nsUri); + return ns === "" || ns === parentTagNs ? tagName : ns + ":" + tagName; + } + return tagName; + } - return xmlStrings; + fixNs(xsdString: CompletionString, localNsMap: INsMap, sp: XmlSchemaProperties, parentTagNs: string = null): CompletionString { + const newName = this.completeNsTagName(xsdString.name, localNsMap, sp ? sp.namespace : null, parentTagNs); + return xsdString.name === newName ? xsdString : xsdString.newWithName(newName); } + } export class XmlSchemaProperties { - schemaUri: vscode.Uri; + schemaUri: monaco.Uri; xsdContent: string; tagCollection: XmlTagCollection; + namespace: string; } export class XmlSchemaPropertiesArray extends Array { - filterUris(uris: vscode.Uri[]): Array { + filterUris(uris: monaco.Uri[]): Array { return this.filter(e => uris .find(u => u.toString() === e.schemaUri.toString()) !== undefined); } } export class XmlDiagnosticData { - line: number; - column: number; + range: monaco.Range; message: string; severity: "error" | "warning" | "info" | "hint"; } export class XmlScope { tagName: string | undefined; - context: "element" | "attribute" | "text" | undefined; + parentTagName: string; + context?: "element" | "attribute" | "text" | undefined; } \ No newline at end of file