From b6248f81309272816a99a91894b48a34413fcdd7 Mon Sep 17 00:00:00 2001 From: yoyo930021 Date: Wed, 11 Dec 2019 17:59:27 +0800 Subject: [PATCH 1/2] feat: add vue file basic support --- fixtures/e2e/completion/AppButton.vue | 31 ++++++++ fixtures/e2e/definition/AppButton.vue | 18 +++++ fixtures/e2e/hover/AppButton.vue | 18 +++++ fixtures/e2e/signature/AppButton.vue | 19 +++++ package.json | 3 +- src/client.ts | 2 +- src/server.ts | 37 +++++++-- src/test/e2e/runTest.ts | 14 +++- .../e2e/suite/completion/completion.test.ts | 19 +++++ .../e2e/suite/definition/definitions.test.ts | 23 ++++++ src/test/e2e/suite/hover/hover.test.ts | 26 +++++++ .../e2e/suite/signature/signature.test.ts | 54 +++++++++++++ src/test/utils/vue.spec.ts | 78 +++++++++++++++++++ src/utils/vue.ts | 50 ++++++++++++ 14 files changed, 380 insertions(+), 12 deletions(-) create mode 100644 fixtures/e2e/completion/AppButton.vue create mode 100644 fixtures/e2e/definition/AppButton.vue create mode 100644 fixtures/e2e/hover/AppButton.vue create mode 100644 fixtures/e2e/signature/AppButton.vue create mode 100644 src/test/utils/vue.spec.ts create mode 100644 src/utils/vue.ts diff --git a/fixtures/e2e/completion/AppButton.vue b/fixtures/e2e/completion/AppButton.vue new file mode 100644 index 00000000..589658e0 --- /dev/null +++ b/fixtures/e2e/completion/AppButton.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/fixtures/e2e/definition/AppButton.vue b/fixtures/e2e/definition/AppButton.vue new file mode 100644 index 00000000..c82ca68e --- /dev/null +++ b/fixtures/e2e/definition/AppButton.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/fixtures/e2e/hover/AppButton.vue b/fixtures/e2e/hover/AppButton.vue new file mode 100644 index 00000000..c82ca68e --- /dev/null +++ b/fixtures/e2e/hover/AppButton.vue @@ -0,0 +1,18 @@ + + + + + diff --git a/fixtures/e2e/signature/AppButton.vue b/fixtures/e2e/signature/AppButton.vue new file mode 100644 index 00000000..53ad7726 --- /dev/null +++ b/fixtures/e2e/signature/AppButton.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/package.json b/package.json index f4623629..e87ca832 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "Programming Languages" ], "activationEvents": [ - "onLanguage:scss" + "onLanguage:scss", + "onLanguage:vue" ], "main": "./out/client.js", "contributes": { diff --git a/src/client.ts b/src/client.ts index a7f5df8d..3f23fea9 100644 --- a/src/client.ts +++ b/src/client.ts @@ -38,7 +38,7 @@ export function activate(context: vscode.ExtensionContext) { }; const clientOptions: LanguageClientOptions = { - documentSelector: ['scss'], + documentSelector: ['scss', 'vue'], synchronize: { configurationSection: ['scss'], fileEvents: vscode.workspace.createFileSystemWatcher('**/*.scss') diff --git a/src/server.ts b/src/server.ts index 050c3dd1..9203a066 100644 --- a/src/server.ts +++ b/src/server.ts @@ -22,6 +22,7 @@ import { doSignatureHelp } from './providers/signatureHelp'; import { goDefinition } from './providers/goDefinition'; import { searchWorkspaceSymbol } from './providers/workspaceSymbol'; import { findFiles } from './utils/fs'; +import { getSCSSRegionsDocument } from './utils/vue'; let workspaceRoot: string; let settings: ISettings; @@ -91,26 +92,46 @@ connection.onDidChangeWatchedFiles(event => { }); connection.onCompletion(textDocumentPosition => { - const document = documents.get(textDocumentPosition.textDocument.uri); - const offset = document.offsetAt(textDocumentPosition.position); + const { document, offset } = getSCSSRegionsDocument( + documents.get(textDocumentPosition.textDocument.uri), + textDocumentPosition.position + ); + if (!document) { + return null; + } return doCompletion(document, offset, settings, storageService); }); connection.onHover(textDocumentPosition => { - const document = documents.get(textDocumentPosition.textDocument.uri); - const offset = document.offsetAt(textDocumentPosition.position); + const { document, offset } = getSCSSRegionsDocument( + documents.get(textDocumentPosition.textDocument.uri), + textDocumentPosition.position + ); + if (!document) { + return null; + } return doHover(document, offset, storageService); }); connection.onSignatureHelp(textDocumentPosition => { - const document = documents.get(textDocumentPosition.textDocument.uri); - const offset = document.offsetAt(textDocumentPosition.position); + const { document, offset } = getSCSSRegionsDocument( + documents.get(textDocumentPosition.textDocument.uri), + textDocumentPosition.position + ); + if (!document) { + return null; + } return doSignatureHelp(document, offset, storageService); }); connection.onDefinition(textDocumentPosition => { - const document = documents.get(textDocumentPosition.textDocument.uri); - const offset = document.offsetAt(textDocumentPosition.position); + const { document, offset } = getSCSSRegionsDocument( + documents.get(textDocumentPosition.textDocument.uri), + textDocumentPosition.position + ); + if (!document) { + return null; + } return goDefinition(document, offset, storageService); }); diff --git a/src/test/e2e/runTest.ts b/src/test/e2e/runTest.ts index 5020fdd8..6722f79d 100644 --- a/src/test/e2e/runTest.ts +++ b/src/test/e2e/runTest.ts @@ -1,6 +1,7 @@ import * as path from 'path'; +import * as cp from 'child_process'; -import { runTests } from 'vscode-test'; +import { runTests, downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath } from 'vscode-test'; async function main() { try { @@ -15,11 +16,20 @@ async function main() { const workspaceDir = path.resolve(__dirname, '../../../fixtures/e2e'); // Download VS Code, unzip it and run the integration test + const vscodeExecutablePath = await downloadAndUnzipVSCode('1.40.0'); + + const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExecutablePath); + cp.spawnSync(cliPath, ['--install-extension', 'octref.vetur'], { + encoding: 'utf-8', + stdio: 'inherit' + }); + await runTests({ + vscodeExecutablePath, version: '1.40.0', extensionDevelopmentPath, extensionTestsPath, - launchArgs: [workspaceDir, '--disable-extensions'] + launchArgs: [workspaceDir] }); } catch (err) { console.error('Failed to run tests'); diff --git a/src/test/e2e/suite/completion/completion.test.ts b/src/test/e2e/suite/completion/completion.test.ts index d41717e5..6f53cc9b 100644 --- a/src/test/e2e/suite/completion/completion.test.ts +++ b/src/test/e2e/suite/completion/completion.test.ts @@ -3,9 +3,11 @@ import { testCompletion } from './helper'; describe('SCSS Completion Test', () => { const docUri = getDocUri('completion/main.scss'); + const vueDocUri = getDocUri('completion/AppButton.vue'); before(async () => { await showFile(docUri); + await showFile(vueDocUri); await sleep(2000); }); @@ -20,4 +22,21 @@ describe('SCSS Completion Test', () => { it('Offers completions from partial file', async () => { await testCompletion(docUri, position(17, 11), [{ label: '$partial', detail: '_partial.scss' }]); }); + + it('no completions on vue file outside scss regions', async () => { + await testCompletion(vueDocUri, position(2, 9), []); + await testCompletion(vueDocUri, position(6, 8), []); + }); + + it('Offers variable completions on vue file', async () => { + await testCompletion(vueDocUri, position(16, 11), ['$color', '$fonts']); + }); + + it('Offers completions from tilde imports on vue file', async () => { + await testCompletion(vueDocUri, position(22, 11), [{ label: '$tilde', detail: 'node_modules/foo/bar.scss' }]); + }); + + it('Offers completions from partial file on vue file', async () => { + await testCompletion(vueDocUri, position(28, 11), [{ label: '$partial', detail: '_partial.scss' }]); + }); }); diff --git a/src/test/e2e/suite/definition/definitions.test.ts b/src/test/e2e/suite/definition/definitions.test.ts index e1a248fd..1cdec099 100644 --- a/src/test/e2e/suite/definition/definitions.test.ts +++ b/src/test/e2e/suite/definition/definitions.test.ts @@ -3,9 +3,11 @@ import { testDefinition } from './helper'; describe('SCSS Definition Test', () => { const docUri = getDocUri('definition/main.scss'); + const vueDocUri = getDocUri('definition/AppButton.vue'); before(async () => { await showFile(docUri); + await showFile(vueDocUri); await sleep(2000); }); @@ -29,4 +31,25 @@ describe('SCSS Definition Test', () => { await testDefinition(docUri, position(4, 12), expectedLocation); }); + + it('should find definition for variables on vue file', async () => { + const expectedDocumentUri = getDocUri('_variables.scss'); + const expectedLocation = sameLineLocation(expectedDocumentUri, 1, 1, 10); + + await testDefinition(vueDocUri, position(13, 13), expectedLocation); + }); + + it('should find definition for functions on vue file', async () => { + const expectedDocumentUri = getDocUri('_functions.scss'); + const expectedLocation = sameLineLocation(expectedDocumentUri, 1, 1, 9); + + await testDefinition(vueDocUri, position(13, 24), expectedLocation); + }); + + it('should find definition for mixins on vue file', async () => { + const expectedDocumentUri = getDocUri('_mixins.scss'); + const expectedLocation = sameLineLocation(expectedDocumentUri, 1, 1, 6); + + await testDefinition(vueDocUri, position(15, 12), expectedLocation); + }); }); diff --git a/src/test/e2e/suite/hover/hover.test.ts b/src/test/e2e/suite/hover/hover.test.ts index bf40f861..5fcea693 100644 --- a/src/test/e2e/suite/hover/hover.test.ts +++ b/src/test/e2e/suite/hover/hover.test.ts @@ -3,9 +3,11 @@ import { testHover } from './helper'; describe('SCSS Hover Test', () => { const docUri = getDocUri('hover/main.scss'); + const vueDocUri = getDocUri('hover/AppButton.vue'); before(async () => { await showFile(docUri); + await showFile(vueDocUri); await sleep(2000); }); @@ -26,4 +28,28 @@ describe('SCSS Hover Test', () => { contents: ['\n```scss\n@mixin mixin() {…}\n@import "../_mixins.scss" (implicitly)\n```\n'] }); }); + + it('shows hover for variables on vue file', async () => { + await testHover(vueDocUri, position(13, 13), { + contents: [ + 'Determines which page\\-based occurrence of a given element is applied to a counter or string value\\.', + '\n```scss\n$variable: \'value\';\n@import "../_variables.scss" (implicitly)\n```\n' + ] + }); + }); + + it('shows hover for functions on vue file', async () => { + await testHover(vueDocUri, position(13, 24), { + contents: [ + 'Determines which page\\-based occurrence of a given element is applied to a counter or string value\\.', + '\n```scss\n@function function() {…}\n@import "../_functions.scss" (implicitly)\n```\n' + ] + }); + }); + + it('shows hover for mixins on vue file', async () => { + await testHover(vueDocUri, position(15, 12), { + contents: ['\n```scss\n@mixin mixin() {…}\n@import "../_mixins.scss" (implicitly)\n```\n'] + }); + }); }); diff --git a/src/test/e2e/suite/signature/signature.test.ts b/src/test/e2e/suite/signature/signature.test.ts index 2b77758d..4dde3ad0 100644 --- a/src/test/e2e/suite/signature/signature.test.ts +++ b/src/test/e2e/suite/signature/signature.test.ts @@ -3,9 +3,11 @@ import { testSignature } from './helper'; describe('SCSS Signature Help Test', () => { const docUri = getDocUri('signature/main.scss'); + const vueDocUri = getDocUri('signature/AppButton.vue'); before(async () => { await showFile(docUri); + await showFile(vueDocUri); await sleep(2000); }); @@ -35,6 +37,32 @@ describe('SCSS Signature Help Test', () => { ] }); }); + + it('should suggest all parameters of mixin on vue file', async () => { + await testSignature(vueDocUri, position(13, 19), { + activeParameter: 0, + activeSignature: 0, + signatures: [ + { + label: 'square ($size: null, $radius: 0)', + parameters: [{ label: '$size' }, { label: '$radius' }] + } + ] + }); + }); + + it('should suggest the second parameter of mixin on vue file', async () => { + await testSignature(vueDocUri, position(14, 21), { + activeParameter: 1, + activeSignature: 0, + signatures: [ + { + label: 'square ($size: null, $radius: 0)', + parameters: [{ label: '$size' }, { label: '$radius' }] + } + ] + }); + }); }); describe('Function', () => { @@ -63,5 +91,31 @@ describe('SCSS Signature Help Test', () => { ] }); }); + + it('should suggest all parameters of function on vue file', async () => { + await testSignature(vueDocUri, position(16, 16), { + activeParameter: 0, + activeSignature: 0, + signatures: [ + { + label: 'pow ($base: null, $exponent: null)', + parameters: [{ label: '$base' }, { label: '$exponent' }] + } + ] + }); + }); + + it('should suggest the second parameter of function on vue file', async () => { + await testSignature(vueDocUri, position(16, 26), { + activeParameter: 1, + activeSignature: 0, + signatures: [ + { + label: 'pow ($base: null, $exponent: null)', + parameters: [{ label: '$base' }, { label: '$exponent' }] + } + ] + }); + }); }); }); diff --git a/src/test/utils/vue.spec.ts b/src/test/utils/vue.spec.ts new file mode 100644 index 00000000..3e89042e --- /dev/null +++ b/src/test/utils/vue.spec.ts @@ -0,0 +1,78 @@ +'use strict'; + +import * as assert from 'assert'; + +import { + isVueFile, + getVueSCSSRegions, + getVueSCSSContent, + getSCSSRegionsDocument +} from '../../utils/vue'; +import { TextDocument } from 'vscode-css-languageservice'; +import { Position } from 'vscode-languageserver'; + +describe('Utils/Vue', () => { + it('isVueFile', () => { + assert.strictEqual(isVueFile('sdasdsa/AppButton.vue'), true); + assert.strictEqual(isVueFile('sdasdsa/AppButton.scss.vue'), true); + assert.strictEqual(isVueFile('sdasdsa/AppButton.vue.ts'), false); + assert.strictEqual(isVueFile('sdasdsa/sdadsf.ts'), false); + assert.strictEqual(isVueFile('sda.vue/AppButton.scss'), false); + assert.strictEqual(isVueFile('sdasdsa/AppButton.vue.scss'), false); + }); + + it('getVueSCSSRegions', () => { + assert.deepStrictEqual(getVueSCSSRegions(''), [[34, 34]]); + assert.deepStrictEqual(getVueSCSSRegions(''), [[26, 26]]); + assert.deepStrictEqual(getVueSCSSRegions(''), [[26, 26]]); + assert.deepStrictEqual(getVueSCSSRegions(''), [[19, 19]]); + assert.deepStrictEqual(getVueSCSSRegions(''), [[19, 19]]); + + assert.deepStrictEqual(getVueSCSSRegions(''), [[90, 90]]); + assert.deepStrictEqual(getVueSCSSRegions('\n'), [[91, 91]]); + assert.deepStrictEqual(getVueSCSSRegions('\n'), [[91, 92]]); + assert.deepStrictEqual(getVueSCSSRegions('\n'), [[91, 110]]); + + assert.deepStrictEqual(getVueSCSSRegions( + `\n`) + , [[90, 109], [143, 162]]); + assert.deepStrictEqual(getVueSCSSRegions( + `\n\n`) + , [[90, 109], [143, 162], [202, 221]]); + + assert.deepStrictEqual(getVueSCSSRegions(''), []); + assert.deepStrictEqual(getVueSCSSRegions(''), []); + assert.deepStrictEqual(getVueSCSSRegions(''), []); + assert.deepStrictEqual(getVueSCSSRegions(''), []); + assert.deepStrictEqual(getVueSCSSRegions(''), []); + }); + + it('getVueSCSSContent', () => { + assert.strictEqual(getVueSCSSContent('sadja|sio|fuioaf', [[5, 10]]), ' |sio| '); + assert.strictEqual(getVueSCSSContent('sadja|sio|fuio^af^', [[5, 10], [14, 18]]), ' |sio| ^af^'); + + assert.strictEqual(getVueSCSSContent(''), ' '.repeat(90) + ' a\n { color: white; }' + ' '.repeat(8)); + }); + + it('getSCSSRegionsDocument', () => { + const exSCSSDocument = TextDocument.create('sdfdsf.vue/sasfsf.scss', 'scss', 1, ''); + assert.strictEqual(getSCSSRegionsDocument(exSCSSDocument, Position.create(0, 0)).document, exSCSSDocument); + + const exVueDocument = TextDocument.create('components/AppButton.vue', 'vue', 1, ` + + + `); + assert.notDeepStrictEqual(getSCSSRegionsDocument(exVueDocument, Position.create(2, 15)).document, exVueDocument); + assert.deepStrictEqual(getSCSSRegionsDocument(exVueDocument, Position.create(2, 15)).document, null); + + assert.notDeepStrictEqual(getSCSSRegionsDocument(exVueDocument, Position.create(5, 15)).document, exVueDocument); + assert.notDeepStrictEqual(getSCSSRegionsDocument(exVueDocument, Position.create(5, 15)).document, null); + + assert.notDeepStrictEqual(getSCSSRegionsDocument(exVueDocument, Position.create(6, 9)).document, exVueDocument); + assert.deepStrictEqual(getSCSSRegionsDocument(exVueDocument, Position.create(6, 9)).document, null); + }); +}); diff --git a/src/utils/vue.ts b/src/utils/vue.ts new file mode 100644 index 00000000..0bb3bdd0 --- /dev/null +++ b/src/utils/vue.ts @@ -0,0 +1,50 @@ +import { TextDocument, Position } from 'vscode-languageserver'; + +type Region = [number, number]; + +export function isVueFile(path: string) { + return path.endsWith('.vue'); +} + +export function getVueSCSSRegions(content: string) { + const regions: Region[] = []; + const startRe = //g; + const endRe = /<\/style>/g; + /* tslint:disable:no-conditional-assignment */ + let start: RegExpExecArray; + let end: RegExpExecArray; + while ((start = startRe.exec(content)) !== null && (end = endRe.exec(content)) !== null) { + regions.push([start.index + start[0].length, end.index]); + } + return regions; +} + +export function getVueSCSSContent(content: string, regions = getVueSCSSRegions(content)) { + const oldContent = content; + + let newContent = oldContent + .split('\n') + .map(line => ' '.repeat(line.length)) + .join('\n'); + + for (const r of regions) { + newContent = newContent.slice(0, r[0]) + oldContent.slice(r[0], r[1]) + newContent.slice(r[1]); + } + + return newContent; +} + +function convertVueTextDocument(document: TextDocument, regions: Region[]) { + return TextDocument.create(document.uri, 'scss', document.version, getVueSCSSContent(document.getText(), regions)); +} + +export function getSCSSRegionsDocument(document: TextDocument, position: Position) { + const offset = document.offsetAt(position); + if (!isVueFile(document.uri)) { return { document, offset }; } + + const vueSCSSRegions = getVueSCSSRegions(document.getText()); + if (vueSCSSRegions.some(region => region[0] <= offset && region[1] >= offset)) { + return { document: convertVueTextDocument(document, vueSCSSRegions), offset }; + } + return { document: null, offset }; +} From 903c69343e964b99db815b4f22baca74f3586b1e Mon Sep 17 00:00:00 2001 From: yoyo930021 Date: Sun, 5 Jan 2020 17:42:35 +0800 Subject: [PATCH 2/2] chore: fix unit test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e87ca832..99c7457d 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "clean": "rimraf out", "lint": "tslint src/**/*.ts --project ./tsconfig.json", "compile": "tsc", - "test": "mocha out/**/*.spec.js", + "test": "mocha \"out/**/*.spec.js\"", "test:e2e": "node ./out/test/e2e/runTest.js", "build": "npm run clean && npm run lint && npm run compile && npm test", "watch": "npm run clean && npm run lint && tsc --watch"