mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-04-27 16:55:44 +08:00
416 lines
11 KiB
TypeScript
416 lines
11 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { TextDocument, Position, Range } from 'vscode-languageclient';
|
|
import { LanguageService, TokenType } from 'vscode-html-languageservice';
|
|
|
|
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 isInsideStyleRegion(
|
|
languageService: LanguageService,
|
|
documentText: string,
|
|
offset: number
|
|
) {
|
|
let scanner = languageService.createScanner(documentText);
|
|
|
|
let token = scanner.scan();
|
|
while (token !== TokenType.EOS) {
|
|
switch (token) {
|
|
case TokenType.Styles:
|
|
if (offset >= scanner.getTokenOffset() && offset <= scanner.getTokenEnd()) {
|
|
return true;
|
|
}
|
|
}
|
|
token = scanner.scan();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function getCSSVirtualContent(
|
|
languageService: LanguageService,
|
|
documentText: string
|
|
): string {
|
|
let regions: EmbeddedRegion[] = [];
|
|
let scanner = languageService.createScanner(documentText);
|
|
let lastTagName: string = '';
|
|
let lastAttributeName: string | null = null;
|
|
let languageIdFromType: string | undefined = undefined;
|
|
let 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 {
|
|
let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
|
|
if (attributeLanguageId) {
|
|
let start = scanner.getTokenOffset();
|
|
let end = scanner.getTokenEnd();
|
|
let firstChar = documentText[start];
|
|
if (firstChar === "'" || firstChar === '"') {
|
|
start++;
|
|
end--;
|
|
}
|
|
regions.push({
|
|
languageId: attributeLanguageId,
|
|
start,
|
|
end,
|
|
attributeValue: true
|
|
});
|
|
}
|
|
}
|
|
lastAttributeName = null;
|
|
break;
|
|
}
|
|
token = scanner.scan();
|
|
}
|
|
|
|
let content = documentText
|
|
.split('\n')
|
|
.map(line => {
|
|
return ' '.repeat(line.length);
|
|
}).join('\n');
|
|
|
|
regions.forEach(r => {
|
|
if (r.languageId === 'css') {
|
|
content = content.slice(0, r.start) + documentText.slice(r.start, r.end) + content.slice(r.end);
|
|
}
|
|
});
|
|
|
|
return content;
|
|
}
|
|
|
|
|
|
export function getDocumentRegions(
|
|
languageService: LanguageService,
|
|
document: TextDocument
|
|
): HTMLDocumentRegions {
|
|
let regions: EmbeddedRegion[] = [];
|
|
let scanner = languageService.createScanner(document.getText());
|
|
let lastTagName: string = '';
|
|
let lastAttributeName: string | null = null;
|
|
let languageIdFromType: string | undefined = undefined;
|
|
let 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 {
|
|
let attributeLanguageId = getAttributeLanguage(lastAttributeName!);
|
|
if (attributeLanguageId) {
|
|
let start = scanner.getTokenOffset();
|
|
let end = scanner.getTokenEnd();
|
|
let 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[] {
|
|
let result: LanguageRange[] = [];
|
|
let currentPos = range ? range.start : Position.create(0, 0);
|
|
let currentOffset = range ? document.offsetAt(range.start) : 0;
|
|
let endOffset = range ? document.offsetAt(range.end) : document.getText().length;
|
|
for (let region of regions) {
|
|
if (region.end > currentOffset && region.start < endOffset) {
|
|
let start = Math.max(region.start, currentOffset);
|
|
let startPos = document.positionAt(start);
|
|
if (currentOffset < region.start) {
|
|
result.push({
|
|
start: currentPos,
|
|
end: startPos,
|
|
languageId: 'html'
|
|
});
|
|
}
|
|
let end = Math.min(region.end, endOffset);
|
|
let 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) {
|
|
let endPos = range ? range.end : document.positionAt(endOffset);
|
|
result.push({
|
|
start: currentPos,
|
|
end: endPos,
|
|
languageId: 'html'
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function getLanguagesInDocument(
|
|
_document: TextDocument,
|
|
regions: EmbeddedRegion[]
|
|
): string[] {
|
|
let result = [];
|
|
for (let 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 {
|
|
let offset = document.offsetAt(position);
|
|
for (let 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;
|
|
let oldContent = document.getText();
|
|
let result = '';
|
|
let lastSuffix = '';
|
|
for (let 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++) {
|
|
let 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 {
|
|
let match = attributeName.match(/^(style)$|^(on\w+)$/i);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
return match[1] ? 'css' : 'javascript';
|
|
}
|