| /*--------------------------------------------------------------------------------------------- |
| * Copyright (c) 2020 DeltaXML Ltd. All rights reserved. |
| * Licensed under the MIT License. See License.txt in the project root for license information. |
| *--------------------------------------------------------------------------------------------*/ |
| |
| import { EOL } from 'os' |
| import * as vscode from 'vscode' |
| import { XPathLexer, ExitCondition, LexPosition, Token } from './xpLexer' |
| import { XslLexer } from './xslLexer' |
| import { XsltTokenDiagnostics } from './xsltTokenDiagnostics' |
| import { DocumentChangeHandler } from './documentChangeHandler' |
| |
| const tokenModifiers = new Map<string, number>() |
| const startList: vscode.Position[] = [] |
| const endList: vscode.Position[] = [] |
| |
| const legend = (function () { |
| const tokenTypesLegend = XslLexer.getTextmateTypeLegend() |
| |
| const tokenModifiersLegend = [ |
| 'declaration', |
| 'documentation', |
| 'member', |
| 'static', |
| 'abstract', |
| 'deprecated', |
| 'modification', |
| 'async', |
| ] |
| tokenModifiersLegend.forEach((tokenModifier, index) => |
| tokenModifiers.set(tokenModifier, index) |
| ) |
| |
| return new vscode.SemanticTokensLegend(tokenTypesLegend, tokenModifiersLegend) |
| })() |
| |
| export class XPathSemanticTokensProvider |
| implements vscode.DocumentSemanticTokensProvider |
| { |
| private xpLexer = new XPathLexer() |
| private collection: vscode.DiagnosticCollection |
| private diagnosticList = new Array<vscode.Diagnostic>() |
| public constructor(collection: vscode.DiagnosticCollection) { |
| this.collection = collection |
| } |
| |
| async provideDocumentSemanticTokens( |
| document: vscode.TextDocument, |
| token: vscode.CancellationToken |
| ): Promise<vscode.SemanticTokens> { |
| this.xpLexer.documentTokens = [] |
| let [variables, namespaces]: [string[], string[]] = |
| this.findAllVariablesandNamespaces(document.getText()) |
| let tokens: Token[] = [] |
| |
| const tokenPositions = this.findAllXPath(document.getText()) |
| |
| for (let i = 0; i < tokenPositions.length; i++) { |
| const line = tokenPositions[i][0] |
| const startCharacter = tokenPositions[i][1] |
| const documentOffset = tokenPositions[i][2] |
| |
| const lexPositions: LexPosition = { |
| line: line, |
| startCharacter: startCharacter, |
| documentOffset: documentOffset, |
| } |
| let start = new vscode.Position(line, startCharacter) |
| let tmpTokens = this.xpLexer.analyse( |
| document.getText(), |
| ExitCondition.CurlyBrace, |
| lexPositions |
| ) |
| let end = new vscode.Position( |
| lexPositions.line, |
| lexPositions.startCharacter |
| ) |
| //Add start and end positions of XPath so we don't have to recalculate every time Intellisense is triggered |
| startList.push(start) |
| endList.push(end) |
| tokens = tokens.concat(tmpTokens) |
| |
| // This was moved to inside the loop. If it isn't, the sections of XPath will be treated |
| // as a single XPath section instead of multiples |
| setTimeout( |
| () => this.reportProblems(tmpTokens, document, variables, namespaces), |
| 0 |
| ) |
| |
| // Reset the xpLexer. If this is not done, existing tokens will not be flushed |
| // and will be re-added to the tokens list. This might not affect the operation, but it does |
| // increase the memory required by the tokenizer, potentially running out of memory. |
| this.xpLexer.reset() |
| } |
| |
| const builder = new vscode.SemanticTokensBuilder() |
| tokens.forEach((token) => { |
| builder.push( |
| token.line, |
| token.startCharacter, |
| token.length, |
| token.tokenType, |
| 0 |
| ) |
| }) |
| this.diagnosticList = new Array<vscode.Diagnostic>() |
| return builder.build() |
| } |
| |
| // Identify all sections in the full document that should be treated as XPath |
| private findAllXPath(document: String): [number, number, number][] { |
| let tokensFound: [number, number, number][] = [] |
| let charCount = 0 |
| const lines = document.split('\n') |
| |
| // Regex should match up to the character before we need to start detecting XPath |
| // In these cases, there is a left curly brace right after the regex match, so |
| // we may need to adjust the exact points if there are schemas with spaces between |
| // the open quote and the left curly brace. |
| // Also note that the start location that we return for processing should NOT include the |
| // left curly brace. The way that the tokenizer determines when to stop processing |
| // is to find an extra closing character (curly brace, single quote, or double quote) |
| // If it doesn't terminate, it will tokenize the remainder of the file. |
| const xPathRegex = /(\w+)=("|')(?=\{)/ |
| let isComment: Boolean = false |
| |
| for (let i = 0; i < lines.length; i++) { |
| let xPathMatch = lines[i].match(xPathRegex) |
| |
| if (!isComment && lines[i].includes('<!--')) { |
| isComment = true |
| } |
| |
| if (isComment) { |
| let closeIndex = lines[i].search('-->') |
| |
| if (closeIndex !== -1) { |
| isComment = false |
| |
| if (xPathMatch) { |
| if (closeIndex > lines[i].search(xPathMatch[0])) { |
| xPathMatch = null |
| } |
| } |
| } else { |
| xPathMatch = null |
| } |
| } |
| |
| // The items in the tuple are used to determine the start point for the tokenizer. They are |
| // the line number, position offset in the line, and document offset. |
| // The +1 on the position offset accounts for the opening curly brace. |
| if (xPathMatch) { |
| const lineOffset = |
| lines[i].search(xPathMatch[0]) + xPathMatch[0].length + 1 |
| tokensFound.push([ |
| i, |
| (xPathMatch.index || 0) + xPathMatch[0].length + 1, |
| charCount + lineOffset, |
| ]) |
| } |
| |
| // Used to keep track of the document offset. The +1 accounts for newlines. |
| charCount += lines[i].length + 1 |
| } |
| |
| return tokensFound |
| } |
| |
| // Find the names of all variables in the file |
| private findAllVariablesandNamespaces( |
| document: String | undefined |
| ): [string[], string[]] { |
| if (document === undefined) { |
| return [[], []] |
| } |
| |
| const lines = document.split(EOL) |
| |
| const variableRegex = /(dfdl:defineVariable.*name=\")(.*?)\"/ |
| // These are built-in predefined variables. Double check if we still need to hardcode these |
| // once we have the ability to create a DFDL model outside of a Parse operation. |
| const variables: string[] = [ |
| 'dfdl:encoding', |
| 'dfdl:byteOrder', |
| 'dfdl:binaryFloatRep', |
| 'dfdl:outputNewLine', |
| ] |
| |
| const namespaceRegex = /xmlns:(.*?)=\"/g |
| const namespaces: string[] = [] |
| |
| // Capture and return a list of variable names |
| for (let i = 0; i < lines.length; i++) { |
| const variableMatch = lines[i].match(variableRegex) |
| |
| if (variableMatch) { |
| variables.push(variableMatch[2]) |
| } |
| |
| // If a DFDL schema has multiple namespaces defined on the same line, we need to make sure we are globally |
| // matching for the namespace pattern so that we don't miss any |
| let namespaceMatch: RegExpExecArray | null = null |
| |
| // Continue matching until we don't find any more namespaces on the same line |
| do { |
| namespaceMatch = namespaceRegex.exec(lines[i]) |
| |
| if (namespaceMatch) { |
| namespaces.push(namespaceMatch[1]) |
| } |
| } while (namespaceMatch) |
| } |
| |
| return [variables, namespaces] |
| } |
| |
| // This function will produce the error/warning list for vscode |
| private reportProblems( |
| allTokens: Token[], |
| document: vscode.TextDocument, |
| variables: string[], |
| namespaces: string[] |
| ) { |
| let diagnostics = XsltTokenDiagnostics.calculateDiagnostics( |
| document, |
| allTokens, |
| DocumentChangeHandler.lastXMLDocumentGlobalData, |
| [], |
| [], |
| variables, |
| namespaces |
| ) |
| diagnostics.forEach((diag) => { |
| this.diagnosticList.push(diag) |
| }) |
| if (this.diagnosticList.length > 0) { |
| this.collection.set(document.uri, this.diagnosticList) |
| } else { |
| this.collection.clear() |
| } |
| } |
| } |
| |
| export function isXPath(position: vscode.Position): boolean { |
| for (let i = 0; i < startList.length; i++) { |
| if ( |
| position.isBeforeOrEqual(endList[i]) && |
| position.isAfterOrEqual(startList[i]) |
| ) |
| return true |
| } |
| return false |
| } |
| |
| export function activate(context: vscode.ExtensionContext) { |
| const docChangeHandler = new DocumentChangeHandler() |
| let activeEditor = vscode.window.activeTextEditor |
| |
| if (activeEditor) { |
| docChangeHandler.registerXMLEditor(activeEditor) |
| } |
| |
| const xpathDiagnosticsCollection = |
| vscode.languages.createDiagnosticCollection('dfdl') |
| context.subscriptions.push( |
| vscode.languages.registerDocumentSemanticTokensProvider( |
| { language: 'dfdl' }, |
| new XPathSemanticTokensProvider(xpathDiagnosticsCollection), |
| legend |
| ) |
| ) |
| } |