Skip to content
Permalink
Browse files

feat(smartAutocomplete): Go to Definition (#104)

* chore(package.json): relatedFiles config now array with multiple exts

* chore(package.json): remove obsolete configs better naming

* add(processFiles): where vars and methods are defined + types

* feat(goto): first draft of goto def from view to viweModel

* cleanup(main.ts): remove helper code which helped me get to the first
draft

* add(GetFileExtensionsFromConfig): utility function

* feat(goto): provide goto in aurelia views

* add(HTMLDocumentParser): line number to tags

* add(goto): support for repeat.for

Eg. 'button of buttons' where `button` will be the definition

* wip(smartAutocomplete): Make attributes go-to-able

* wip(smartAutocomplete.viewAttributes):
- remove related file exts function (now simple object)
- improved typings

* fix(goto): cursor now at start of variable
(instead of end of variable's line)

* feat(goto.viewAttribute): Go to custom element attributes

* fix(goto.view): Now correctly goes to repeat.for def.
Eg. goto trigger on ${button}
-> go to line repeat.for="button of buttons"

* rename(ExposeAureliaDefinitions): storeDefinitions
-> storeViewModelDefinitions

* fix(goto.view): method and property values

* feat(goto.view): go to custom element's viewModel from other views

* style(*):

* remove(*): unneeded code
(Used for debugging)

* fix(goto): tag attrs not only restricted to "bind" anymore

* fix(goto): error in vscode output tab

Request textDocument/definition failed.
  Message: Unhandled method textDocument/definition
  Code: -32601
  • Loading branch information
hiaux0 authored and eriklieben committed Nov 5, 2019
1 parent 7ba55d0 commit f49cea91a65d7939cdb5931b844049c4e22bd27c
@@ -0,0 +1,20 @@
import * as Path from 'path';

/**
* Note, this function's logic was taken from `ProcessFiles.ts`
* @param path Path of a file
* @returns file name in kebab case
*
* @example
* const path = '/Users/abc/Desktop/aurelia-example/src/my-text.html'
* const result = getFileNameAsKebabCase(path);
* result // my-text
*
* const path = '/Users/abc/Desktop/aurelia-example/src/MyOtherText.ts'
* const result = getFileNameAsKebabCase(path);
* result // my-other-text
*/
export function getFileNameAsKebabCase(path: string): string {
const name = Path.basename(path).replace(/\.(ts|js|html)$/, '').split(/(?=[A-Z])/).map(s => s.toLowerCase()).join('-');
return name;
}
@@ -0,0 +1,7 @@

export default {
scriptExtensions: [".js", ".ts"],
styleExtensions: [".less", ".sass", ".scss", ".styl", ".css"],
unitExtensions: [".spec.js", ".spec.ts"],
viewExtensions: [".html"],
};
@@ -0,0 +1,8 @@
export class AureliaConfigProperties {
public relatedFiles: {
scriptExtensions: string[],
styleExtensions: string[],
unitExtensions: string[],
viewExtensions: string[],
}
}
@@ -1,14 +1,76 @@
import * as path from 'path';
import { ExtensionContext, OutputChannel, window, languages, SnippetString, commands, TextEdit } from 'vscode';
import {
ExtensionContext,
OutputChannel,
window,
languages,
SnippetString,
commands,
TextEdit,
LocationLink,
Uri,
DefinitionProvider,
TextDocument,
Position,
DefinitionLink,
} from 'vscode';
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient';
import AureliaCliCommands from './aureliaCLICommands';
import { RelatedFiles } from './relatedFiles';
import { registerPreview } from './Preview/Register';
import { TextDocumentContentProvider } from './Preview/TextDocumentContentProvider';
import { WebComponent } from '../server/FileParser/Model/WebComponent';
import { getFileNameAsKebabCase } from '../Util/GetFileNameAsKebabCase';

let outputChannel: OutputChannel;

export function activate(context: ExtensionContext) {
class SearchDefinitionInViewV2 implements DefinitionProvider {
client: LanguageClient;

constructor(client: LanguageClient) {
this.client = client;
}

public async provideDefinition(
document: TextDocument,
position: Position): Promise<DefinitionLink[]> {
const { definitionsInfo, definitionsAttributesInfo } = await this.client.sendRequest('aurelia-definition-provide');

const goToSourceWordRange = document.getWordRangeAtPosition(position);
const goToSourceWord = document.getText(goToSourceWordRange);

const foundDefinitions = definitionsInfo[goToSourceWord];
const getFileName = (path: string): string => {
return path.split('/').pop().replace(/\..+/, '');
}
const currentFileName = getFileName(document.fileName);
const possibleDefs = foundDefinitions.filter(foundDef => {
const isCustomElement = getFileName(foundDef.targetUri) === goToSourceWord;
const isViewModelVariable = getFileName(foundDef.targetUri) === currentFileName; // eg. `.bind="viewModelVariable"`
const isBindingAttribute = definitionsAttributesInfo[goToSourceWord] // eg. `binding-attribute.bind="..."`

return isCustomElement || isViewModelVariable || isBindingAttribute;
});

let targetDef;
if (possibleDefs.length === 1) {
targetDef = possibleDefs[0];
} else {
targetDef = possibleDefs.find(possibleDef => {
const attrInfos = definitionsAttributesInfo[goToSourceWord] // eg. `binding-attribute.bind="..."`
const isBindingAttribute = attrInfos[0].customElementName === getFileName(possibleDef.targetUri);
return isBindingAttribute;
});
}

return {
...targetDef,
targetUri: Uri.parse(targetDef.targetUri)
}
}
}

export function activate(context: ExtensionContext) {
// Create default output channel
outputChannel = window.createOutputChannel('aurelia');
context.subscriptions.push(outputChannel);
@@ -18,12 +80,11 @@ export function activate(context: ExtensionContext) {
context.subscriptions.push(new RelatedFiles());

// Register Code Actions
const edit = (uri: string, documentVersion: number, edits: TextEdit[]) =>
{
const edit = (uri: string, documentVersion: number, edits: TextEdit[]) => {
let textEditor = window.activeTextEditor;
if (textEditor && textEditor.document.uri.toString() === uri) {
textEditor.edit(mutator => {
for(let edit of edits) {
for (let edit of edits) {
mutator.replace(client.protocol2CodeConverter.asRange(edit.range), edit.newText);
}
}).then((success) => {
@@ -56,7 +117,12 @@ export function activate(context: ExtensionContext) {

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


context.subscriptions.push(languages.registerDefinitionProvider(
{ scheme: 'file', language: 'html' },
new SearchDefinitionInViewV2(client))
);

const disposable = client.start();
context.subscriptions.push(disposable);
}
@@ -0,0 +1,41 @@
import {
CompletionItem,
CompletionItemKind,
InsertTextFormat
} from 'vscode-languageserver';
import { autoinject } from 'aurelia-dependency-injection';
import ElementLibrary from './Library/_elementLibrary';
import BaseAttributeCompletionFactory from './BaseAttributeCompletionFactory';
import { includeCodeAutoComplete } from './AttributeValueCompletionFactory';
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()
/**
* Provide viewModel completion
*
* This class would provide `someVar` as a completion item
* @example
* class SomeViewModel {
* public someVar: string
* }
*/
export default class ViewModelViewAttributesCompletionFactory extends BaseAttributeCompletionFactory {
constructor(
library: ElementLibrary,
private application: AureliaApplication,
private settings: AureliaSettings) { super(library); }

public create(uri: string): Array<CompletionItem> {
let result: Array<CompletionItem> = [];

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

return result;
}
}

0 comments on commit f49cea9

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