From ea625e14f80f41a9889306274424519fd7e440e4 Mon Sep 17 00:00:00 2001 From: Erik Lieben Date: Sun, 31 Dec 2017 14:32:26 +0100 Subject: [PATCH] fix(auto-complete): suggest with as with.bind --- src/server/CompletionItemFactory.ts | 328 ++-- .../Completions/Library/_elementStructure.ts | 1616 ++++++++--------- 2 files changed, 972 insertions(+), 972 deletions(-) diff --git a/src/server/CompletionItemFactory.ts b/src/server/CompletionItemFactory.ts index 8b32b6ca..663f7e40 100644 --- a/src/server/CompletionItemFactory.ts +++ b/src/server/CompletionItemFactory.ts @@ -1,164 +1,164 @@ -import { autoinject } from 'aurelia-dependency-injection'; -import { CompletionItem, Position } from 'vscode-languageserver-types'; -import AttributeCompletionFactory from './Completions/AttributeCompletionFactory'; -import ElementCompletionFactory from './Completions/ElementCompletionFactory'; -import AttributeValueCompletionFactory from './Completions/AttributeValueCompletionFactory'; -import BindingCompletionFactory from './Completions/BindingCompletionFactory'; -import EmmetCompletionFactory from './Completions/EmmetCompletionFactory'; -import { DocumentParser, TagDefinition, AttributeDefinition } from './DocumentParser'; - -@autoinject() -export default class CompletionItemFactory { - - constructor( - private attributeCompletionFactory: AttributeCompletionFactory, - private elementCompletionFactory: ElementCompletionFactory, - private attributeValueCompletionFactory: AttributeValueCompletionFactory, - private bindingCompletionFactory: BindingCompletionFactory, - private emmetCompletionFactory: EmmetCompletionFactory, - private parser: DocumentParser) { } - - public async create( - triggerCharacter: string, - position: Position, - text: string, - positionNumber: number, - uri: string): Promise> { - - let nodes = await this.parser.parse(text); - let insideTag: TagDefinition = null; - let lastIdx = 0; - - // get insidetag and last index of tag - for(let i = 0; i < nodes.length; i++) { - let node = nodes[i]; - if (!insideTag && positionNumber >= node.startOffset && positionNumber <= node.endOffset) { - insideTag = node; - } - if (node !== insideTag && node.endOffset > positionNumber) { - lastIdx = i; - break; - } - } - - // get open parent tag - let tags = this.getOpenHtmlTags(nodes, lastIdx); - let parentTag = tags[tags.length - 1]; - - // auto complete inside a tag - if (insideTag) { - - let elementString = text.substring(insideTag.startOffset, positionNumber); - if (this.notInAttributeValue(elementString)) { - - if (triggerCharacter === ' ') { - return this.attributeCompletionFactory.create(insideTag.name, insideTag.attributes.map(i => i.name)); - } else if (triggerCharacter === '.' && this.canExpandDot(elementString)) { - return this.createBindingCompletion(insideTag, text, positionNumber); - } else { - return []; - } - - // inside attribute, perform attribute completion - } else if (triggerCharacter === '"' || triggerCharacter === '\'') { - return this.createValueCompletion(insideTag, text, positionNumber); - } else { - return []; - } - } - - // auto complete others - switch (triggerCharacter) { - case '[': - return this.createEmmetCompletion(text, positionNumber); - case '<': - return this.elementCompletionFactory.create(parentTag); - } - } - - private notInAttributeValue(tagText: string) { - let single = 0, double = 0; - for(let char of tagText) { - if (char === '"') double += 1; - if (char === '\'') single += 1; - } - return single % 2 == 0 && double % 2 == 0; - } - - private canExpandDot(elementString) { - return !/([^a-zA-Z]|\.(bind|one-way|two-way|one-time|delegate|trigger|call|capture|ref))\.$/g.test(elementString); - } - - private getOpenHtmlTags(nodes: Array, lastIdx: number) { - let tags: Array = []; - for(let i = 0; i < lastIdx; i++) { - if (nodes[i].startTag) { - tags.push(nodes[i].name); - } else { - var index = tags.indexOf(nodes[i].name); - if (index >= 0) { - tags.splice( index, 1 ); - } - } - } - return tags; - } - - private createValueCompletion(tag: TagDefinition, text: string, position: number) { - let nextCharacter = text.substring(position, position + 1); - if (/['"]/.test(nextCharacter)) { - let attribute; - let elementText = text.substring(tag.startOffset, tag.endOffset); - let tagPosition = position - tag.startOffset; - const attributeRegex = /([\w-]+)\.?\w*\=['"]/g; - let matches; - while (matches = attributeRegex.exec(elementText)) { - if (tagPosition >= matches.index && (tagPosition <= matches.index + matches[0].length)) { - let foundText = matches[1]; - let attributes = tag.attributes.filter(a => a && a.name == foundText); - if (attributes.length) { - attribute = attributes[0]; - break; - } - } - } - if (!attribute) { - return []; - } - return this.attributeValueCompletionFactory.create(tag.name, attribute.name, attribute.binding); - } - } - - private createEmmetCompletion(text: string, position: number) { - const emmetRegex = /^([^<]*?>)*?([\w|-]*)\[$/gm; - let matches = emmetRegex.exec(text.substring(0, position)); - if (!matches) { - return []; - } - let elementName = matches[2]; - return this.emmetCompletionFactory.create(elementName); - } - - private createBindingCompletion(tag: TagDefinition, text: string, position: number) { - let attribute; - let elementText = text.substring(tag.startOffset, tag.endOffset); - let tagPosition = position - tag.startOffset; - const attributeRegex = /([\w\.-]+)(\=['"](.*?)["'])?/g; - let matches; - let foundText = ''; - while (matches = attributeRegex.exec(elementText)) { - if (tagPosition >= matches.index && (tagPosition <= matches.index + matches[1].length)) { - foundText = matches[1]; - let attributes = tag.attributes.filter(a => a.name + (a.binding !== undefined ? '.' : '') == foundText); - if (attributes.length) { - attribute = attributes[0]; - break; - } - } - } - if (!attribute) { - attribute = new AttributeDefinition(foundText.substring(0, foundText.length-1), ''); - } - return this.bindingCompletionFactory.create(tag, attribute, text.substring(position, position + 1)); - } -} +import { autoinject } from 'aurelia-dependency-injection'; +import { CompletionItem, Position } from 'vscode-languageserver-types'; +import AttributeCompletionFactory from './Completions/AttributeCompletionFactory'; +import ElementCompletionFactory from './Completions/ElementCompletionFactory'; +import AttributeValueCompletionFactory from './Completions/AttributeValueCompletionFactory'; +import BindingCompletionFactory from './Completions/BindingCompletionFactory'; +import EmmetCompletionFactory from './Completions/EmmetCompletionFactory'; +import { DocumentParser, TagDefinition, AttributeDefinition } from './DocumentParser'; + +@autoinject() +export default class CompletionItemFactory { + + constructor( + private attributeCompletionFactory: AttributeCompletionFactory, + private elementCompletionFactory: ElementCompletionFactory, + private attributeValueCompletionFactory: AttributeValueCompletionFactory, + private bindingCompletionFactory: BindingCompletionFactory, + private emmetCompletionFactory: EmmetCompletionFactory, + private parser: DocumentParser) { } + + public async create( + triggerCharacter: string, + position: Position, + text: string, + positionNumber: number, + uri: string): Promise> { + + let nodes = await this.parser.parse(text); + let insideTag: TagDefinition = null; + let lastIdx = 0; + + // get insidetag and last index of tag + for(let i = 0; i < nodes.length; i++) { + let node = nodes[i]; + if (!insideTag && positionNumber >= node.startOffset && positionNumber <= node.endOffset) { + insideTag = node; + } + if (node !== insideTag && node.endOffset > positionNumber) { + lastIdx = i; + break; + } + } + + // get open parent tag + let tags = this.getOpenHtmlTags(nodes, lastIdx); + let parentTag = tags[tags.length - 1]; + + // auto complete inside a tag + if (insideTag) { + + let elementString = text.substring(insideTag.startOffset, positionNumber); + if (this.notInAttributeValue(elementString)) { + + if (triggerCharacter === ' ') { + return this.attributeCompletionFactory.create(insideTag.name, insideTag.attributes.map(i => i.name)); + } else if (triggerCharacter === '.' && this.canExpandDot(elementString)) { + return this.createBindingCompletion(insideTag, text, positionNumber); + } else { + return []; + } + + // inside attribute, perform attribute completion + } else if (triggerCharacter === '"' || triggerCharacter === '\'') { + return this.createValueCompletion(insideTag, text, positionNumber); + } else { + return []; + } + } + + // auto complete others + switch (triggerCharacter) { + case '[': + return this.createEmmetCompletion(text, positionNumber); + case '<': + return this.elementCompletionFactory.create(parentTag); + } + } + + private notInAttributeValue(tagText: string) { + let single = 0, double = 0; + for(let char of tagText) { + if (char === '"') double += 1; + if (char === '\'') single += 1; + } + return single % 2 == 0 && double % 2 == 0; + } + + private canExpandDot(elementString) { + return !/([^a-zA-Z]|\.(bind|one-way|two-way|one-time|from-view|to-view|delegate|trigger|call|capture|ref))\.$/g.test(elementString); + } + + private getOpenHtmlTags(nodes: Array, lastIdx: number) { + let tags: Array = []; + for(let i = 0; i < lastIdx; i++) { + if (nodes[i].startTag) { + tags.push(nodes[i].name); + } else { + var index = tags.indexOf(nodes[i].name); + if (index >= 0) { + tags.splice( index, 1 ); + } + } + } + return tags; + } + + private createValueCompletion(tag: TagDefinition, text: string, position: number) { + let nextCharacter = text.substring(position, position + 1); + if (/['"]/.test(nextCharacter)) { + let attribute; + let elementText = text.substring(tag.startOffset, tag.endOffset); + let tagPosition = position - tag.startOffset; + const attributeRegex = /([\w-]+)\.?\w*\=['"]/g; + let matches; + while (matches = attributeRegex.exec(elementText)) { + if (tagPosition >= matches.index && (tagPosition <= matches.index + matches[0].length)) { + let foundText = matches[1]; + let attributes = tag.attributes.filter(a => a && a.name == foundText); + if (attributes.length) { + attribute = attributes[0]; + break; + } + } + } + if (!attribute) { + return []; + } + return this.attributeValueCompletionFactory.create(tag.name, attribute.name, attribute.binding); + } + } + + private createEmmetCompletion(text: string, position: number) { + const emmetRegex = /^([^<]*?>)*?([\w|-]*)\[$/gm; + let matches = emmetRegex.exec(text.substring(0, position)); + if (!matches) { + return []; + } + let elementName = matches[2]; + return this.emmetCompletionFactory.create(elementName); + } + + private createBindingCompletion(tag: TagDefinition, text: string, position: number) { + let attribute; + let elementText = text.substring(tag.startOffset, tag.endOffset); + let tagPosition = position - tag.startOffset; + const attributeRegex = /([\w\.-]+)(\=['"](.*?)["'])?/g; + let matches; + let foundText = ''; + while (matches = attributeRegex.exec(elementText)) { + if (tagPosition >= matches.index && (tagPosition <= matches.index + matches[1].length)) { + foundText = matches[1]; + let attributes = tag.attributes.filter(a => a.name + (a.binding !== undefined ? '.' : '') == foundText); + if (attributes.length) { + attribute = attributes[0]; + break; + } + } + } + if (!attribute) { + attribute = new AttributeDefinition(foundText.substring(0, foundText.length-1), ''); + } + return this.bindingCompletionFactory.create(tag, attribute, text.substring(position, position + 1)); + } +} diff --git a/src/server/Completions/Library/_elementStructure.ts b/src/server/Completions/Library/_elementStructure.ts index 12657392..90902579 100644 --- a/src/server/Completions/Library/_elementStructure.ts +++ b/src/server/Completions/Library/_elementStructure.ts @@ -1,808 +1,808 @@ -export class Value { - constructor(public documentation: string = '') { - - } -} - -class BaseAttribute { - constructor( - public documentation: string, - public url: string, - public customLabel: string = null, - public values: Map = new Map()) { } -} - -export class SimpleAttribute extends BaseAttribute { - constructor( - documentation: string, - url: string = null, - customLabel: string = null, - values: Map = new Map()) { - super(documentation, url, customLabel, values); - } -} - -export class EmptyAttribute extends BaseAttribute { - constructor( - documentation: string, - url: string = null, - customLabel: string = null) { - super(documentation, url, customLabel); - } -} - -export class BindableAttribute extends BaseAttribute { - constructor( - documentation: string, - url: string = null, - public customSnippet: string = null, - public customBindingSnippet: string = null, - customLabel: string = null, - values: Map = new Map()) { - super(documentation, url, customLabel, values); - } -} - -export class Event { - constructor( - public documentation: string, - public url: string = null, - public bubbles: boolean = false, - public cancelable: boolean = false - ) { } -} - -export class GlobalAttributes { - public static attributes: Map = new Map([ - [ - 'accesskey', - new BindableAttribute(`Provides a hint for generating a keyboard shortcut for the current element. This attribute consists of a space-separated list of characters. The browser should use the first one that exists on the computer keyboard layout.`, - 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey') - ], - [ - 'class', - new BindableAttribute(`Is a space-separated list of the classes of the element. Classes allows CSS and JavaScript to select and access specific elements via the class selectors or functions like the method Document.getElementsByClassName().`, - 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class') - ], - [ - 'contenteditable', - new BindableAttribute(`Is an enumerated attribute indicating if the element should be editable by the user. If so, the browser modifies its widget to allow editing.`, - 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable', - null, - null, - null, - new Map([ - ['true', new Value(`or the empty string, which indicates that the element must be editable.`)], - ['fase', new Value(`indicates that the element must not be editable.`)] - ]) - ) - ], - [ - 'contextmenu', - new BindableAttribute(`Is the id of an (https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu) to use as the contextual menu for this element.`, - 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contextmenu') - ], - [ - 'data-*', - new BindableAttribute(`Forms a class of attributes, called custom data attributes, that allow proprietary information to be exchanged between the HTML and its DOM representation that may be used by scripts. All such custom data are available via the HTMLElement interface of the element the attribute is set on. The HTMLElement.dataset property gives access to them.`, - 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*', - 'data-$1="$0"', - 'data-$1.bind="$0"') - ], - ['dir', - new BindableAttribute(`Is an enumerated attribute indicating the directionality of the element's text.`, - 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir', - null, - null, - null, - new Map([ - ['ltr', new Value(`means left to right and is to be used for languages that are written from the left to the right (like English)`)], - ['rtl', new Value(`means right to left and is to be used for languages that are written from the right to the left (like Arabic)`)], - ['auto', new Value(`let the user agent decides. It uses a basic algorithm as it parses the characters inside the element until it finds a character with a strong directionality, then apply that directionality to the whole element.`)], - ]) - )], - [ - 'hidden', - new BindableAttribute(`Is a Boolean attribute indicates that the element is not yet, or is no longer, relevant. For example, it can be used to hide elements of the page that can't be used until the login process has been completed. The browser won't render such elements. This attribute must not be used to hide content that could legitimately be shown.`, - 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden') - ], - [ - 'id', - new BindableAttribute(`Defines a unique identifier (ID) which must be unique in the whole document. Its purpose is to identify the element when linking (using a fragment identifier), scripting, or styling (with CSS).`, - 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id') - ], - [ - 'lang', - new BindableAttribute(`Participates in defining the language of the element, the language that non-editable elements are written in or the language that editable elements should be written in. The tag contains one single entry value in the format defines in the Tags for Identifying Languages (BCP47) IETF document. xml:lang has priority over it.`, - 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang') - ], - - [ - 'slot', - new BindableAttribute(`Assigns a slot in a shadow DOM shadow tree to an element: An element with a slot attribute is assigned to the slot created by the element whose name attribute's value matches that slot attribute's value.`, - 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/slot') - ], - [ - 'style', - new BindableAttribute(`Contains CSS styling declarations to be applied to the element. Note that it is recommended for styles to be defined in a separate file or files. This attribute and the