import XsdManager from './XsdManager' import { editor, IPosition, languages, Position } from 'monaco-editor' import { XsdWorker } from './XsdWorker' import CompletionItemProvider = languages.CompletionItemProvider import ITextModel = editor.ITextModel import CompletionContext = languages.CompletionContext import ProviderResult = languages.ProviderResult import CompletionList = languages.CompletionList import CompletionItem = languages.CompletionItem import CompletionTriggerKind = languages.CompletionTriggerKind import CompletionItemKind = languages.CompletionItemKind import IWordAtPosition = editor.IWordAtPosition import { CompletionType, ICompletion } from './types' import { SimpleParser } from './SimpleParser' import { XsdNamespaces } from './XsdNamespaces' export default class XsdCompletion { private xsdManager: XsdManager constructor(xsdCollection: XsdManager) { this.xsdManager = xsdCollection } public provider = (): CompletionItemProvider => ({ triggerCharacters: ['<', ' ', '\n', '/'], provideCompletionItems: ( model: ITextModel, position: Position, context: CompletionContext, ): ProviderResult => ({ suggestions: this.getCompletionItems(model, position, context), }), }) private getCompletionItems = ( model: ITextModel, position: Position, context: CompletionContext, ): CompletionItem[] => { const completions: ICompletion[] = this.getCompletions(model, position, context) const wordUntilPosition = model.getWordUntilPosition(position) const tagBeforePosition = this.getLastTagBeforePosition(model, position) const startColumn = this.getStartColumnForTagWithNamespace( wordUntilPosition, tagBeforePosition, ) const wordRange = { startColumn: startColumn, startLineNumber: position.lineNumber, endColumn: wordUntilPosition.endColumn, endLineNumber: position.lineNumber, } return completions.map((completion: ICompletion): CompletionItem => { return { ...completion, ...{ range: wordRange }, } }) } private getStartColumnForTagWithNamespace = ( wordUntilPosition: IWordAtPosition, tagBeforePosition: string | undefined, ) => { if ( wordUntilPosition && tagBeforePosition && wordUntilPosition.word === this.getTagName(tagBeforePosition) ) { const lengthDifferance = Math.abs( tagBeforePosition.length - wordUntilPosition.word.length, ) return wordUntilPosition.startColumn - lengthDifferance } return wordUntilPosition.startColumn } private getCompletions = ( model: ITextModel, position: Position, context: CompletionContext, ): ICompletion[] | [] => { const completionType = this.getCompletionType(model, position, context) if (completionType == CompletionType.none) return [] const parentTag = this.getParentTag(model, position) if (completionType == CompletionType.closingElement && parentTag) return this.getClosingElementCompletion(parentTag) const namespaces = XsdNamespaces.getXsdNamespaces(model) const xsdWorkers = XsdNamespaces.getXsdWorkersForNamespace(namespaces, this.xsdManager) const parentTagName = this.getTagName(parentTag) let completions: ICompletion[] = [] xsdWorkers.forEach((xsdWorker: XsdWorker) => { completions = [ ...completions, ...xsdWorker.doCompletion( completionType, parentTagName ? parentTagName : parentTag, ), ] }) return completions } private getCompletionType = ( model: ITextModel, position: Position, context: CompletionContext, ): CompletionType => { const wordsBeforePosition = this.getWordsBeforePosition(model, position) const textUntilPosition = this.getTextUntilPosition(model, position) if (this.isInsideAttributeValue(wordsBeforePosition)) return CompletionType.none if (this.isInsideComment(textUntilPosition)) return CompletionType.none switch (context.triggerKind) { case CompletionTriggerKind.Invoke: { const completionType = this.getCompletionTypeByPreviousText(textUntilPosition) if (completionType !== undefined) return completionType return this.getCompletionTypeForIncompleteCompletion(wordsBeforePosition) } case CompletionTriggerKind.TriggerForIncompleteCompletions: return this.getCompletionTypeForIncompleteCompletion(wordsBeforePosition) case CompletionTriggerKind.TriggerCharacter: return this.getCompletionTypeByTriggerCharacter( context.triggerCharacter, textUntilPosition, ) } } private isInsideAttributeValue = (text: string): boolean => { const regexForInsideAttributeValue = /="[^"]+$/ const matches = text.match(regexForInsideAttributeValue) return !!matches } private getCompletionTypeForIncompleteCompletion = (text: string): CompletionType => { const currentTag = this.getTextFromCurrentTag(text) if (currentTag) { if (this.textContainsAttributes(currentTag)) return CompletionType.incompleteAttribute if (this.textContainsTags(currentTag)) return CompletionType.incompleteElement } return CompletionType.snippet } private getTextFromCurrentTag = (text: string): string => SimpleParser.getMatchesForRegex(text, /(<(?!--)\/*[^>]*)$/g)[0] private textContainsAttributes = (text: string): boolean => this.getAttributesFromText(text).length > 0 private getAttributesFromText = (text: string): string[] => SimpleParser.getMatchesForRegex(text, /(?<=\s)[A-Za-z0-9_-]+/g) private textContainsTags = (text: string): boolean => { const tags = this.getTagsFromText(text) return tags !== undefined && tags.length > 0 } private isInsideComment = (text: string): boolean => { const lastCommentOpen = text.lastIndexOf('') return lastCommentOpen > lastCommentClose } private getTagsFromText = (text: string): string[] | undefined => { const textWithoutComments = text.replace(//g, '') const textWithoutSelfClosing = textWithoutComments.replace(/<[^>]*\/>/g, '') return SimpleParser.getMatchesForRegex(textWithoutSelfClosing, /(?<=<|<\/)[^?\s|/>]+/g) } private getCompletionTypeByTriggerCharacter = ( triggerCharacter: string | undefined, textUntilPosition: string, ): CompletionType => { switch (triggerCharacter) { case '<': return CompletionType.element case ' ': case '\n': return this.getCompletionTypeOnWhitespace(textUntilPosition) case '/': return this.getCompletionTypeOnSlash(textUntilPosition) } return CompletionType.none } private getCompletionTypeOnWhitespace = (textUntilPosition: string): CompletionType => { if (this.isInsideOpenTag(textUntilPosition)) return CompletionType.attribute if (/>\s*$/.test(textUntilPosition)) return CompletionType.snippet return CompletionType.none } private getCompletionTypeOnSlash = (textUntilPosition: string): CompletionType => this.isInsideOpenTag(textUntilPosition) ? CompletionType.none : CompletionType.closingElement private getCompletionTypeByPreviousText = (text: string): CompletionType | undefined => { const lastCharacterBeforePosition = text[text.length - 1] switch (lastCharacterBeforePosition) { case '<': return CompletionType.incompleteElement case ' ': return this.getCompletionTypeAfterWhitespace(text) case '/': return CompletionType.closingElement } } private getCompletionTypeAfterWhitespace = (text: string): CompletionType | undefined => { if (this.isInsideTag(text)) return CompletionType.incompleteAttribute if (this.isAfterTag(text)) return CompletionType.snippet } private isInsideOpenTag = (text: string): boolean => { const lastOpen = text.lastIndexOf('<') const lastClose = text.lastIndexOf('>') if (lastOpen <= lastClose) return false const fromOpen = text.slice(lastOpen) return this.isRegularOpenTag(fromOpen) } private isRegularOpenTag = (fromLastOpen: string): boolean => { const isComment = fromLastOpen.startsWith('