Skip to content
Permalink
Browse files

feat(autocomplete): custom elements in view (#111)

* feat(autocomplete): Provide viewModel variables inside custom elements
tags

* feat(autocomplete) restrict autocomplete in tags attributes to bindables

* add autocomplete: - snippet insertion
- type into completion documentation,
- and detail header

* feat(autocomplete): List of all your custom elements in view.
Under restriction, that your component has at least
script + view and/or styles

Because, we filter out component by `component.paths.length >= 2`

* feat(autocomplete): use empty string for unsued param
  • Loading branch information
hiaux0 committed Dec 24, 2019
1 parent f49cea9 commit 25b123f50a831d46af87c90cdc5c74d2e00598d3
@@ -1,31 +1,37 @@
import { AureliaApplication } from './FileParser/Model/AureliaApplication';
import { autoinject } from 'aurelia-dependency-injection';
import { CompletionItem, Position } from 'vscode-languageserver';
import AttributeCompletionFactory from './Completions/AttributeCompletionFactory';
import ElementCompletionFactory from './Completions/ElementCompletionFactory';
import CustomElementCompletionFactory from './Completions/CustomElementCompletionFactory';
import AttributeValueCompletionFactory from './Completions/AttributeValueCompletionFactory';
import BindingCompletionFactory from './Completions/BindingCompletionFactory';
import EmmetCompletionFactory from './Completions/EmmetCompletionFactory';
import { HTMLDocumentParser, TagDefinition, AttributeDefinition } from './FileParser/HTMLDocumentParser';
import ViewModelVariablesCompletionFactory from './Completions/ViewModelVariablesCompletionFactory';
import ViewModelViewAttributesCompletionFactory from './Completions/ViewModelViewAttributesCompletionFactory';

@autoinject()
export default class CompletionItemFactory {

constructor(
private attributeCompletionFactory: AttributeCompletionFactory,
private elementCompletionFactory: ElementCompletionFactory,
private customElementCompletionFactory: CustomElementCompletionFactory,
private attributeValueCompletionFactory: AttributeValueCompletionFactory,
private bindingCompletionFactory: BindingCompletionFactory,
private emmetCompletionFactory: EmmetCompletionFactory,
private viewModelVariablesCompletionFactory: ViewModelVariablesCompletionFactory,
private viewModelViewAttributesCompletionFactory: ViewModelViewAttributesCompletionFactory,
private parser: HTMLDocumentParser) { }

public async create(
triggerCharacter: string,
position: Position,
text: string,
positionNumber: number,
uri: string): Promise<Array<CompletionItem>> {
uri: string,
aureliaApplication: AureliaApplication): Promise<Array<CompletionItem>> {

let nodes = await this.parser.parse(text);
let insideTag: TagDefinition = null;
@@ -54,7 +60,7 @@ export default class CompletionItemFactory {
if (this.notInAttributeValue(elementString)) {

if (triggerCharacter === ' ') {
return this.attributeCompletionFactory.create(insideTag.name, insideTag.attributes.map(i => i.name));
return this.attributeCompletionFactory.create(insideTag.name, insideTag.attributes.map(i => i.name), aureliaApplication);
} else if (triggerCharacter === '.' && this.canExpandDot(elementString)) {
return this.createBindingCompletion(insideTag, text, positionNumber);
} else {
@@ -75,7 +81,9 @@ export default class CompletionItemFactory {
case '[':
return this.createEmmetCompletion(text, positionNumber);
case '<':
return this.elementCompletionFactory.create(parentTag);
const customElementResult = this.customElementCompletionFactory.create('', aureliaApplication);
const elementResult = this.elementCompletionFactory.create(parentTag);
return customElementResult.concat(elementResult);
}
}

@@ -1,40 +1,75 @@
import {
CompletionItem,
CompletionItemKind,
InsertTextFormat,
MarkedString } from 'vscode-languageserver';
import { autoinject } from 'aurelia-dependency-injection';
import ElementLibrary from './Library/_elementLibrary';
import { GlobalAttributes } from './Library/_elementStructure';
import BaseAttributeCompletionFactory from './BaseAttributeCompletionFactory';
import AureliaSettings from './../AureliaSettings';

@autoinject()
export default class AureliaAttributeCompletionFactory extends BaseAttributeCompletionFactory {

constructor(library: ElementLibrary, private settings: AureliaSettings) { super(library); }

public create(elementName: string, existingAttributes: Array<string>): Array<CompletionItem> {

let result:Array<CompletionItem> = [];
let element = this.getElement(elementName);

if (element.hasGlobalAttributes) {
this.addAttributes(GlobalAttributes.attributes, result, existingAttributes, this.settings.quote);
}

if (element.attributes) {
this.addAttributes(element.attributes, result, existingAttributes, this.settings.quote);
}

if (element.hasGlobalEvents) {
this.addEvents(GlobalAttributes.events, result, existingAttributes, this.settings.quote);
}

if (element.events) {
this.addEvents(element.events, result, existingAttributes, this.settings.quote);
}

return result;
}
}
import { AureliaApplication } from './../FileParser/Model/AureliaApplication';
import {
CompletionItem,
CompletionItemKind,
InsertTextFormat,
MarkedString } from 'vscode-languageserver';
import { autoinject } from 'aurelia-dependency-injection';
import ElementLibrary from './Library/_elementLibrary';
import { GlobalAttributes } from './Library/_elementStructure';
import BaseAttributeCompletionFactory from './BaseAttributeCompletionFactory';
import AureliaSettings from './../AureliaSettings';

@autoinject()
export default class AureliaAttributeCompletionFactory extends BaseAttributeCompletionFactory {

constructor(library: ElementLibrary, private settings: AureliaSettings) { super(library); }

public create(elementName: string, existingAttributes: Array<string>, aureliaApplication: AureliaApplication): Array<CompletionItem> {

let result:Array<CompletionItem> = [];
let element = this.getElement(elementName);

this.addViewModelBindables(result, elementName, aureliaApplication);

if (element.hasGlobalAttributes) {
this.addAttributes(GlobalAttributes.attributes, result, existingAttributes, this.settings.quote);
}

if (element.attributes) {
this.addAttributes(element.attributes, result, existingAttributes, this.settings.quote);
}

if (element.hasGlobalEvents) {
this.addEvents(GlobalAttributes.events, result, existingAttributes, this.settings.quote);
}

if (element.events) {
this.addEvents(element.events, result, existingAttributes, this.settings.quote);
}

return result;
}

/**
* Look at the View Model and provide all class variables as completion in kebab case.
* TODO: Only bindables should be provided.
* @param result
* @param elementName
* @param aureliaApplication
*/
private addViewModelBindables(result: Array<CompletionItem>, elementName: string, aureliaApplication: AureliaApplication) {
if (aureliaApplication.components) {
aureliaApplication.components.forEach(component => {
if (component.name !== elementName) return;
if (!component.viewModel) return;
if (!component.viewModel.properties) return;

component.viewModel.properties.forEach(property => {
if (!property.isBindable) return;

const quote = this.settings.quote;
const varAsKebabCase = property.name.split(/(?=[A-Z])/).map(s => s.toLowerCase()).join('-');
result.push({
documentation: property.type,
detail: 'View Model Bindable',
insertText: `${varAsKebabCase}.$\{1:bind\}=${quote}$\{0:${property.name}\}${quote}`,
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Variable,
label: `View Model: ${varAsKebabCase}`
});
});
})
}
}
}
@@ -0,0 +1,37 @@
import {
CompletionItem,
CompletionItemKind,
InsertTextFormat, MarkedString
} from 'vscode-languageserver';
import { autoinject } from 'aurelia-dependency-injection';
import { AureliaApplication } from './../FileParser/Model/AureliaApplication';
import ElementLibrary from './Library/_elementLibrary';

/**
* Get all the Custom elements from (based on aureliaApplication), and provide completion
*/
@autoinject()
export default class CustomElementCompletionFactory {

constructor(private library: ElementLibrary) { }

public create(parent: string, aureliaApplication: AureliaApplication): Array<CompletionItem> {

let result: Array<CompletionItem> = [];

// Assumption: Every custom element has a script and a template
const customElements = aureliaApplication.components.filter(component => component.paths.length >= 2);
customElements.forEach(element => {
result.push({
documentation: MarkedString.fromPlainText(`${element.name}`).toString(),
detail: `Custom Element`,
insertText: `${element.name}$2>$1</${element.name}>$0`,
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Property,
label: `Custom Element: ${element.name}`,
});
});

return result;
}
}
@@ -13,6 +13,7 @@ export declare type Methods = Method[];
type Property = {
name: string;
modifiers: string[];
isBindable: boolean;
type: string | undefined;
range: Range // Position[]
};
@@ -168,9 +168,15 @@ function processClassDeclaration(node: Node) {
startPos = property.name.pos - lineStart + 1;
endPos = srcFile.getLineEndOfPosition(lineNumber);

let isBindable = false;
if (member.decorators) {
isBindable = member.decorators[0].getText() === '@bindable';
}

properties.push({
name: propertyName,
modifiers: propertyModifiers,
isBindable,
type: propertyType,
range: Range.create(
Position.create(lineNumber, startPos),
@@ -112,7 +112,7 @@ connection.onCompletion(async (textDocumentPosition) => {
let offset = document.offsetAt(textDocumentPosition.position);
let triggerCharacter = text.substring(offset - 1, offset);
let position = textDocumentPosition.position;
return CompletionList.create(await completionItemFactory.create(triggerCharacter, position, text, offset, textDocumentPosition.textDocument.uri), false);
return CompletionList.create(await completionItemFactory.create(triggerCharacter, position, text, offset, textDocumentPosition.textDocument.uri, aureliaApplication), false);
});


0 comments on commit 25b123f

Please sign in to comment.
You can’t perform that action at this time.