Skip to content

Commit 1516adf

Browse files
committed
feat: try embedded language
1 parent 2d2a25e commit 1516adf

File tree

7 files changed

+457
-12
lines changed

7 files changed

+457
-12
lines changed

package-lock.json

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/css-variables-language-server/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
"description": "CSS Variables Language Server in node.",
44
"version": "2.6.2",
55
"author": "Vu Nguyen",
6-
"license": "MIT",
76
"repository": {
87
"type": "git",
98
"url": "https://github.com/vunguyentuan/vscode-css-variables.git"
109
},
11-
"main": "dist/index.js",
10+
"main": "./dist/index.js",
11+
"module": "./dist/index.mjs",
12+
"license": "MIT",
13+
"files": [
14+
"dist/**"
15+
],
1216
"dependencies": {
1317
"axios": "^0.27.2",
1418
"culori": "0.20.1",
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Range, Position } from 'vscode-languageserver';
7+
import { TextDocument } from 'vscode-languageserver-textdocument';
8+
9+
10+
11+
export interface LanguageRange extends Range {
12+
languageId: string | undefined;
13+
attributeValue?: boolean;
14+
}
15+
16+
export interface HTMLDocumentRegions {
17+
getEmbeddedDocument(languageId: string, ignoreAttributeValues?: boolean): TextDocument;
18+
getLanguageRanges(range: Range): LanguageRange[];
19+
getLanguageAtPosition(position: Position): string | undefined;
20+
getLanguagesInDocument(): string[];
21+
getImportedScripts(): string[];
22+
}
23+
24+
export const CSS_STYLE_RULE = '__';
25+
26+
interface EmbeddedRegion { languageId: string | undefined; start: number; end: number; attributeValue?: boolean; }
27+
28+
29+
export function getDocumentRegions(languageService: LanguageService, document: TextDocument): HTMLDocumentRegions {
30+
const regions: EmbeddedRegion[] = [];
31+
const scanner = languageService.createScanner(document.getText());
32+
let lastTagName = '';
33+
let lastAttributeName: string | null = null;
34+
let languageIdFromType: string | undefined = undefined;
35+
const importedScripts: string[] = [];
36+
37+
let token = scanner.scan();
38+
while (token !== TokenType.EOS) {
39+
switch (token) {
40+
case TokenType.StartTag:
41+
lastTagName = scanner.getTokenText();
42+
lastAttributeName = null;
43+
languageIdFromType = 'javascript';
44+
break;
45+
case TokenType.Styles:
46+
regions.push({ languageId: 'css', start: scanner.getTokenOffset(), end: scanner.getTokenEnd() });
47+
break;
48+
case TokenType.Script:
49+
regions.push({ languageId: languageIdFromType, start: scanner.getTokenOffset(), end: scanner.getTokenEnd() });
50+
break;
51+
case TokenType.AttributeName:
52+
lastAttributeName = scanner.getTokenText();
53+
break;
54+
case TokenType.AttributeValue:
55+
if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') {
56+
let value = scanner.getTokenText();
57+
if (value[0] === '\'' || value[0] === '"') {
58+
value = value.substr(1, value.length - 1);
59+
}
60+
importedScripts.push(value);
61+
} else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') {
62+
if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(scanner.getTokenText())) {
63+
languageIdFromType = 'javascript';
64+
} else if (/["']text\/typescript["']/.test(scanner.getTokenText())) {
65+
languageIdFromType = 'typescript';
66+
} else {
67+
languageIdFromType = undefined;
68+
}
69+
} else {
70+
const attributeLanguageId = getAttributeLanguage(lastAttributeName!);
71+
if (attributeLanguageId) {
72+
let start = scanner.getTokenOffset();
73+
let end = scanner.getTokenEnd();
74+
const firstChar = document.getText()[start];
75+
if (firstChar === '\'' || firstChar === '"') {
76+
start++;
77+
end--;
78+
}
79+
regions.push({ languageId: attributeLanguageId, start, end, attributeValue: true });
80+
}
81+
}
82+
lastAttributeName = null;
83+
break;
84+
}
85+
token = scanner.scan();
86+
}
87+
return {
88+
getLanguageRanges: (range: Range) => getLanguageRanges(document, regions, range),
89+
getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) => getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues),
90+
getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, regions, position),
91+
getLanguagesInDocument: () => getLanguagesInDocument(document, regions),
92+
getImportedScripts: () => importedScripts
93+
};
94+
}
95+
96+
97+
function getLanguageRanges(document: TextDocument, regions: EmbeddedRegion[], range: Range): LanguageRange[] {
98+
const result: LanguageRange[] = [];
99+
let currentPos = range ? range.start : Position.create(0, 0);
100+
let currentOffset = range ? document.offsetAt(range.start) : 0;
101+
const endOffset = range ? document.offsetAt(range.end) : document.getText().length;
102+
for (const region of regions) {
103+
if (region.end > currentOffset && region.start < endOffset) {
104+
const start = Math.max(region.start, currentOffset);
105+
const startPos = document.positionAt(start);
106+
if (currentOffset < region.start) {
107+
result.push({
108+
start: currentPos,
109+
end: startPos,
110+
languageId: 'html'
111+
});
112+
}
113+
const end = Math.min(region.end, endOffset);
114+
const endPos = document.positionAt(end);
115+
if (end > region.start) {
116+
result.push({
117+
start: startPos,
118+
end: endPos,
119+
languageId: region.languageId,
120+
attributeValue: region.attributeValue
121+
});
122+
}
123+
currentOffset = end;
124+
currentPos = endPos;
125+
}
126+
}
127+
if (currentOffset < endOffset) {
128+
const endPos = range ? range.end : document.positionAt(endOffset);
129+
result.push({
130+
start: currentPos,
131+
end: endPos,
132+
languageId: 'html'
133+
});
134+
}
135+
return result;
136+
}
137+
138+
function getLanguagesInDocument(_document: TextDocument, regions: EmbeddedRegion[]): string[] {
139+
const result = [];
140+
for (const region of regions) {
141+
if (region.languageId && result.indexOf(region.languageId) === -1) {
142+
result.push(region.languageId);
143+
if (result.length === 3) {
144+
return result;
145+
}
146+
}
147+
}
148+
result.push('html');
149+
return result;
150+
}
151+
152+
function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[], position: Position): string | undefined {
153+
const offset = document.offsetAt(position);
154+
for (const region of regions) {
155+
if (region.start <= offset) {
156+
if (offset <= region.end) {
157+
return region.languageId;
158+
}
159+
} else {
160+
break;
161+
}
162+
}
163+
return 'html';
164+
}
165+
166+
function getEmbeddedDocument(document: TextDocument, contents: EmbeddedRegion[], languageId: string, ignoreAttributeValues: boolean): TextDocument {
167+
let currentPos = 0;
168+
const oldContent = document.getText();
169+
let result = '';
170+
let lastSuffix = '';
171+
for (const c of contents) {
172+
if (c.languageId === languageId && (!ignoreAttributeValues || !c.attributeValue)) {
173+
result = substituteWithWhitespace(result, currentPos, c.start, oldContent, lastSuffix, getPrefix(c));
174+
result += oldContent.substring(c.start, c.end);
175+
currentPos = c.end;
176+
lastSuffix = getSuffix(c);
177+
}
178+
}
179+
result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, '');
180+
return TextDocument.create(document.uri, languageId, document.version, result);
181+
}
182+
183+
function getPrefix(c: EmbeddedRegion) {
184+
if (c.attributeValue) {
185+
switch (c.languageId) {
186+
case 'css': return CSS_STYLE_RULE + '{';
187+
}
188+
}
189+
return '';
190+
}
191+
function getSuffix(c: EmbeddedRegion) {
192+
if (c.attributeValue) {
193+
switch (c.languageId) {
194+
case 'css': return '}';
195+
case 'javascript': return ';';
196+
}
197+
}
198+
return '';
199+
}
200+
201+
function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) {
202+
let accumulatedWS = 0;
203+
result += before;
204+
for (let i = start + before.length; i < end; i++) {
205+
const ch = oldContent[i];
206+
if (ch === '\n' || ch === '\r') {
207+
// only write new lines, skip the whitespace
208+
accumulatedWS = 0;
209+
result += ch;
210+
} else {
211+
accumulatedWS++;
212+
}
213+
}
214+
result = append(result, ' ', accumulatedWS - after.length);
215+
result += after;
216+
return result;
217+
}
218+
219+
function append(result: string, str: string, n: number): string {
220+
while (n > 0) {
221+
if (n & 1) {
222+
result += str;
223+
}
224+
n >>= 1;
225+
str += str;
226+
}
227+
return result;
228+
}
229+
230+
function getAttributeLanguage(attributeName: string): string | null {
231+
const match = attributeName.match(/^(style)$|^(on\w+)$/i);
232+
if (!match) {
233+
return null;
234+
}
235+
return match[1] ? 'css' : 'javascript';
236+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { TextDocument } from 'vscode-languageserver-textdocument';
7+
8+
export interface LanguageModelCache<T> {
9+
get(document: TextDocument): T;
10+
onDocumentRemoved(document: TextDocument): void;
11+
dispose(): void;
12+
}
13+
14+
export function getLanguageModelCache<T>(maxEntries: number, cleanupIntervalTimeInSec: number, parse: (document: TextDocument) => T): LanguageModelCache<T> {
15+
let languageModels: { [uri: string]: { version: number, languageId: string, cTime: number, languageModel: T } } = {};
16+
let nModels = 0;
17+
18+
let cleanupInterval: NodeJS.Timer | undefined = undefined;
19+
if (cleanupIntervalTimeInSec > 0) {
20+
cleanupInterval = setInterval(() => {
21+
const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000;
22+
const uris = Object.keys(languageModels);
23+
for (const uri of uris) {
24+
const languageModelInfo = languageModels[uri];
25+
if (languageModelInfo.cTime < cutoffTime) {
26+
delete languageModels[uri];
27+
nModels--;
28+
}
29+
}
30+
}, cleanupIntervalTimeInSec * 1000);
31+
}
32+
33+
return {
34+
get(document: TextDocument): T {
35+
const version = document.version;
36+
const languageId = document.languageId;
37+
const languageModelInfo = languageModels[document.uri];
38+
if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) {
39+
languageModelInfo.cTime = Date.now();
40+
return languageModelInfo.languageModel;
41+
}
42+
const languageModel = parse(document);
43+
languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() };
44+
if (!languageModelInfo) {
45+
nModels++;
46+
}
47+
48+
if (nModels === maxEntries) {
49+
let oldestTime = Number.MAX_VALUE;
50+
let oldestUri = null;
51+
for (const uri in languageModels) {
52+
const languageModelInfo = languageModels[uri];
53+
if (languageModelInfo.cTime < oldestTime) {
54+
oldestUri = uri;
55+
oldestTime = languageModelInfo.cTime;
56+
}
57+
}
58+
if (oldestUri) {
59+
delete languageModels[oldestUri];
60+
nModels--;
61+
}
62+
}
63+
return languageModel;
64+
65+
},
66+
onDocumentRemoved(document: TextDocument) {
67+
const uri = document.uri;
68+
if (languageModels[uri]) {
69+
delete languageModels[uri];
70+
nModels--;
71+
}
72+
},
73+
dispose() {
74+
if (typeof cleanupInterval !== 'undefined') {
75+
clearInterval(cleanupInterval);
76+
cleanupInterval = undefined;
77+
languageModels = {};
78+
nModels = 0;
79+
}
80+
}
81+
};
82+
}

0 commit comments

Comments
 (0)