Skip to content

Commit

Permalink
feat(auto-complete): experimental auto complete from view model (behi…
Browse files Browse the repository at this point in the history
…nd feature toggle: smartAutocomplete)

experimental feature: smart auto complete (behind feature toggle "smartAutocomplete")

Basic implementation to enable autocomplete for items from the view model.
This is a very limited/ simple version, currently, it only works on bindings and hints simple properties, doesn't work on string interpolation bindings.
  • Loading branch information
Erik Lieben authored Jan 1, 2018
1 parent ea625e1 commit 7312b03
Show file tree
Hide file tree
Showing 24 changed files with 986 additions and 320 deletions.
526 changes: 274 additions & 252 deletions package.json

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions src/client/Preview/Register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as vscode from 'vscode';
import {TextDocumentContentProvider} from './TextDocumentContentProvider';

export function registerPreview(context, window, client) {

let previewUri = vscode.Uri.parse('aurelia-preview://authority/aurelia-preview');

let provider = new TextDocumentContentProvider(client);
let registration = vscode.workspace.registerTextDocumentContentProvider('aurelia-preview', provider);

vscode.workspace.onDidChangeTextDocument((e: vscode.TextDocumentChangeEvent) => {
if (e.document === vscode.window.activeTextEditor.document) {
provider.update(previewUri);
}
});

vscode.window.onDidChangeTextEditorSelection((e: vscode.TextEditorSelectionChangeEvent) => {
if (e.textEditor === vscode.window.activeTextEditor) {
provider.update(previewUri);
}
});

context.subscriptions.push(vscode.commands.registerCommand('aurelia.showViewProperties', () => {

const smartAutocomplete = vscode.workspace.getConfiguration().get('aurelia.featureToggles.smartAutocomplete');
if (smartAutocomplete) {
return vscode.commands.executeCommand('vscode.previewHtml', previewUri, vscode.ViewColumn.Two, 'Aurelia view data')
.then(
(success) => {
},
(reason) => {
window.showErrorMessage(reason);
});
} else {
return vscode.window.showWarningMessage('This command requires the experimental feature "smartAutocomplete" to be enabled');
}


}));

}

121 changes: 121 additions & 0 deletions src/client/Preview/TextDocumentContentProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import * as vscode from 'vscode';
import { LanguageClient } from 'vscode-languageclient';
import { WebComponent } from './../../server/FileParser/Model/WebComponent';

export class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {

constructor(private client: LanguageClient) {

}

private _onDidChange = new vscode.EventEmitter<vscode.Uri>();

public async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
let editor = vscode.window.activeTextEditor;
if (!vscode.window.activeTextEditor.document) {
return Promise.resolve('<p>no data</p>');
}
let fileName = vscode.window.activeTextEditor.document.fileName;
let component = <WebComponent> await this.client.sendRequest('aurelia-view-information', fileName);

let headerHTML = `<h1>Component: '${component.name}'</h1>`;
headerHTML += '<h2>Files</h2><ul>';
for (let path of component.paths) {
headerHTML += `<li>${path}</li>`;
}
headerHTML += '</ul>';

let viewModelHTML = `<h2>no viewmodel found</h2>`;
if (component.viewModel) {
viewModelHTML = `<h2>ViewModel</h2>`;
viewModelHTML += `<p>type: ${component.viewModel.type}</p>`;
viewModelHTML += `<h3>Properties</h3>`;
viewModelHTML += '<ul>';
for (let prop of component.viewModel.properties) {
viewModelHTML += `<li>${prop.name} (${prop.type})</li>`;
}
viewModelHTML += '</ul>';
viewModelHTML += `<h3>Methods</h3>`;
viewModelHTML += '<ul>';
for (let prop of component.viewModel.methods) {
viewModelHTML += `<li>${prop.name} (${prop.returnType}) => (params: ${prop.parameters.join(',')})</li>`;
}
viewModelHTML += '</ul>';
}

let viewHTML = `<h2>No view found</h2>`;
if (component.document) {
viewHTML = `<h2>View</h2>`;


if (component.document.references && component.document.references.length) {
viewHTML += '<h3>Require/ references in template</h3>';
viewHTML += `<ul>`;
for(let reference of component.document.references) {
viewHTML += `<li>path: ${reference.path}</li>`;
viewHTML += `<li>as: ${reference.as}</li>`;
}
viewHTML += '</ul>';
}

if (component.document.bindables && component.document.bindables.length) {
viewHTML += '<h3>bindable property on template</h3><ul>';
for (let bindable of component.document.bindables) {
viewHTML += `<li>${bindable}</li>`;
}
viewHTML += '</ul>';
}

if (component.document.dynamicBindables && component.document.dynamicBindables.length) {
viewHTML += '<h3>Attributes with bindings found in template</h3>';
for(let bindable of component.document.dynamicBindables) {
viewHTML += `
<ul>
<li>attribute name : <code>${bindable.name}</code></li>
<li>attribute value : <code>${bindable.value}</code></li>
<li>binding type: <code>${bindable.bindingType}</code></li>
</ul>`;
viewHTML += `<pre><code>${JSON.stringify(bindable.bindingData, null, 2) }</code></pre>`;
}
}

if (component.document.interpolationBindings && component.document.interpolationBindings.length) {
viewHTML += '<h2>String interpolation found in template</h2>';
for(let bindable of component.document.interpolationBindings) {
viewHTML += `
<code>${bindable.value}</code>`;
viewHTML += `<pre><code>${JSON.stringify(bindable.bindingData, null, 2) }</code></pre>`;
}
}
}

let classesHTML = '<h2>no extra classes found</h2>';
if (component.classes) {
classesHTML = '<h2>exports to this view</h2>';
for(let cl of component.classes) {
classesHTML += `<li>${cl.name}</li>`;
}
classesHTML += '</ul>';

}

return `<body><style>pre { border: 1px solid #333; display: block; background: #1a1a1a; margin: 1rem;color: #999; }</style>
${headerHTML}
<hr>
${viewModelHTML}
<hr>
${viewHTML}
<hr>
${classesHTML}
<br><br>
</body>`;
}

