From eee039c90d16772912eb04c42f6d5b131daaa7e6 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 3 Nov 2016 14:14:24 -0400 Subject: [PATCH] feat(diagnostics): adds invalid casing diagnostic (#16) --- dist/src/client/main.js | 19 +-- dist/src/client/main.js.map | 2 +- .../aureliaLanguageService.js | 5 +- .../aureliaLanguageService.js.map | 2 +- .../services/htmlValidation.js | 90 ++++++++++++ .../services/htmlValidation.js.map | 1 + dist/src/server/main.js | 15 +- dist/src/server/main.js.map | 2 +- package.json | 2 +- src/client/main.ts | 43 +++--- src/server/.vscode/launch.json | 14 ++ src/server/.vscode/settings.json | 4 + .../aureliaLanguageService.ts | 21 ++- .../services/htmlValidation.ts | 129 ++++++++++++++++++ src/server/main.ts | 8 +- 15 files changed, 317 insertions(+), 40 deletions(-) create mode 100644 dist/src/server/aurelia-languageservice/services/htmlValidation.js create mode 100644 dist/src/server/aurelia-languageservice/services/htmlValidation.js.map create mode 100644 src/server/.vscode/launch.json create mode 100644 src/server/.vscode/settings.json create mode 100644 src/server/aurelia-languageservice/services/htmlValidation.ts diff --git a/dist/src/client/main.js b/dist/src/client/main.js index 6e3810d8..82653b40 100644 --- a/dist/src/client/main.js +++ b/dist/src/client/main.js @@ -1,5 +1,5 @@ "use strict"; -const path = require("path"); +const path = require('path'); const vscode_1 = require('vscode'); const vscode_languageclient_1 = require('vscode-languageclient'); const aureliaCLICommands_1 = require('./aureliaCLICommands'); @@ -11,21 +11,22 @@ function activate(context) { // Register CLI commands context.subscriptions.push(aureliaCLICommands_1.default.registerCommands(outputChannel)); // Register Aurelia language server - let serverModule = context.asAbsolutePath(path.join('dist', 'src', 'server', 'main.js')); - let debugOptions = { execArgv: ['--nolazy', '--debug=6004'] }; - let serverOptions = { + const serverModule = context.asAbsolutePath(path.join('dist', 'src', 'server', 'main.js')); + const debugOptions = { execArgv: ['--nolazy', '--debug=6004'] }; + const serverOptions = { run: { module: serverModule, transport: vscode_languageclient_1.TransportKind.ipc }, debug: { module: serverModule, transport: vscode_languageclient_1.TransportKind.ipc, options: debugOptions } }; - let clientOptions = { + const clientOptions = { + diagnosticCollectionName: 'Aurelia', documentSelector: ['html'], + initializationOptions: {}, synchronize: { configurationSection: ['aurelia'], - }, - initializationOptions: {} + } }; - let client = new vscode_languageclient_1.LanguageClient('html', 'Aurelia', serverOptions, clientOptions); - let disposable = client.start(); + const client = new vscode_languageclient_1.LanguageClient('html', 'Aurelia', serverOptions, clientOptions); + const disposable = client.start(); context.subscriptions.push(disposable); } exports.activate = activate; diff --git a/dist/src/client/main.js.map b/dist/src/client/main.js.map index 366659c9..4118e122 100644 --- a/dist/src/client/main.js.map +++ b/dist/src/client/main.js.map @@ -1 +1 @@ -{"version":3,"file":"main.js","sourceRoot":"","sources":["../../../src/client/main.ts"],"names":[],"mappings":";AAAA,MAAY,IAAI,WAAM,MAAM,CAAC,CAAA;AAC7B,yBAAmE,QAAQ,CAAC,CAAA;AAC5E,wCAAoF,uBAAuB,CAAC,CAAA;AAC5G,qCAA+B,sBAAsB,CAAC,CAAA;AAEtD,IAAI,aAA4B,CAAC;AAEjC,kBAAyB,OAAyB;IAEhD,gCAAgC;IAChC,aAAa,GAAG,eAAM,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;IACtD,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAE1C,wBAAwB;IACxB,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,4BAAkB,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC,CAAC;IAE/E,mCAAmC;IACnC,IAAI,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;IACzF,IAAI,YAAY,GAAG,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC,EAAE,CAAC;IAC9D,IAAI,aAAa,GAAkB;QACnC,GAAG,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,qCAAa,CAAC,GAAG,EAAE;QAC3D,KAAK,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,qCAAa,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE;KACpF,CAAC;IACD,IAAI,aAAa,GAA0B;QACzC,gBAAgB,EAAE,CAAC,MAAM,CAAC;QAC1B,WAAW,EAAE;YACX,oBAAoB,EAAE,CAAC,SAAS,CAAC;SAClC;QACD,qBAAqB,EAAE,EAAE;KAC1B,CAAC;IACF,IAAI,MAAM,GAAG,IAAI,sCAAc,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,CAAC,CAAC;IAClF,IAAI,UAAU,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;IAC/B,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AACzC,CAAC;AA1Be,gBAAQ,WA0BvB,CAAA"} \ No newline at end of file +{"version":3,"file":"main.js","sourceRoot":"","sources":["../../../src/client/main.ts"],"names":[],"mappings":";AAAA,MAAY,IAAI,WAAM,MAAM,CAAC,CAAA;AAC7B,yBAAmE,QAAQ,CAAC,CAAA;AAC5E,wCAAoF,uBAAuB,CAAC,CAAA;AAC5G,qCAA+B,sBAAsB,CAAC,CAAA;AAEtD,IAAI,aAA4B,CAAC;AAEjC,kBAAyB,OAAyB;IAEjD,gCAAgC;IAChC,aAAa,GAAG,eAAM,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;IACtD,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAE1C,wBAAwB;IACxB,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,4BAAkB,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC,CAAC;IAE/E,mCAAmC;IACnC,MAAM,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;IAC3F,MAAM,YAAY,GAAG,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC,EAAE,CAAC;IAChE,MAAM,aAAa,GAAkB;QACpC,GAAG,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,qCAAa,CAAC,GAAG,EAAE;QAC3D,KAAK,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,qCAAa,CAAC,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE;KACpF,CAAC;IAEF,MAAM,aAAa,GAA0B;QAC5C,wBAAwB,EAAE,SAAS;QACnC,gBAAgB,EAAE,CAAC,MAAM,CAAC;QAC1B,qBAAqB,EAAE,EAAE;QACzB,WAAW,EAAE;YACZ,oBAAoB,EAAE,CAAC,SAAS,CAAC;SACjC;KACD,CAAC;IAEF,MAAM,MAAM,GAAG,IAAI,sCAAc,CAAC,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,CAAC,CAAC;IACnF,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;IAClC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AACxC,CAAC;AA7Be,gBAAQ,WA6BvB,CAAA"} \ No newline at end of file diff --git a/dist/src/server/aurelia-languageservice/aureliaLanguageService.js b/dist/src/server/aurelia-languageservice/aureliaLanguageService.js index 498a6f9a..3a413f46 100644 --- a/dist/src/server/aurelia-languageservice/aureliaLanguageService.js +++ b/dist/src/server/aurelia-languageservice/aureliaLanguageService.js @@ -1,10 +1,13 @@ "use strict"; const htmlParser_1 = require('./parser/htmlParser'); const htmlCompletion_1 = require('./services/htmlCompletion'); +const htmlValidation_1 = require('./services/htmlValidation'); function getLanguageService() { + const validation = new htmlValidation_1.HTMLValidation(); return { doComplete: htmlCompletion_1.doComplete, - parseHTMLDocument: document => htmlParser_1.parse(document.getText()), + doValidation: validation.doValidation.bind(validation), + parseHTMLDocument: document => htmlParser_1.parse(document.getText()) }; } exports.getLanguageService = getLanguageService; diff --git a/dist/src/server/aurelia-languageservice/aureliaLanguageService.js.map b/dist/src/server/aurelia-languageservice/aureliaLanguageService.js.map index c75b3504..0dd39f3e 100644 --- a/dist/src/server/aurelia-languageservice/aureliaLanguageService.js.map +++ b/dist/src/server/aurelia-languageservice/aureliaLanguageService.js.map @@ -1 +1 @@ -{"version":3,"file":"aureliaLanguageService.js","sourceRoot":"","sources":["../../../../src/server/aurelia-languageservice/aureliaLanguageService.ts"],"names":[],"mappings":";AAAA,6BAAsB,qBAAqB,CAAC,CAAA;AAC5C,iCAA2B,2BAA2B,CAAC,CAAA;AAUvD;IACC,MAAM,CAAC;QACN,uCAAU;QACR,iBAAiB,EAAE,QAAQ,IAAI,kBAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;KAC1D,CAAC;AACH,CAAC;AALe,0BAAkB,qBAKjC,CAAA"} \ No newline at end of file +{"version":3,"file":"aureliaLanguageService.js","sourceRoot":"","sources":["../../../../src/server/aurelia-languageservice/aureliaLanguageService.ts"],"names":[],"mappings":";AAAA,6BAAsB,qBAAqB,CAAC,CAAA;AAC5C,iCAA2B,2BAA2B,CAAC,CAAA;AACvD,iCAA+B,2BAA2B,CAAC,CAAA;AAoB3D;IACC,MAAM,UAAU,GAAG,IAAI,+BAAc,EAAE,CAAC;IACxC,MAAM,CAAC;QACN,uCAAU;QACV,YAAY,EAAE,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC;QACtD,iBAAiB,EAAE,QAAQ,IAAI,kBAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;KACxD,CAAC;AACH,CAAC;AAPe,0BAAkB,qBAOjC,CAAA"} \ No newline at end of file diff --git a/dist/src/server/aurelia-languageservice/services/htmlValidation.js b/dist/src/server/aurelia-languageservice/services/htmlValidation.js new file mode 100644 index 00000000..f12ba475 --- /dev/null +++ b/dist/src/server/aurelia-languageservice/services/htmlValidation.js @@ -0,0 +1,90 @@ +'use strict'; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator.throw(value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments)).next()); + }); +}; +const vscode_languageserver_types_1 = require('vscode-languageserver-types'); +const htmlScanner_1 = require('../parser/htmlScanner'); +exports.DiagnosticCodes = { + InvalidCasing: 'invalid-casing', + InvalidMethod: 'invalid-method' +}; +exports.DiagnosticSource = 'Aurelia'; +const kebabCaseValidationRegex = /(.*)\.(bind|one-way|two-way|one-time|call|delegate|trigger)/; +const methodRegex = /\"(.*)\(/; +function kebabToCamel(s) { + return s.replace(/(\-\w)/g, m => m[1].toUpperCase()); +} +function camelToKebab(s) { + return s.replace(/\.?([A-Z])/g, (x, y) => "-" + y.toLowerCase()).replace(/^-/, ""); +} +class HTMLValidation { + constructor() { + this.validationEnabled = true; + } + configure(raw) { + if (raw) { + this.validationEnabled = raw.validate; + } + } + doValidation(document, htmlDocument) { + return __awaiter(this, void 0, void 0, function* () { + if (!this.validationEnabled) + return Promise.resolve([]); + const text = document.getText(); + const scanner = htmlScanner_1.createScanner(text, htmlDocument.roots[0].start); + const diagnostics = []; + let attr; + let token = scanner.scan(); + while (token !== htmlScanner_1.TokenType.EOS) { + switch (token) { + case htmlScanner_1.TokenType.AttributeName: + attr = { + name: scanner.getTokenText(), + start: scanner.getTokenOffset(), + length: scanner.getTokenLength() + }; + break; + case htmlScanner_1.TokenType.AttributeValue: + attr.value = scanner.getTokenText(); + yield this._validateAttribute(attr, document, diagnostics); + break; + } + token = scanner.scan(); + } + return Promise.resolve(diagnostics); + }); + } + _validateAttribute(attr, document, diagnostics) { + return __awaiter(this, void 0, void 0, function* () { + let match = kebabCaseValidationRegex.exec(attr.name); + if (match && match.length) { + const prop = match[1]; + const op = match[2]; + if (prop !== prop.toLowerCase()) { + diagnostics.push(this.toDiagnostic(attr, document, `'${attr.name}' has invalid casing; it should likely be '${camelToKebab(prop)}.${op}'`, exports.DiagnosticCodes.InvalidCasing)); + } + } + return Promise.resolve(); + }); + } + toDiagnostic(attr, document, message, code = undefined, serverity = 1 /* Error */) { + const range = vscode_languageserver_types_1.Range.create(document.positionAt(attr.start), document.positionAt(attr.start + attr.length)); + const diagnostic = { + message: message, + range: range, + severity: serverity, + source: exports.DiagnosticSource + }; + if (code !== undefined) { + diagnostic.code = code; + } + return diagnostic; + } +} +exports.HTMLValidation = HTMLValidation; +//# sourceMappingURL=htmlValidation.js.map \ No newline at end of file diff --git a/dist/src/server/aurelia-languageservice/services/htmlValidation.js.map b/dist/src/server/aurelia-languageservice/services/htmlValidation.js.map new file mode 100644 index 00000000..79661efe --- /dev/null +++ b/dist/src/server/aurelia-languageservice/services/htmlValidation.js.map @@ -0,0 +1 @@ +{"version":3,"file":"htmlValidation.js","sourceRoot":"","sources":["../../../../../src/server/aurelia-languageservice/services/htmlValidation.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/src/server/main.js b/dist/src/server/main.js index 954502bc..9bfa4253 100644 --- a/dist/src/server/main.js +++ b/dist/src/server/main.js @@ -1,4 +1,12 @@ "use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator.throw(value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments)).next()); + }); +}; const vscode_languageserver_1 = require('vscode-languageserver'); const aureliaLanguageService_1 = require('./aurelia-languageservice/aureliaLanguageService'); const languageModelCache_1 = require('./languageModelCache'); @@ -25,8 +33,13 @@ connection.onInitialize((params) => { }; }); let aureliaSettings; -connection.onDidChangeConfiguration((change) => aureliaSettings = change.settings.aurelia); +connection.onDidChangeConfiguration(change => aureliaSettings = change.settings.aurelia); let languageService = aureliaLanguageService_1.getLanguageService(); +documents.onDidChangeContent((change) => __awaiter(this, void 0, void 0, function* () { + let htmlDocument = htmlDocuments.get(change.document); + const diagnostics = yield languageService.doValidation(change.document, htmlDocument); + connection.sendDiagnostics({ uri: change.document.uri, diagnostics }); +})); connection.onCompletion(textDocumentPosition => { let document = documents.get(textDocumentPosition.textDocument.uri); let htmlDocument = htmlDocuments.get(document); diff --git a/dist/src/server/main.js.map b/dist/src/server/main.js.map index cc5bacae..7fde7f66 100644 --- a/dist/src/server/main.js.map +++ b/dist/src/server/main.js.map @@ -1 +1 @@ -{"version":3,"file":"main.js","sourceRoot":"","sources":["../../../src/server/main.ts"],"names":[],"mappings":";AAAA,wCAAiG,uBAAuB,CAAC,CAAA;AACzH,yCAAiD,kDAAkD,CAAC,CAAA;AACpG,qCAAsC,sBAAsB,CAAC,CAAA;AAE7D,IAAI,UAAU,GAAgB,wCAAgB,EAAE,CAAC;AACjD,OAAO,CAAC,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;AAC9D,OAAO,CAAC,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;AAElE,IAAI,SAAS,GAAkB,IAAI,qCAAa,EAAE,CAAC;AACnD,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;AAE7B,IAAI,aAAa,GAAG,0CAAqB,CAAe,EAAE,EAAE,EAAE,EAAE,QAAQ,IAAI,2CAAkB,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC9H,SAAS,CAAC,UAAU,CAAC,CAAC;IACrB,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;AAC7C,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,UAAU,CAAC;IACrB,aAAa,CAAC,OAAO,EAAE,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH,IAAI,aAAqB,CAAC;AAE1B,UAAU,CAAC,YAAY,CAAC,CAAC,MAAwB;IAChD,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC;IAChC,MAAM,CAAC;QACN,YAAY,EAAE;YACb,gBAAgB,EAAE,SAAS,CAAC,QAAQ;YACpC,kBAAkB,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE;SAC7E;KACD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,eAAe,CAAC;AACpB,UAAU,CAAC,wBAAwB,CAAC,CAAC,MAAM,KAAK,eAAe,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAE3F,IAAI,eAAe,GAAG,2CAAkB,EAAE,CAAC;AAE3C,UAAU,CAAC,YAAY,CAAC,oBAAoB;IAC3C,IAAI,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,oBAAoB,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IACpE,IAAI,YAAY,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,EAAE,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,eAAe,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;AAC/H,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,MAAM,EAAE,CAAC"} \ No newline at end of file +{"version":3,"file":"main.js","sourceRoot":"","sources":["../../../src/server/main.ts"],"names":[],"mappings":";;;;;;;;;AAAA,wCAAiG,uBAAuB,CAAC,CAAA;AACzH,yCAAiD,kDAAkD,CAAC,CAAA;AACpG,qCAAsC,sBAAsB,CAAC,CAAA;AAE7D,IAAI,UAAU,GAAgB,wCAAgB,EAAE,CAAC;AACjD,OAAO,CAAC,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;AAC9D,OAAO,CAAC,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;AAElE,IAAI,SAAS,GAAkB,IAAI,qCAAa,EAAE,CAAC;AACnD,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;AAE7B,IAAI,aAAa,GAAG,0CAAqB,CAAe,EAAE,EAAE,EAAE,EAAE,QAAQ,IAAI,2CAAkB,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC9H,SAAS,CAAC,UAAU,CAAC,CAAC;IACrB,aAAa,CAAC,iBAAiB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;AAC7C,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,UAAU,CAAC;IACrB,aAAa,CAAC,OAAO,EAAE,CAAC;AACzB,CAAC,CAAC,CAAC;AAEH,IAAI,aAAqB,CAAC;AAE1B,UAAU,CAAC,YAAY,CAAC,CAAC,MAAwB;IAChD,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC;IAChC,MAAM,CAAC;QACN,YAAY,EAAE;YACb,gBAAgB,EAAE,SAAS,CAAC,QAAQ;YACpC,kBAAkB,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE;SAC7E;KACD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,eAAe,CAAC;AACpB,UAAU,CAAC,wBAAwB,CAAC,MAAM,IAAI,eAAe,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAEzF,IAAI,eAAe,GAAG,2CAAkB,EAAE,CAAC;AAE3C,SAAS,CAAC,kBAAkB,CAAC,CAAM,MAAM;IACxC,IAAI,YAAY,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACtD,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,YAAY,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACtF,UAAU,CAAC,eAAe,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,EAAE,CAAC,CAAC;AACvE,CAAC,CAAA,CAAC,CAAC;AAEH,UAAU,CAAC,YAAY,CAAC,oBAAoB;IAC3C,IAAI,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,oBAAoB,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IACpE,IAAI,YAAY,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC/C,MAAM,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,EAAE,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,eAAe,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;AAC/H,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,MAAM,EAAE,CAAC"} \ No newline at end of file diff --git a/package.json b/package.json index fa5ab254..5b3a1664 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "vscode-textmate": "^2.2.0" }, "dependencies": { - "aurelia-cli": "^0.20.2", + "aurelia-cli": "^0.21.0", "run-in-terminal": "0.0.3", "vscode-languageclient": "^2.6.0", "vscode-languageserver": "^2.6.0", diff --git a/src/client/main.ts b/src/client/main.ts index 372c0d93..53fe0ac9 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -1,4 +1,4 @@ -import * as path from "path"; +import * as path from 'path'; import { ExtensionContext, OutputChannel, window, languages } from 'vscode'; import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient'; import AureliaCliCommands from './aureliaCLICommands'; @@ -7,28 +7,31 @@ let outputChannel: OutputChannel; export function activate(context: ExtensionContext) { - // Create default output channel - outputChannel = window.createOutputChannel('aurelia'); - context.subscriptions.push(outputChannel); + // Create default output channel + outputChannel = window.createOutputChannel('aurelia'); + context.subscriptions.push(outputChannel); - // Register CLI commands - context.subscriptions.push(AureliaCliCommands.registerCommands(outputChannel)); + // Register CLI commands + context.subscriptions.push(AureliaCliCommands.registerCommands(outputChannel)); - // Register Aurelia language server - let serverModule = context.asAbsolutePath(path.join('dist', 'src', 'server', 'main.js')); - let debugOptions = { execArgv: ['--nolazy', '--debug=6004'] }; - let serverOptions: ServerOptions = { + // Register Aurelia language server + const serverModule = context.asAbsolutePath(path.join('dist', 'src', 'server', 'main.js')); + const debugOptions = { execArgv: ['--nolazy', '--debug=6004'] }; + const serverOptions: ServerOptions = { run: { module: serverModule, transport: TransportKind.ipc }, debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } }; - let clientOptions: LanguageClientOptions = { - documentSelector: ['html'], - synchronize: { - configurationSection: ['aurelia'], - }, - initializationOptions: {} - }; - let client = new LanguageClient('html', 'Aurelia', serverOptions, clientOptions); - let disposable = client.start(); - context.subscriptions.push(disposable); + + const clientOptions: LanguageClientOptions = { + diagnosticCollectionName: 'Aurelia', + documentSelector: ['html'], + initializationOptions: {}, + synchronize: { + configurationSection: ['aurelia'], + } + }; + + const client = new LanguageClient('html', 'Aurelia', serverOptions, clientOptions); + const disposable = client.start(); + context.subscriptions.push(disposable); } diff --git a/src/server/.vscode/launch.json b/src/server/.vscode/launch.json new file mode 100644 index 00000000..5c8b8d1c --- /dev/null +++ b/src/server/.vscode/launch.json @@ -0,0 +1,14 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Attach to Language Service", + "type": "node", + "request": "attach", + "port": 6004, + "sourceMaps": true, + "outDir": "${workspaceRoot}/../../dist" + } + ] +} diff --git a/src/server/.vscode/settings.json b/src/server/.vscode/settings.json new file mode 100644 index 00000000..d60e992b --- /dev/null +++ b/src/server/.vscode/settings.json @@ -0,0 +1,4 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "typescript.tsdk": "../../node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version +} diff --git a/src/server/aurelia-languageservice/aureliaLanguageService.ts b/src/server/aurelia-languageservice/aureliaLanguageService.ts index 141951fd..01c5b5e9 100644 --- a/src/server/aurelia-languageservice/aureliaLanguageService.ts +++ b/src/server/aurelia-languageservice/aureliaLanguageService.ts @@ -1,17 +1,30 @@ import { parse } from './parser/htmlParser'; import { doComplete } from './services/htmlCompletion'; -import { TextDocument, Position, CompletionList } from 'vscode-languageserver-types'; +import { HTMLValidation } from './services/htmlValidation'; +import { CompletionList, Diagnostic, Position, TextDocument } from 'vscode-languageserver-types'; +import { HTMLDocument } from './parser/htmlParser'; -export declare type HTMLDocument = {}; +export { HTMLDocument } from './parser/htmlParser'; + +export interface CompletionConfiguration { + [provider: string]: boolean; +} + +export interface LanguageSettings { + validate?: boolean; +} export interface LanguageService { doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument, quotes: string): CompletionList; - parseHTMLDocument(document: TextDocument): HTMLDocument; + doValidation(document: TextDocument, htmlDocument: HTMLDocument): Thenable; + parseHTMLDocument(document: TextDocument): HTMLDocument; } export function getLanguageService(): LanguageService { + const validation = new HTMLValidation(); return { doComplete, - parseHTMLDocument: document => parse(document.getText()), + doValidation: validation.doValidation.bind(validation), + parseHTMLDocument: document => parse(document.getText()) }; } diff --git a/src/server/aurelia-languageservice/services/htmlValidation.ts b/src/server/aurelia-languageservice/services/htmlValidation.ts new file mode 100644 index 00000000..1f0fe057 --- /dev/null +++ b/src/server/aurelia-languageservice/services/htmlValidation.ts @@ -0,0 +1,129 @@ +'use strict'; +//import { commands } from 'vscode'; +import { Files } from 'vscode-languageserver'; +import { Diagnostic, DiagnosticSeverity, Range, SymbolInformation, SymbolKind, TextDocument } from 'vscode-languageserver-types'; +import { LanguageSettings } from "../aureliaLanguageService"; +import { HTMLDocument } from '../parser/htmlParser'; +import { TokenType, createScanner, ScannerState } from '../parser/htmlScanner' +import * as fs from 'fs'; +import * as path from 'path'; + +export type DiagnosticCodes = 'invalid-casing' | 'invalid-method'; +export const DiagnosticCodes = { + InvalidCasing: 'invalid-casing' as DiagnosticCodes, + InvalidMethod: 'invalid-method' as DiagnosticCodes +} + +export const DiagnosticSource = 'Aurelia'; + +interface Attribute { + name: string; + start: number; + length: number; + value?: string; +} + +const kebabCaseValidationRegex = /(.*)\.(bind|one-way|two-way|one-time|call|delegate|trigger)/; +const methodRegex = /\"(.*)\(/; + +function kebabToCamel(s: string) { + return s.replace(/(\-\w)/g, m => m[1].toUpperCase()); +} + +function camelToKebab(s: string) { + return s.replace(/\.?([A-Z])/g, (x, y) => "-" + y.toLowerCase()).replace(/^-/, ""); +} + +export class HTMLValidation { + private validationEnabled: boolean; + + constructor() { + this.validationEnabled = true; + } + + public configure(raw: LanguageSettings) { + if (raw) { + this.validationEnabled = raw.validate; + } + } + + async doValidation(document: TextDocument, htmlDocument: HTMLDocument): Promise { + if (!this.validationEnabled) return Promise.resolve([]); + + const text = document.getText(); + const scanner = createScanner(text, htmlDocument.roots[0].start); + + const diagnostics: Diagnostic[] = []; + + let attr; + let token = scanner.scan(); + while (token !== TokenType.EOS) { + switch (token) { + case TokenType.AttributeName: + attr = { + name: scanner.getTokenText(), + start: scanner.getTokenOffset(), + length: scanner.getTokenLength() + }; + break; + case TokenType.AttributeValue: + attr.value = scanner.getTokenText(); + await this._validateAttribute(attr, document, diagnostics); + break; + } + token = scanner.scan(); + } + + return Promise.resolve(diagnostics); + } + + private async _validateAttribute(attr: Attribute, document: TextDocument, diagnostics: Diagnostic[]) { + let match = kebabCaseValidationRegex.exec(attr.name); + if (match && match.length) { + const prop = match[1]; + const op = match[2]; + + if (prop !== prop.toLowerCase()) { + diagnostics.push(this.toDiagnostic(attr, document, + `'${attr.name}' has invalid casing; it should likely be '${camelToKebab(prop)}.${op}'`, + DiagnosticCodes.InvalidCasing)); + } + + // if (op === 'call' || op === 'trigger' || op === 'delegate') { + // match = methodRegex.exec(attr.value); + // const method = match[1]; + + // const parseFilename = path.parse(Files.uriToFilePath(document.uri)); + // let codeBehindFilename = path.join(parseFilename.dir, `${parseFilename.name}.ts`); + + // if (!fs.existsSync(codeBehindFilename)) { + // codeBehindFilename = path.join(parseFilename.dir, `${parseFilename.name}.js`); + // if (!fs.existsSync(codeBehindFilename)) return Promise.resolve(); + // } + + // // Can't access vscode commands here -- do we really need to talk directly to the typescript language service? + // const symbols = await >commands.executeCommand('vscode.executeDocumentSymbolProvider', Uri.file(codeBehindFilename)); + // if (!symbols.some(s => (s.kind === SymbolKind.Function || s.kind === SymbolKind.Method) && s.name === method)) { + // diagnostics.push(this.toDiagnostic(attr, document, `'${method}' is missing from ${kebabToCamel(prop)}`)); + // } + // } + } + + return Promise.resolve(); + } + + private toDiagnostic(attr: Attribute, document: TextDocument, message: string, code: DiagnosticCodes | undefined = undefined, serverity: DiagnosticSeverity = DiagnosticSeverity.Error): Diagnostic { + const range = Range.create(document.positionAt(attr.start), document.positionAt(attr.start + attr.length)); + const diagnostic = { + message: message, + range: range, + severity: serverity, + source: DiagnosticSource + }; + + if (code !== undefined) { + diagnostic.code = code; + } + return diagnostic; + } +} diff --git a/src/server/main.ts b/src/server/main.ts index 15bdf5be..a98d4ea9 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -31,10 +31,16 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { }); let aureliaSettings; -connection.onDidChangeConfiguration((change) => aureliaSettings = change.settings.aurelia); +connection.onDidChangeConfiguration(change => aureliaSettings = change.settings.aurelia); let languageService = getLanguageService(); +documents.onDidChangeContent(async change => { + let htmlDocument = htmlDocuments.get(change.document); + const diagnostics = await languageService.doValidation(change.document, htmlDocument); + connection.sendDiagnostics({ uri: change.document.uri, diagnostics }); +}); + connection.onCompletion(textDocumentPosition => { let document = documents.get(textDocumentPosition.textDocument.uri); let htmlDocument = htmlDocuments.get(document);