diff --git a/package.json b/package.json index 0b493f9..7237c60 100644 --- a/package.json +++ b/package.json @@ -57,17 +57,6 @@ "type": "number", "default": 1000, "description": "Controls the maximum number of problems produced by the server." - }, - "gherlint.trace.server": { - "scope": "window", - "type": "string", - "enum": [ - "off", - "message", - "verbose" - ], - "default": "off", - "description": "Traces the communication between VS Code and the language server." } } } diff --git a/server/package-lock.json b/server/package-lock.json index 63b268e..4038c97 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -26,6 +26,11 @@ "vscode-languageserver-types": "3.16.0" } }, + "vscode-languageserver-textdocument": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz", + "integrity": "sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ==" + }, "vscode-languageserver-types": { "version": "3.16.0", "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", diff --git a/server/package.json b/server/package.json index aff4063..3c7c549 100644 --- a/server/package.json +++ b/server/package.json @@ -8,6 +8,7 @@ "author": "Sawjan G.", "license": "MIT", "dependencies": { - "vscode-languageserver": "^7.0.0" + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.4" } } diff --git a/server/src/linter.js b/server/src/linter.js index c20de89..e638f65 100644 --- a/server/src/linter.js +++ b/server/src/linter.js @@ -1,11 +1,41 @@ const { DiagnosticSeverity } = require('vscode-languageserver/node'); -module.exports.validateSteps = async function (document) { +const keywordsRegex = + /(Feature:|Rule:|Background:|Scenario( (Outline|Template))?:|Given|When|Then|And|But|Example(s)?:|Scenarios:)/; + +const stepKeywords = ['Given', 'When', 'Then', 'And', 'But']; +const exampleKeywords = ['Background', 'Scenario', 'Example', 'Scenario Outline', 'Scenario Template']; + +module.exports.validateDocument = function (document, docConfig) { + const diagnostics = []; + diagnostics.push(...checkBeginningStep(document, diagnostics)); + diagnostics.push(...checkRepeatedSteps(document, diagnostics)); + return diagnostics; +}; + +function checkBeginningStep(document, diagnostics) { + let text = document.getText(); + const regex = /(Background:|Scenario( (Outline|Template))?:|Example:)/; + let noOfIssue = 0; + while ((match = regex.exec(text)) && noOfIssue < docConfig.maxNumberOfProblems) { + noOfIssue++; + diagnostics.push({ + severity: DiagnosticSeverity.Warning, + range: { + start: document.positionAt(match.index), + end: document.positionAt(match.index + match[0].length), + }, + message: `Starting step must be "Given" or "When" step`, + }); + } + return diagnostics; +} + +function checkRepeatedSteps(document, diagnostics) { let text = document.getText(); const lines = text.split('\n'); - const regex = /[GWTAB]{1}[ivenhdut]{2,4}\s/; + const regex = /[GWTAB]{1}[ivenhdut]{2,4}/; - let diagnostics = []; let prevStep = ''; let index = 0; lines.forEach((line) => { @@ -18,7 +48,7 @@ module.exports.validateSteps = async function (document) { } const match = regex.exec(line); if (Boolean(match)) { - const matchStep = match[0].trim(); + const matchStep = match[0].trim().trim(':'); if (prevStep === matchStep) { const rangeEnd = index + match.index + matchStep.length; let diagnostic = { @@ -27,18 +57,18 @@ module.exports.validateSteps = async function (document) { start: document.positionAt(index + match.index), end: document.positionAt(rangeEnd), }, - message: `Replace '${matchStep}' with 'And'`, + message: `Replace '${matchStep}' with 'And' or 'But'`, }; diagnostics.push(diagnostic); } if (matchStep !== 'And') { prevStep = matchStep; } - } else { - prevStep = ''; - // send invalid step error + if (!stepKeywords.includes(matchStep)) { + prevStep = ''; + } } index += lineLength; }); return diagnostics; -}; +} diff --git a/server/src/server.js b/server/src/server.js index 936602d..59abcb2 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -5,15 +5,15 @@ const { DidChangeConfigurationNotification, TextDocumentSyncKind, } = require('vscode-languageserver/node'); +const { TextDocument } = require('vscode-languageserver-textdocument'); -const { validateSteps } = require('./linter'); -const { formatDocument } = require('./formatter'); +const { validateDocument } = require('./linter'); // Create a connection for the server, using Node's IPC as a transport. // Also include all preview / proposed LSP features. const connection = createConnection(ProposedFeatures.all); // Text document manager -const documents = new TextDocuments(); +const documents = new TextDocuments(TextDocument); let hasConfigurationCapability = false; let hasWorkspaceFolderCapability = false; @@ -29,11 +29,6 @@ connection.onInitialize((params) => { const result = { capabilities: { textDocumentSync: TextDocumentSyncKind.Full, - completionProvider: { - resolveProvider: true, - }, - documentFormattingProvider: true, - documentRangeFormattingProvider: true, }, }; @@ -73,27 +68,15 @@ connection.onDidChangeConfiguration((change) => { // Reset all cached document settings documentSettings.clear(); } else { - globalSettings = change.settings.languageServerExample || defaultSettings; + globalSettings = change.settings.gherlint || defaultSettings; } - // Revalidate all open text documents - documents.all().forEach(validateSteps); + // Revalidate all open documents + documents.all().forEach((document) => { + const docConfig = getDocumentConfig(document); + validateDocument(document, docConfig); + }); }); -function getDocumentSettings(resource) { - if (!hasConfigurationCapability) { - return Promise.resolve(globalSettings); - } - let result = documentSettings.get(resource); - if (!result) { - result = connection.workspace.getConfiguration({ - scopeUri: resource, - section: 'languageServerExample', - }); - documentSettings.set(resource, result); - } - return result; -} - // Only keep settings for open documents documents.onDidClose((_event) => { documentSettings.delete(_event.document.uri); @@ -102,39 +85,32 @@ documents.onDidClose((_event) => { // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. documents.onDidChangeContent(async ({ document }) => { - // check for errors - const diagnostics = await validateSteps(document); + const docConfig = getDocumentConfig(document); + // Revalidate the document + const diagnostics = await validateDocument(document, docConfig); // Send the computed diagnostics to VS Code. connection.sendDiagnostics({ uri: document.uri, diagnostics }); }); -connection.onDidChangeWatchedFiles(() => { - connection.console.log('We received a file change event'); -}); - -connection.onDocumentFormatting((params) => { - const result = formatDocument(documents, params); - return Promise.resolve(result); -}); - -// This handler provides the initial list of the completion items. -connection.onCompletion(() => { - // The pass parameter contains the position of the text document in - // which code complete got requested. For the example we ignore this - // info and always provide the same completion items. - return []; -}); - -// This handler resolves additional information for the item selected in -// the completion list. -connection.onCompletionResolve((item) => { - return item; -}); - // Make the text document manager listen on the connection // for open, change and close text document events documents.listen(connection); // Listen on the connection connection.listen(); + +function getDocumentConfig(document) { + if (!hasConfigurationCapability) { + return Promise.resolve(globalSettings); + } + let result = documentSettings.get(document); + if (!result) { + result = connection.workspace.getConfiguration({ + scopeUri: document, + section: 'gherlint', + }); + documentSettings.set(document, result); + } + return result; +}