get onDidChange(): vscode.Event<vscode.Uri> {
return this._onDidChange.event;
}

public update(uri: vscode.Uri) {
this._onDidChange.fire(uri);
}
}
3 changes: 3 additions & 0 deletions src/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ExtensionContext, OutputChannel, window, languages, SnippetString, comm
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient';
import AureliaCliCommands from './aureliaCLICommands';
import { RelatedFiles } from './relatedFiles';
import { registerPreview } from './Preview/Register';

let outputChannel: OutputChannel;

Expand Down Expand Up @@ -54,6 +55,8 @@ export function activate(context: ExtensionContext) {
};

const client = new LanguageClient('html', 'Aurelia', serverOptions, clientOptions);
registerPreview(context, window, client);

const disposable = client.start();
context.subscriptions.push(disposable);
}
4 changes: 4 additions & 0 deletions src/server/AureliaSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ export default class AureliaSettings {
public bindings = {
data : []
}

public featureToggles = {
smartAutocomplete : true
}
}
4 changes: 2 additions & 2 deletions src/server/CodeActions/HtmlInvalidCaseCodeAction.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Diagnostic, TextEdit, Command, TextDocument } from "vscode-languageserver-types";
import { DocumentParser } from "../DocumentParser";
import { HTMLDocumentParser } from "./../FileParser/HTMLDocumentParser";
import { attributeInvalidCaseFix } from "../Common/AttributeInvalidCaseFix";

