From 2d2a25e6471fa96ad4b4c5acf2796414a3ec7bca Mon Sep 17 00:00:00 2001 From: Vu Nguyen Date: Sat, 18 Mar 2023 11:35:02 +0800 Subject: [PATCH 1/2] chore: fix debug mode --- .vscode/launch.json | 15 +++------------ .vscode/tasks.json | 2 +- package.json | 1 + 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index fea3525..fe8c3bd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,23 +10,14 @@ "args": ["--extensionDevelopmentPath=${workspaceRoot}/packages/vscode-css-variables"], "sourceMaps": true, "outFiles": ["${workspaceRoot}/packages/vscode-css-variables/dist/**/*.js"], - "preLaunchTask": "npm: build" + "preLaunchTask": "npm: debug" }, { "type": "node", "request": "attach", - "name": "Attach to Server 6011", + "name": "Attach to Server 6009", "address": "localhost", - "port": 6011, - "sourceMaps": true, - "outFiles": ["${workspaceRoot}/packages/vscode-css-variables/dist/**/*.js"] - }, - { - "type": "node", - "request": "attach", - "name": "Attach to Server 6012", - "address": "localhost", - "port": 6012, + "port": 6009, "sourceMaps": true, "outFiles": ["${workspaceRoot}/packages/vscode-css-variables/dist/**/*.js"] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ba51426..4cbca01 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,7 +3,7 @@ "tasks": [ { "type": "npm", - "script": "build", + "script": "debug", "group": "build", "presentation": { "panel": "dedicated", diff --git a/package.json b/package.json index d52c581..f2a87cd 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ ], "scripts": { "build": "turbo run build", + "debug": "turbo run build -- --sourcemap", "test": "turbo run test", "lint": "turbo run lint", "dev": "turbo run dev", From 1516adf9fa38c04f0f1e96b74f908f1a0ae8000d Mon Sep 17 00:00:00 2001 From: Vu Nguyen Date: Sat, 18 Mar 2023 11:36:01 +0800 Subject: [PATCH 2/2] feat: try embedded language --- package-lock.json | 5 +- .../package.json | 8 +- .../src/embeddedSupport.ts | 236 ++++++++++++++++++ .../src/languageModelCache.ts | 82 ++++++ .../src/languageModes.ts | 122 +++++++++ packages/vscode-css-variables/package.json | 4 +- packages/vscode-css-variables/src/index.ts | 12 +- 7 files changed, 457 insertions(+), 12 deletions(-) create mode 100644 packages/css-variables-language-server/src/embeddedSupport.ts create mode 100644 packages/css-variables-language-server/src/languageModelCache.ts create mode 100644 packages/css-variables-language-server/src/languageModes.ts diff --git a/package-lock.json b/package-lock.json index e92a1dc..399b4b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10298,7 +10298,7 @@ } }, "packages/css-variables-language-server": { - "version": "2.6.1", + "version": "2.6.2", "license": "MIT", "dependencies": { "axios": "^0.27.2", @@ -10352,7 +10352,7 @@ } }, "packages/vscode-css-variables": { - "version": "2.6.1", + "version": "2.6.3", "dependencies": { "css-variables-language-server": "*" }, @@ -10363,6 +10363,7 @@ "@typescript-eslint/eslint-plugin": "^5.35.1", "@typescript-eslint/parser": "^5.35.1", "@vscode/test-electron": "^2.1.5", + "css-variables-language-server": "*", "eslint": "^8.23.0", "eslint-config-airbnb-typescript": "^17.0.0", "mocha": "^10.0.0", diff --git a/packages/css-variables-language-server/package.json b/packages/css-variables-language-server/package.json index 95ff113..c2bf53f 100644 --- a/packages/css-variables-language-server/package.json +++ b/packages/css-variables-language-server/package.json @@ -3,12 +3,16 @@ "description": "CSS Variables Language Server in node.", "version": "2.6.2", "author": "Vu Nguyen", - "license": "MIT", "repository": { "type": "git", "url": "https://github.com/vunguyentuan/vscode-css-variables.git" }, - "main": "dist/index.js", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "license": "MIT", + "files": [ + "dist/**" + ], "dependencies": { "axios": "^0.27.2", "culori": "0.20.1", diff --git a/packages/css-variables-language-server/src/embeddedSupport.ts b/packages/css-variables-language-server/src/embeddedSupport.ts new file mode 100644 index 0000000..128c140 --- /dev/null +++ b/packages/css-variables-language-server/src/embeddedSupport.ts @@ -0,0 +1,236 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Range, Position } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; + + + +export interface LanguageRange extends Range { + languageId: string | undefined; + attributeValue?: boolean; +} + +export interface HTMLDocumentRegions { + getEmbeddedDocument(languageId: string, ignoreAttributeValues?: boolean): TextDocument; + getLanguageRanges(range: Range): LanguageRange[]; + getLanguageAtPosition(position: Position): string | undefined; + getLanguagesInDocument(): string[]; + getImportedScripts(): string[]; +} + +export const CSS_STYLE_RULE = '__'; + +interface EmbeddedRegion { languageId: string | undefined; start: number; end: number; attributeValue?: boolean; } + + +export function getDocumentRegions(languageService: LanguageService, document: TextDocument): HTMLDocumentRegions { + const regions: EmbeddedRegion[] = []; + const scanner = languageService.createScanner(document.getText()); + let lastTagName = ''; + let lastAttributeName: string | null = null; + let languageIdFromType: string | undefined = undefined; + const importedScripts: string[] = []; + + let token = scanner.scan(); + while (token !== TokenType.EOS) { + switch (token) { + case TokenType.StartTag: + lastTagName = scanner.getTokenText(); + lastAttributeName = null; + languageIdFromType = 'javascript'; + break; + case TokenType.Styles: + regions.push({ languageId: 'css', start: scanner.getTokenOffset(), end: scanner.getTokenEnd() }); + break; + case TokenType.Script: + regions.push({ languageId: languageIdFromType, start: scanner.getTokenOffset(), end: scanner.getTokenEnd() }); + break; + case TokenType.AttributeName: + lastAttributeName = scanner.getTokenText(); + break; + case TokenType.AttributeValue: + if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') { + let value = scanner.getTokenText(); + if (value[0] === '\'' || value[0] === '"') { + value = value.substr(1, value.length - 1); + } + importedScripts.push(value); + } else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') { + if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(scanner.getTokenText())) { + languageIdFromType = 'javascript'; + } else if (/["']text\/typescript["']/.test(scanner.getTokenText())) { + languageIdFromType = 'typescript'; + } else { + languageIdFromType = undefined; + } + } else { + const attributeLanguageId = getAttributeLanguage(lastAttributeName!); + if (attributeLanguageId) { + let start = scanner.getTokenOffset(); + let end = scanner.getTokenEnd(); + const firstChar = document.getText()[start]; + if (firstChar === '\'' || firstChar === '"') { + start++; + end--; + } + regions.push({ languageId: attributeLanguageId, start, end, attributeValue: true }); + } + } + lastAttributeName = null; + break; + } + token = scanner.scan(); + } + return { + getLanguageRanges: (range: Range) => getLanguageRanges(document, regions, range), + getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) => getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues), + getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, regions, position), + getLanguagesInDocument: () => getLanguagesInDocument(document, regions), + getImportedScripts: () => importedScripts + }; +} + + +function getLanguageRanges(document: TextDocument, regions: EmbeddedRegion[], range: Range): LanguageRange[] { + const result: LanguageRange[] = []; + let currentPos = range ? range.start : Position.create(0, 0); + let currentOffset = range ? document.offsetAt(range.start) : 0; + const endOffset = range ? document.offsetAt(range.end) : document.getText().length; + for (const region of regions) { + if (region.end > currentOffset && region.start < endOffset) { + const start = Math.max(region.start, currentOffset); + const startPos = document.positionAt(start); + if (currentOffset < region.start) { + result.push({ + start: currentPos, + end: startPos, + languageId: 'html' + }); + } + const end = Math.min(region.end, endOffset); + const endPos = document.positionAt(end); + if (end > region.start) { + result.push({ + start: startPos, + end: endPos, + languageId: region.languageId, + attributeValue: region.attributeValue + }); + } + currentOffset = end; + currentPos = endPos; + } + } + if (currentOffset < endOffset) { + const endPos = range ? range.end : document.positionAt(endOffset); + result.push({ + start: currentPos, + end: endPos, + languageId: 'html' + }); + } + return result; +} + +function getLanguagesInDocument(_document: TextDocument, regions: EmbeddedRegion[]): string[] { + const result = []; + for (const region of regions) { + if (region.languageId && result.indexOf(region.languageId) === -1) { + result.push(region.languageId); + if (result.length === 3) { + return result; + } + } + } + result.push('html'); + return result; +} + +function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[], position: Position): string | undefined { + const offset = document.offsetAt(position); + for (const region of regions) { + if (region.start <= offset) { + if (offset <= region.end) { + return region.languageId; + } + } else { + break; + } + } + return 'html'; +} + +function getEmbeddedDocument(document: TextDocument, contents: EmbeddedRegion[], languageId: string, ignoreAttributeValues: boolean): TextDocument { + let currentPos = 0; + const oldContent = document.getText(); + let result = ''; + let lastSuffix = ''; + for (const c of contents) { + if (c.languageId === languageId && (!ignoreAttributeValues || !c.attributeValue)) { + result = substituteWithWhitespace(result, currentPos, c.start, oldContent, lastSuffix, getPrefix(c)); + result += oldContent.substring(c.start, c.end); + currentPos = c.end; + lastSuffix = getSuffix(c); + } + } + result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, ''); + return TextDocument.create(document.uri, languageId, document.version, result); +} + +function getPrefix(c: EmbeddedRegion) { + if (c.attributeValue) { + switch (c.languageId) { + case 'css': return CSS_STYLE_RULE + '{'; + } + } + return ''; +} +function getSuffix(c: EmbeddedRegion) { + if (c.attributeValue) { + switch (c.languageId) { + case 'css': return '}'; + case 'javascript': return ';'; + } + } + return ''; +} + +function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) { + let accumulatedWS = 0; + result += before; + for (let i = start + before.length; i < end; i++) { + const ch = oldContent[i]; + if (ch === '\n' || ch === '\r') { + // only write new lines, skip the whitespace + accumulatedWS = 0; + result += ch; + } else { + accumulatedWS++; + } + } + result = append(result, ' ', accumulatedWS - after.length); + result += after; + return result; +} + +function append(result: string, str: string, n: number): string { + while (n > 0) { + if (n & 1) { + result += str; + } + n >>= 1; + str += str; + } + return result; +} + +function getAttributeLanguage(attributeName: string): string | null { + const match = attributeName.match(/^(style)$|^(on\w+)$/i); + if (!match) { + return null; + } + return match[1] ? 'css' : 'javascript'; +} \ No newline at end of file diff --git a/packages/css-variables-language-server/src/languageModelCache.ts b/packages/css-variables-language-server/src/languageModelCache.ts new file mode 100644 index 0000000..1823c6f --- /dev/null +++ b/packages/css-variables-language-server/src/languageModelCache.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TextDocument } from 'vscode-languageserver-textdocument'; + +export interface LanguageModelCache { + get(document: TextDocument): T; + onDocumentRemoved(document: TextDocument): void; + dispose(): void; +} + +export function getLanguageModelCache(maxEntries: number, cleanupIntervalTimeInSec: number, parse: (document: TextDocument) => T): LanguageModelCache { + let languageModels: { [uri: string]: { version: number, languageId: string, cTime: number, languageModel: T } } = {}; + let nModels = 0; + + let cleanupInterval: NodeJS.Timer | undefined = undefined; + if (cleanupIntervalTimeInSec > 0) { + cleanupInterval = setInterval(() => { + const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; + const uris = Object.keys(languageModels); + for (const uri of uris) { + const languageModelInfo = languageModels[uri]; + if (languageModelInfo.cTime < cutoffTime) { + delete languageModels[uri]; + nModels--; + } + } + }, cleanupIntervalTimeInSec * 1000); + } + + return { + get(document: TextDocument): T { + const version = document.version; + const languageId = document.languageId; + const languageModelInfo = languageModels[document.uri]; + if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) { + languageModelInfo.cTime = Date.now(); + return languageModelInfo.languageModel; + } + const languageModel = parse(document); + languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() }; + if (!languageModelInfo) { + nModels++; + } + + if (nModels === maxEntries) { + let oldestTime = Number.MAX_VALUE; + let oldestUri = null; + for (const uri in languageModels) { + const languageModelInfo = languageModels[uri]; + if (languageModelInfo.cTime < oldestTime) { + oldestUri = uri; + oldestTime = languageModelInfo.cTime; + } + } + if (oldestUri) { + delete languageModels[oldestUri]; + nModels--; + } + } + return languageModel; + + }, + onDocumentRemoved(document: TextDocument) { + const uri = document.uri; + if (languageModels[uri]) { + delete languageModels[uri]; + nModels--; + } + }, + dispose() { + if (typeof cleanupInterval !== 'undefined') { + clearInterval(cleanupInterval); + cleanupInterval = undefined; + languageModels = {}; + nModels = 0; + } + } + }; +} \ No newline at end of file diff --git a/packages/css-variables-language-server/src/languageModes.ts b/packages/css-variables-language-server/src/languageModes.ts new file mode 100644 index 0000000..3d848c8 --- /dev/null +++ b/packages/css-variables-language-server/src/languageModes.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getCSSLanguageService } from 'vscode-css-languageservice'; +import { + CompletionList, + Diagnostic, + getLanguageService as getHTMLLanguageService, + Position, + Range, +} from 'vscode-html-languageservice'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { getCSSMode } from './modes/cssMode'; +import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport'; +import { getHTMLMode } from './modes/htmlMode'; +import { getLanguageModelCache, LanguageModelCache } from './languageModelCache'; + +export * from 'vscode-html-languageservice'; + +export interface LanguageMode { + getId(): string; + doValidation?: (document: TextDocument) => Diagnostic[]; + doComplete?: (document: TextDocument, position: Position) => CompletionList; + onDocumentRemoved(document: TextDocument): void; + dispose(): void; +} + +export interface LanguageModes { + getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined; + getModesInRange(document: TextDocument, range: Range): LanguageModeRange[]; + getAllModes(): LanguageMode[]; + getAllModesInDocument(document: TextDocument): LanguageMode[]; + getMode(languageId: string): LanguageMode | undefined; + onDocumentRemoved(document: TextDocument): void; + dispose(): void; +} + +export interface LanguageModeRange extends Range { + mode: LanguageMode | undefined; + attributeValue?: boolean; +} + +export function getLanguageModes(): LanguageModes { + const htmlLanguageService = getHTMLLanguageService(); + const cssLanguageService = getCSSLanguageService(); + + const documentRegions = getLanguageModelCache(10, 60, document => + getDocumentRegions(htmlLanguageService, document) + ); + + let modelCaches: LanguageModelCache[] = []; + modelCaches.push(documentRegions); + + let modes = Object.create(null); + modes['html'] = getHTMLMode(htmlLanguageService); + modes['css'] = getCSSMode(cssLanguageService, documentRegions); + + return { + getModeAtPosition( + document: TextDocument, + position: Position + ): LanguageMode | undefined { + const languageId = documentRegions.get(document).getLanguageAtPosition(position); + if (languageId) { + return modes[languageId]; + } + return undefined; + }, + getModesInRange(document: TextDocument, range: Range): LanguageModeRange[] { + return documentRegions + .get(document) + .getLanguageRanges(range) + .map(r => { + return { + start: r.start, + end: r.end, + mode: r.languageId && modes[r.languageId], + attributeValue: r.attributeValue + }; + }); + }, + getAllModesInDocument(document: TextDocument): LanguageMode[] { + const result = []; + for (const languageId of documentRegions.get(document).getLanguagesInDocument()) { + const mode = modes[languageId]; + if (mode) { + result.push(mode); + } + } + return result; + }, + getAllModes(): LanguageMode[] { + const result = []; + for (const languageId in modes) { + const mode = modes[languageId]; + if (mode) { + result.push(mode); + } + } + return result; + }, + getMode(languageId: string): LanguageMode { + return modes[languageId]; + }, + onDocumentRemoved(document: TextDocument) { + modelCaches.forEach(mc => mc.onDocumentRemoved(document)); + for (const mode in modes) { + modes[mode].onDocumentRemoved(document); + } + }, + dispose(): void { + modelCaches.forEach(mc => mc.dispose()); + modelCaches = []; + for (const mode in modes) { + modes[mode].dispose(); + } + modes = {}; + } + }; +} \ No newline at end of file diff --git a/packages/vscode-css-variables/package.json b/packages/vscode-css-variables/package.json index 84ad2be..e41ecf8 100644 --- a/packages/vscode-css-variables/package.json +++ b/packages/vscode-css-variables/package.json @@ -1,5 +1,6 @@ { "name": "vscode-css-variables", + "private": true, "version": "2.6.3", "displayName": "CSS Variable Autocomplete", "description": "Autocomplete CSS Variable support CSS, SCSS, LESS, PostCSS, VueJS, ReactJS and more", @@ -132,8 +133,7 @@ "eslint": "^8.23.0", "eslint-config-airbnb-typescript": "^17.0.0", "@typescript-eslint/eslint-plugin": "^5.35.1", - "@typescript-eslint/parser": "^5.35.1", - "css-variables-language-server": "*" + "@typescript-eslint/parser": "^5.35.1" }, "__metadata": { "id": "3f96fda8-094b-4817-97fb-a7eab7a8f5e0", diff --git a/packages/vscode-css-variables/src/index.ts b/packages/vscode-css-variables/src/index.ts index f5bddb6..aa468fa 100644 --- a/packages/vscode-css-variables/src/index.ts +++ b/packages/vscode-css-variables/src/index.ts @@ -47,13 +47,13 @@ export function activate(context: ExtensionContext) { 'onLanguage:less', 'onLanguage:css', 'onLanguage:html', - 'onLanguage:javascript', - 'onLanguage:javascriptreact', - 'onLanguage:typescript', - 'onLanguage:typescriptreact', - 'onLanguage:source.css.styled', + // 'onLanguage:javascript', + // 'onLanguage:javascriptreact', + // 'onLanguage:typescript', + // 'onLanguage:typescriptreact', + // 'onLanguage:source.css.styled', ].map((event) => ({ - scheme: 'file', + // scheme: '', language: event.split(':')[1], })), synchronize: {