export class HtmlInvalidCaseCodeAction {
Expand All @@ -9,7 +9,7 @@ export class HtmlInvalidCaseCodeAction {
const text = document.getText();
const start = document.offsetAt(diagnostic.range.start);
const end = document.offsetAt(diagnostic.range.end);
const parser = new DocumentParser();
const parser = new HTMLDocumentParser();
const element = await parser.getElementAtPosition(text, start, end);

const original = text.substring(start, end);
Expand Down
2 changes: 1 addition & 1 deletion src/server/Common/AttributeInvalidCaseFix.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AttributeMap } from 'aurelia-templating-binding';
import {AttributeDefinition, TagDefinition } from './../../server/DocumentParser';
import {AttributeDefinition, TagDefinition } from './../FileParser/HTMLDocumentParser';

export function attributeInvalidCaseFix(name: string, elementName: string) {

Expand Down
10 changes: 5 additions & 5 deletions src/server/CompletionItemFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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';
import { HTMLDocumentParser, TagDefinition, AttributeDefinition } from './FileParser/HTMLDocumentParser';

@autoinject()
export default class CompletionItemFactory {
Expand All @@ -16,7 +16,7 @@ export default class CompletionItemFactory {
private attributeValueCompletionFactory: AttributeValueCompletionFactory,
private bindingCompletionFactory: BindingCompletionFactory,
private emmetCompletionFactory: EmmetCompletionFactory,
private parser: DocumentParser) { }
private parser: HTMLDocumentParser) { }

public async create(
triggerCharacter: string,
Expand Down Expand Up @@ -61,7 +61,7 @@ export default class CompletionItemFactory {

// inside attribute, perform attribute completion
} else if (triggerCharacter === '"' || triggerCharacter === '\'') {
return this.createValueCompletion(insideTag, text, positionNumber);
return this.createValueCompletion(insideTag, text, positionNumber, uri);
} else {
return [];
}
Expand Down Expand Up @@ -104,7 +104,7 @@ export default class CompletionItemFactory {
return tags;
}

private createValueCompletion(tag: TagDefinition, text: string, position: number) {
private createValueCompletion(tag: TagDefinition, text: string, position: number, uri: string) {
let nextCharacter = text.substring(position, position + 1);
if (/['"]/.test(nextCharacter)) {
let attribute;
Expand All @@ -125,7 +125,7 @@ export default class CompletionItemFactory {
if (!attribute) {
return [];
}
return this.attributeValueCompletionFactory.create(tag.name, attribute.name, attribute.binding);
return this.attributeValueCompletionFactory.create(tag.name, attribute.name, attribute.binding, uri);
}
}

Expand Down
139 changes: 97 additions & 42 deletions src/server/Completions/AttributeValueCompletionFactory.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,97 @@
import {
CompletionItem,
CompletionItemKind,
InsertTextFormat } from 'vscode-languageserver-types';
import { autoinject } from 'aurelia-dependency-injection';
import ElementLibrary from './Library/_elementLibrary';
import { GlobalAttributes } from './Library/_elementStructure';
import BaseAttributeCompletionFactory from './BaseAttributeCompletionFactory';

@autoinject()
export default class AttributeCompletionFactory extends BaseAttributeCompletionFactory {

constructor(library: ElementLibrary) { super(library); }

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

let result:Array<CompletionItem> = [];

if (bindingName === undefined || bindingName === null || bindingName === '') {
let element = this.getElement(elementName);

let attribute = element.attributes.get(attributeName);
if (!attribute) {
attribute = GlobalAttributes.attributes.get(attributeName);
}

if (attribute && attribute.values) {
for (let [key, value] of attribute.values.entries()) {
result.push({
documentation: value.documentation,
insertText: key,
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Property,
label: key,
});
}
}
}

return result;
}
}
import {
CompletionItem,
CompletionItemKind,
InsertTextFormat } from 'vscode-languageserver-types';
import { autoinject } from 'aurelia-dependency-injection';
import ElementLibrary from './Library/_elementLibrary';
import { GlobalAttributes } from './Library/_elementStructure';
import BaseAttributeCompletionFactory from './BaseAttributeCompletionFactory';
import {AureliaApplication} from './../FileParser/Model/AureliaApplication';
import AureliaSettings from '../AureliaSettings';
import { settings } from 'cluster';
import { fileUriToPath } from './../Util/FileUriToPath';
import { normalizePath } from './../Util/NormalizePath';

@autoinject()
export default class AttributeCompletionFactory extends BaseAttributeCompletionFactory {

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

public create(elementName: string, attributeName: string, bindingName: string, uri: string): Array<CompletionItem> {

let result:Array<CompletionItem> = [];

if (bindingName === undefined || bindingName === null || bindingName === '') {
let element = this.getElement(elementName);

let attribute = element.attributes.get(attributeName);
if (!attribute) {
attribute = GlobalAttributes.attributes.get(attributeName);
}

if (attribute && attribute.values) {
for (let [key, value] of attribute.values.entries()) {
result.push({
documentation: value.documentation,
insertText: key,
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Property,
label: key,
});
}
}
}

if (this.settings.featureToggles.smartAutocomplete) {
includeCodeAutoComplete(this.application, result, normalizePath(fileUriToPath(uri)));
}

return result;
}
}

function includeCodeAutoComplete(application, result, path) {
path = path.toLowerCase();
const compoment = application.components.find(i => i.paths.map(x => x.toLowerCase()).indexOf(path) > -1);

if (compoment) {
if (compoment.viewModel) {
compoment.viewModel.methods.forEach(x => {

let inner = '';
for(let i=0; i < x.parameters.length;i++) {
inner += `\$${i+1},`;
}
if (x.parameters.length) {
inner = inner.substring(0, inner.length-1);
}

result.push({
documentation: x.name,
insertText: `${x.name}(${inner})$0`,
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Method,
label: x.name,
});
});

compoment.viewModel.properties.forEach(x => {
let documentation = x.name;
if (x.type) {
documentation += ` (${x.type})`;
}

result.push({
documentation: documentation,
insertText: x.name,
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Property,
label: x.name,
})
});
}
}
}
Loading

0 comments on commit 7312b03

Please sign in to comment.