Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@ -0,0 +1,73 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
|
||||
import { Stylesheet, LanguageService as CSSLanguageService } from 'vscode-css-languageservice';
|
||||
import { LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList, DocumentContext } from './languageModes';
|
||||
import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport';
|
||||
|
||||
export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegions: LanguageModelCache<HTMLDocumentRegions>, workspace: Workspace): LanguageMode {
|
||||
let embeddedCSSDocuments = getLanguageModelCache<TextDocument>(10, 60, document => documentRegions.get(document).getEmbeddedDocument('css'));
|
||||
let cssStylesheets = getLanguageModelCache<Stylesheet>(10, 60, document => cssLanguageService.parseStylesheet(document));
|
||||
|
||||
return {
|
||||
getId() {
|
||||
return 'css';
|
||||
},
|
||||
async doValidation(document: TextDocument, settings = workspace.settings) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded), settings && settings.css);
|
||||
},
|
||||
async doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, _settings = workspace.settings) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
const stylesheet = cssStylesheets.get(embedded);
|
||||
return cssLanguageService.doComplete2(embedded, position, stylesheet, documentContext) || CompletionList.create();
|
||||
},
|
||||
async doHover(document: TextDocument, position: Position) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.doHover(embedded, position, cssStylesheets.get(embedded));
|
||||
},
|
||||
async findDocumentHighlight(document: TextDocument, position: Position) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.findDocumentHighlights(embedded, position, cssStylesheets.get(embedded));
|
||||
},
|
||||
async findDocumentSymbols(document: TextDocument) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.findDocumentSymbols(embedded, cssStylesheets.get(embedded)).filter(s => s.name !== CSS_STYLE_RULE);
|
||||
},
|
||||
async findDefinition(document: TextDocument, position: Position) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.findDefinition(embedded, position, cssStylesheets.get(embedded));
|
||||
},
|
||||
async findReferences(document: TextDocument, position: Position) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.findReferences(embedded, position, cssStylesheets.get(embedded));
|
||||
},
|
||||
async findDocumentColors(document: TextDocument) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.findDocumentColors(embedded, cssStylesheets.get(embedded));
|
||||
},
|
||||
async getColorPresentations(document: TextDocument, color: Color, range: Range) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.getColorPresentations(embedded, cssStylesheets.get(embedded), color, range);
|
||||
},
|
||||
async getFoldingRanges(document: TextDocument) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.getFoldingRanges(embedded, {});
|
||||
},
|
||||
async getSelectionRange(document: TextDocument, position: Position) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.getSelectionRanges(embedded, [position], cssStylesheets.get(embedded))[0];
|
||||
},
|
||||
onDocumentRemoved(document: TextDocument) {
|
||||
embeddedCSSDocuments.onDocumentRemoved(document);
|
||||
cssStylesheets.onDocumentRemoved(document);
|
||||
},
|
||||
dispose() {
|
||||
embeddedCSSDocuments.dispose();
|
||||
cssStylesheets.dispose();
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,233 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, LanguageService, TokenType, Range } from './languageModes';
|
||||
|
||||
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 {
|
||||
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';
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LanguageModes, Settings, LanguageModeRange, TextDocument, Range, TextEdit, FormattingOptions, Position } from './languageModes';
|
||||
import { pushAll } from '../utils/arrays';
|
||||
import { isEOL } from '../utils/strings';
|
||||
|
||||
export async function format(languageModes: LanguageModes, document: TextDocument, formatRange: Range, formattingOptions: FormattingOptions, settings: Settings | undefined, enabledModes: { [mode: string]: boolean }) {
|
||||
let result: TextEdit[] = [];
|
||||
|
||||
let endPos = formatRange.end;
|
||||
let endOffset = document.offsetAt(endPos);
|
||||
let content = document.getText();
|
||||
if (endPos.character === 0 && endPos.line > 0 && endOffset !== content.length) {
|
||||
// if selection ends after a new line, exclude that new line
|
||||
let prevLineStart = document.offsetAt(Position.create(endPos.line - 1, 0));
|
||||
while (isEOL(content, endOffset - 1) && endOffset > prevLineStart) {
|
||||
endOffset--;
|
||||
}
|
||||
formatRange = Range.create(formatRange.start, document.positionAt(endOffset));
|
||||
}
|
||||
|
||||
|
||||
// run the html formatter on the full range and pass the result content to the embedded formatters.
|
||||
// from the final content create a single edit
|
||||
// advantages of this approach are
|
||||
// - correct indents in the html document
|
||||
// - correct initial indent for embedded formatters
|
||||
// - no worrying of overlapping edits
|
||||
|
||||
// make sure we start in html
|
||||
let allRanges = languageModes.getModesInRange(document, formatRange);
|
||||
let i = 0;
|
||||
let startPos = formatRange.start;
|
||||
let isHTML = (range: LanguageModeRange) => range.mode && range.mode.getId() === 'html';
|
||||
|
||||
while (i < allRanges.length && !isHTML(allRanges[i])) {
|
||||
let range = allRanges[i];
|
||||
if (!range.attributeValue && range.mode && range.mode.format) {
|
||||
let edits = await range.mode.format(document, Range.create(startPos, range.end), formattingOptions, settings);
|
||||
pushAll(result, edits);
|
||||
}
|
||||
startPos = range.end;
|
||||
i++;
|
||||
}
|
||||
if (i === allRanges.length) {
|
||||
return result;
|
||||
}
|
||||
// modify the range
|
||||
formatRange = Range.create(startPos, formatRange.end);
|
||||
|
||||
// perform a html format and apply changes to a new document
|
||||
let htmlMode = languageModes.getMode('html')!;
|
||||
let htmlEdits = await htmlMode.format!(document, formatRange, formattingOptions, settings);
|
||||
let htmlFormattedContent = TextDocument.applyEdits(document, htmlEdits);
|
||||
let newDocument = TextDocument.create(document.uri + '.tmp', document.languageId, document.version, htmlFormattedContent);
|
||||
try {
|
||||
// run embedded formatters on html formatted content: - formatters see correct initial indent
|
||||
let afterFormatRangeLength = document.getText().length - document.offsetAt(formatRange.end); // length of unchanged content after replace range
|
||||
let newFormatRange = Range.create(formatRange.start, newDocument.positionAt(htmlFormattedContent.length - afterFormatRangeLength));
|
||||
let embeddedRanges = languageModes.getModesInRange(newDocument, newFormatRange);
|
||||
|
||||
let embeddedEdits: TextEdit[] = [];
|
||||
|
||||
for (let r of embeddedRanges) {
|
||||
let mode = r.mode;
|
||||
if (mode && mode.format && enabledModes[mode.getId()] && !r.attributeValue) {
|
||||
let edits = await mode.format(newDocument, r, formattingOptions, settings);
|
||||
for (let edit of edits) {
|
||||
embeddedEdits.push(edit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (embeddedEdits.length === 0) {
|
||||
pushAll(result, htmlEdits);
|
||||
return result;
|
||||
}
|
||||
|
||||
// apply all embedded format edits and create a single edit for all changes
|
||||
let resultContent = TextDocument.applyEdits(newDocument, embeddedEdits);
|
||||
let resultReplaceText = resultContent.substring(document.offsetAt(formatRange.start), resultContent.length - afterFormatRangeLength);
|
||||
|
||||
result.push(TextEdit.replace(formatRange, resultReplaceText));
|
||||
return result;
|
||||
} finally {
|
||||
languageModes.onDocumentRemoved(newDocument);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TextDocument, FoldingRange, Position, Range, LanguageModes, LanguageMode } from './languageModes';
|
||||
import { CancellationToken } from 'vscode-languageserver';
|
||||
|
||||
export async function getFoldingRanges(languageModes: LanguageModes, document: TextDocument, maxRanges: number | undefined, _cancellationToken: CancellationToken | null): Promise<FoldingRange[]> {
|
||||
let htmlMode = languageModes.getMode('html');
|
||||
let range = Range.create(Position.create(0, 0), Position.create(document.lineCount, 0));
|
||||
let result: FoldingRange[] = [];
|
||||
if (htmlMode && htmlMode.getFoldingRanges) {
|
||||
result.push(... await htmlMode.getFoldingRanges(document));
|
||||
}
|
||||
|
||||
// cache folding ranges per mode
|
||||
let rangesPerMode: { [mode: string]: FoldingRange[] } = Object.create(null);
|
||||
let getRangesForMode = async (mode: LanguageMode) => {
|
||||
if (mode.getFoldingRanges) {
|
||||
let ranges = rangesPerMode[mode.getId()];
|
||||
if (!Array.isArray(ranges)) {
|
||||
ranges = await mode.getFoldingRanges(document) || [];
|
||||
rangesPerMode[mode.getId()] = ranges;
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
let modeRanges = languageModes.getModesInRange(document, range);
|
||||
for (let modeRange of modeRanges) {
|
||||
let mode = modeRange.mode;
|
||||
if (mode && mode !== htmlMode && !modeRange.attributeValue) {
|
||||
const ranges = await getRangesForMode(mode);
|
||||
result.push(...ranges.filter(r => r.startLine >= modeRange.start.line && r.endLine < modeRange.end.line));
|
||||
}
|
||||
}
|
||||
if (maxRanges && result.length > maxRanges) {
|
||||
result = limitRanges(result, maxRanges);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function limitRanges(ranges: FoldingRange[], maxRanges: number) {
|
||||
ranges = ranges.sort((r1, r2) => {
|
||||
let diff = r1.startLine - r2.startLine;
|
||||
if (diff === 0) {
|
||||
diff = r1.endLine - r2.endLine;
|
||||
}
|
||||
return diff;
|
||||
});
|
||||
|
||||
// compute each range's nesting level in 'nestingLevels'.
|
||||
// count the number of ranges for each level in 'nestingLevelCounts'
|
||||
let top: FoldingRange | undefined = undefined;
|
||||
let previous: FoldingRange[] = [];
|
||||
let nestingLevels: number[] = [];
|
||||
let nestingLevelCounts: number[] = [];
|
||||
|
||||
let setNestingLevel = (index: number, level: number) => {
|
||||
nestingLevels[index] = level;
|
||||
if (level < 30) {
|
||||
nestingLevelCounts[level] = (nestingLevelCounts[level] || 0) + 1;
|
||||
}
|
||||
};
|
||||
|
||||
// compute nesting levels and sanitize
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
let entry = ranges[i];
|
||||
if (!top) {
|
||||
top = entry;
|
||||
setNestingLevel(i, 0);
|
||||
} else {
|
||||
if (entry.startLine > top.startLine) {
|
||||
if (entry.endLine <= top.endLine) {
|
||||
previous.push(top);
|
||||
top = entry;
|
||||
setNestingLevel(i, previous.length);
|
||||
} else if (entry.startLine > top.endLine) {
|
||||
do {
|
||||
top = previous.pop();
|
||||
} while (top && entry.startLine > top.endLine);
|
||||
if (top) {
|
||||
previous.push(top);
|
||||
}
|
||||
top = entry;
|
||||
setNestingLevel(i, previous.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let entries = 0;
|
||||
let maxLevel = 0;
|
||||
for (let i = 0; i < nestingLevelCounts.length; i++) {
|
||||
let n = nestingLevelCounts[i];
|
||||
if (n) {
|
||||
if (n + entries > maxRanges) {
|
||||
maxLevel = i;
|
||||
break;
|
||||
}
|
||||
entries += n;
|
||||
}
|
||||
}
|
||||
let result = [];
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
let level = nestingLevels[i];
|
||||
if (typeof level === 'number') {
|
||||
if (level < maxLevel || (level === maxLevel && entries++ < maxRanges)) {
|
||||
result.push(ranges[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getLanguageModelCache } from '../languageModelCache';
|
||||
import {
|
||||
LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext, FormattingOptions,
|
||||
HTMLFormatConfiguration, SelectionRange,
|
||||
TextDocument, Position, Range, FoldingRange,
|
||||
LanguageMode, Workspace
|
||||
} from './languageModes';
|
||||
|
||||
export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: Workspace): LanguageMode {
|
||||
let htmlDocuments = getLanguageModelCache<HTMLDocument>(10, 60, document => htmlLanguageService.parseHTMLDocument(document));
|
||||
return {
|
||||
getId() {
|
||||
return 'html';
|
||||
},
|
||||
async getSelectionRange(document: TextDocument, position: Position): Promise<SelectionRange> {
|
||||
return htmlLanguageService.getSelectionRanges(document, [position])[0];
|
||||
},
|
||||
doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, settings = workspace.settings) {
|
||||
let options = settings && settings.html && settings.html.suggest;
|
||||
let doAutoComplete = settings && settings.html && settings.html.autoClosingTags;
|
||||
if (doAutoComplete) {
|
||||
options.hideAutoCompleteProposals = true;
|
||||
}
|
||||
|
||||
const htmlDocument = htmlDocuments.get(document);
|
||||
let completionList = htmlLanguageService.doComplete2(document, position, htmlDocument, documentContext, options);
|
||||
return completionList;
|
||||
},
|
||||
async doHover(document: TextDocument, position: Position) {
|
||||
return htmlLanguageService.doHover(document, position, htmlDocuments.get(document));
|
||||
},
|
||||
async findDocumentHighlight(document: TextDocument, position: Position) {
|
||||
return htmlLanguageService.findDocumentHighlights(document, position, htmlDocuments.get(document));
|
||||
},
|
||||
async findDocumentLinks(document: TextDocument, documentContext: DocumentContext) {
|
||||
return htmlLanguageService.findDocumentLinks(document, documentContext);
|
||||
},
|
||||
async findDocumentSymbols(document: TextDocument) {
|
||||
return htmlLanguageService.findDocumentSymbols(document, htmlDocuments.get(document));
|
||||
},
|
||||
async format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings = workspace.settings) {
|
||||
let formatSettings: HTMLFormatConfiguration = settings && settings.html && settings.html.format;
|
||||
if (formatSettings) {
|
||||
formatSettings = merge(formatSettings, {});
|
||||
} else {
|
||||
formatSettings = {};
|
||||
}
|
||||
if (formatSettings.contentUnformatted) {
|
||||
formatSettings.contentUnformatted = formatSettings.contentUnformatted + ',script';
|
||||
} else {
|
||||
formatSettings.contentUnformatted = 'script';
|
||||
}
|
||||
formatSettings = merge(formatParams, formatSettings);
|
||||
return htmlLanguageService.format(document, range, formatSettings);
|
||||
},
|
||||
async getFoldingRanges(document: TextDocument): Promise<FoldingRange[]> {
|
||||
return htmlLanguageService.getFoldingRanges(document);
|
||||
},
|
||||
async doAutoClose(document: TextDocument, position: Position) {
|
||||
let offset = document.offsetAt(position);
|
||||
let text = document.getText();
|
||||
if (offset > 0 && text.charAt(offset - 1).match(/[>\/]/g)) {
|
||||
return htmlLanguageService.doTagComplete(document, position, htmlDocuments.get(document));
|
||||
}
|
||||
return null;
|
||||
},
|
||||
async doRename(document: TextDocument, position: Position, newName: string) {
|
||||
const htmlDocument = htmlDocuments.get(document);
|
||||
return htmlLanguageService.doRename(document, position, newName, htmlDocument);
|
||||
},
|
||||
async onDocumentRemoved(document: TextDocument) {
|
||||
htmlDocuments.onDocumentRemoved(document);
|
||||
},
|
||||
async findMatchingTagPosition(document: TextDocument, position: Position) {
|
||||
const htmlDocument = htmlDocuments.get(document);
|
||||
return htmlLanguageService.findMatchingTagPosition(document, position, htmlDocument);
|
||||
},
|
||||
async doOnTypeRename(document: TextDocument, position: Position) {
|
||||
const htmlDocument = htmlDocuments.get(document);
|
||||
return htmlLanguageService.findOnTypeRenameRanges(document, position, htmlDocument);
|
||||
},
|
||||
dispose() {
|
||||
htmlDocuments.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function merge(src: any, dst: any): any {
|
||||
for (const key in src) {
|
||||
if (src.hasOwnProperty(key)) {
|
||||
dst[key] = src[key];
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { join, basename, dirname } from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
const contents: { [name: string]: string } = {};
|
||||
|
||||
const serverFolder = basename(__dirname) === 'dist' ? dirname(__dirname) : dirname(dirname(__dirname));
|
||||
const TYPESCRIPT_LIB_SOURCE = join(serverFolder, '../../node_modules/typescript/lib');
|
||||
const JQUERY_PATH = join(serverFolder, 'lib/jquery.d.ts');
|
||||
|
||||
export function loadLibrary(name: string) {
|
||||
let content = contents[name];
|
||||
if (typeof content !== 'string') {
|
||||
let libPath;
|
||||
if (name === 'jquery') {
|
||||
libPath = JQUERY_PATH;
|
||||
} else {
|
||||
libPath = join(TYPESCRIPT_LIB_SOURCE, name); // from source
|
||||
}
|
||||
try {
|
||||
content = readFileSync(libPath).toString();
|
||||
} catch (e) {
|
||||
console.log(`Unable to load library ${name} at ${libPath}: ${e.message}`);
|
||||
content = '';
|
||||
}
|
||||
contents[name] = content;
|
||||
}
|
||||
return content;
|
||||
}
|
@ -0,0 +1,477 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
|
||||
import {
|
||||
SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation,
|
||||
Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, MarkedString,
|
||||
DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange,
|
||||
LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext
|
||||
} from './languageModes';
|
||||
import { getWordAtText, isWhitespaceOnly, repeat } from '../utils/strings';
|
||||
import { HTMLDocumentRegions } from './embeddedSupport';
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticTokens';
|
||||
|
||||
const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
|
||||
|
||||
function getLanguageServiceHost(scriptKind: ts.ScriptKind) {
|
||||
const compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es6.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic, experimentalDecorators: false };
|
||||
|
||||
let currentTextDocument = TextDocument.create('init', 'javascript', 1, '');
|
||||
const jsLanguageService = import(/* webpackChunkName: "javascriptLibs" */ './javascriptLibs').then(libs => {
|
||||
const host: ts.LanguageServiceHost = {
|
||||
getCompilationSettings: () => compilerOptions,
|
||||
getScriptFileNames: () => [currentTextDocument.uri, 'jquery'],
|
||||
getScriptKind: (fileName) => {
|
||||
if (fileName === currentTextDocument.uri) {
|
||||
return scriptKind;
|
||||
}
|
||||
return fileName.substr(fileName.length - 2) === 'ts' ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
||||
},
|
||||
getScriptVersion: (fileName: string) => {
|
||||
if (fileName === currentTextDocument.uri) {
|
||||
return String(currentTextDocument.version);
|
||||
}
|
||||
return '1'; // default lib an jquery.d.ts are static
|
||||
},
|
||||
getScriptSnapshot: (fileName: string) => {
|
||||
let text = '';
|
||||
if (fileName === currentTextDocument.uri) {
|
||||
text = currentTextDocument.getText();
|
||||
} else {
|
||||
text = libs.loadLibrary(fileName);
|
||||
}
|
||||
return {
|
||||
getText: (start, end) => text.substring(start, end),
|
||||
getLength: () => text.length,
|
||||
getChangeRange: () => undefined
|
||||
};
|
||||
},
|
||||
getCurrentDirectory: () => '',
|
||||
getDefaultLibFileName: (_options: ts.CompilerOptions) => 'es6'
|
||||
};
|
||||
return ts.createLanguageService(host);
|
||||
});
|
||||
return {
|
||||
async getLanguageService(jsDocument: TextDocument): Promise<ts.LanguageService> {
|
||||
currentTextDocument = jsDocument;
|
||||
return jsLanguageService;
|
||||
},
|
||||
getCompilationSettings() {
|
||||
return compilerOptions;
|
||||
},
|
||||
dispose() {
|
||||
if (jsLanguageService) {
|
||||
jsLanguageService.then(s => s.dispose());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>, languageId: 'javascript' | 'typescript', workspace: Workspace): LanguageMode {
|
||||
let jsDocuments = getLanguageModelCache<TextDocument>(10, 60, document => documentRegions.get(document).getEmbeddedDocument(languageId));
|
||||
|
||||
const host = getLanguageServiceHost(languageId === 'javascript' ? ts.ScriptKind.JS : ts.ScriptKind.TS);
|
||||
let globalSettings: Settings = {};
|
||||
|
||||
return {
|
||||
getId() {
|
||||
return languageId;
|
||||
},
|
||||
async doValidation(document: TextDocument, settings = workspace.settings): Promise<Diagnostic[]> {
|
||||
host.getCompilationSettings()['experimentalDecorators'] = settings && settings.javascript && settings.javascript.implicitProjectConfig.experimentalDecorators;
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const languageService = await host.getLanguageService(jsDocument);
|
||||
const syntaxDiagnostics: ts.Diagnostic[] = languageService.getSyntacticDiagnostics(jsDocument.uri);
|
||||
const semanticDiagnostics = languageService.getSemanticDiagnostics(jsDocument.uri);
|
||||
return syntaxDiagnostics.concat(semanticDiagnostics).map((diag: ts.Diagnostic): Diagnostic => {
|
||||
return {
|
||||
range: convertRange(jsDocument, diag),
|
||||
severity: DiagnosticSeverity.Error,
|
||||
source: languageId,
|
||||
message: ts.flattenDiagnosticMessageText(diag.messageText, '\n')
|
||||
};
|
||||
});
|
||||
},
|
||||
async doComplete(document: TextDocument, position: Position, _documentContext: DocumentContext): Promise<CompletionList> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
let offset = jsDocument.offsetAt(position);
|
||||
let completions = jsLanguageService.getCompletionsAtPosition(jsDocument.uri, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
|
||||
if (!completions) {
|
||||
return { isIncomplete: false, items: [] };
|
||||
}
|
||||
let replaceRange = convertRange(jsDocument, getWordAtText(jsDocument.getText(), offset, JS_WORD_REGEX));
|
||||
return {
|
||||
isIncomplete: false,
|
||||
items: completions.entries.map(entry => {
|
||||
return {
|
||||
uri: document.uri,
|
||||
position: position,
|
||||
label: entry.name,
|
||||
sortText: entry.sortText,
|
||||
kind: convertKind(entry.kind),
|
||||
textEdit: TextEdit.replace(replaceRange, entry.name),
|
||||
data: { // data used for resolving item details (see 'doResolve')
|
||||
languageId,
|
||||
uri: document.uri,
|
||||
offset: offset
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
},
|
||||
async doResolve(document: TextDocument, item: CompletionItem): Promise<CompletionItem> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
let details = jsLanguageService.getCompletionEntryDetails(jsDocument.uri, item.data.offset, item.label, undefined, undefined, undefined);
|
||||
if (details) {
|
||||
item.detail = ts.displayPartsToString(details.displayParts);
|
||||
item.documentation = ts.displayPartsToString(details.documentation);
|
||||
delete item.data;
|
||||
}
|
||||
return item;
|
||||
},
|
||||
async doHover(document: TextDocument, position: Position): Promise<Hover | null> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
let info = jsLanguageService.getQuickInfoAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
|
||||
if (info) {
|
||||
let contents = ts.displayPartsToString(info.displayParts);
|
||||
return {
|
||||
range: convertRange(jsDocument, info.textSpan),
|
||||
contents: MarkedString.fromPlainText(contents)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
async doSignatureHelp(document: TextDocument, position: Position): Promise<SignatureHelp | null> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
let signHelp = jsLanguageService.getSignatureHelpItems(jsDocument.uri, jsDocument.offsetAt(position), undefined);
|
||||
if (signHelp) {
|
||||
let ret: SignatureHelp = {
|
||||
activeSignature: signHelp.selectedItemIndex,
|
||||
activeParameter: signHelp.argumentIndex,
|
||||
signatures: []
|
||||
};
|
||||
signHelp.items.forEach(item => {
|
||||
|
||||
let signature: SignatureInformation = {
|
||||
label: '',
|
||||
documentation: undefined,
|
||||
parameters: []
|
||||
};
|
||||
|
||||
signature.label += ts.displayPartsToString(item.prefixDisplayParts);
|
||||
item.parameters.forEach((p, i, a) => {
|
||||
let label = ts.displayPartsToString(p.displayParts);
|
||||
let parameter: ParameterInformation = {
|
||||
label: label,
|
||||
documentation: ts.displayPartsToString(p.documentation)
|
||||
};
|
||||
signature.label += label;
|
||||
signature.parameters!.push(parameter);
|
||||
if (i < a.length - 1) {
|
||||
signature.label += ts.displayPartsToString(item.separatorDisplayParts);
|
||||
}
|
||||
});
|
||||
signature.label += ts.displayPartsToString(item.suffixDisplayParts);
|
||||
ret.signatures.push(signature);
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
async findDocumentHighlight(document: TextDocument, position: Position): Promise<DocumentHighlight[]> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
const highlights = jsLanguageService.getDocumentHighlights(jsDocument.uri, jsDocument.offsetAt(position), [jsDocument.uri]);
|
||||
const out: DocumentHighlight[] = [];
|
||||
for (const entry of highlights || []) {
|
||||
for (const highlight of entry.highlightSpans) {
|
||||
out.push({
|
||||
range: convertRange(jsDocument, highlight.textSpan),
|
||||
kind: highlight.kind === 'writtenReference' ? DocumentHighlightKind.Write : DocumentHighlightKind.Text
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
async findDocumentSymbols(document: TextDocument): Promise<SymbolInformation[]> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
let items = jsLanguageService.getNavigationBarItems(jsDocument.uri);
|
||||
if (items) {
|
||||
let result: SymbolInformation[] = [];
|
||||
let existing = Object.create(null);
|
||||
let collectSymbols = (item: ts.NavigationBarItem, containerLabel?: string) => {
|
||||
let sig = item.text + item.kind + item.spans[0].start;
|
||||
if (item.kind !== 'script' && !existing[sig]) {
|
||||
let symbol: SymbolInformation = {
|
||||
name: item.text,
|
||||
kind: convertSymbolKind(item.kind),
|
||||
location: {
|
||||
uri: document.uri,
|
||||
range: convertRange(jsDocument, item.spans[0])
|
||||
},
|
||||
containerName: containerLabel
|
||||
};
|
||||
existing[sig] = true;
|
||||
result.push(symbol);
|
||||
containerLabel = item.text;
|
||||
}
|
||||
|
||||
if (item.childItems && item.childItems.length > 0) {
|
||||
for (let child of item.childItems) {
|
||||
collectSymbols(child, containerLabel);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
items.forEach(item => collectSymbols(item));
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
async findDefinition(document: TextDocument, position: Position): Promise<Definition | null> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
let definition = jsLanguageService.getDefinitionAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
|
||||
if (definition) {
|
||||
return definition.filter(d => d.fileName === jsDocument.uri).map(d => {
|
||||
return {
|
||||
uri: document.uri,
|
||||
range: convertRange(jsDocument, d.textSpan)
|
||||
};
|
||||
});
|
||||
}
|
||||
return null;
|
||||
},
|
||||
async findReferences(document: TextDocument, position: Position): Promise<Location[]> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
let references = jsLanguageService.getReferencesAtPosition(jsDocument.uri, jsDocument.offsetAt(position));
|
||||
if (references) {
|
||||
return references.filter(d => d.fileName === jsDocument.uri).map(d => {
|
||||
return {
|
||||
uri: document.uri,
|
||||
range: convertRange(jsDocument, d.textSpan)
|
||||
};
|
||||
});
|
||||
}
|
||||
return [];
|
||||
},
|
||||
async getSelectionRange(document: TextDocument, position: Position): Promise<SelectionRange> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
function convertSelectionRange(selectionRange: ts.SelectionRange): SelectionRange {
|
||||
const parent = selectionRange.parent ? convertSelectionRange(selectionRange.parent) : undefined;
|
||||
return SelectionRange.create(convertRange(jsDocument, selectionRange.textSpan), parent);
|
||||
}
|
||||
const range = jsLanguageService.getSmartSelectionRange(jsDocument.uri, jsDocument.offsetAt(position));
|
||||
return convertSelectionRange(range);
|
||||
},
|
||||
async format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings: Settings = globalSettings): Promise<TextEdit[]> {
|
||||
const jsDocument = documentRegions.get(document).getEmbeddedDocument('javascript', true);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
|
||||
let formatterSettings = settings && settings.javascript && settings.javascript.format;
|
||||
|
||||
let initialIndentLevel = computeInitialIndent(document, range, formatParams);
|
||||
let formatSettings = convertOptions(formatParams, formatterSettings, initialIndentLevel + 1);
|
||||
let start = jsDocument.offsetAt(range.start);
|
||||
let end = jsDocument.offsetAt(range.end);
|
||||
let lastLineRange = null;
|
||||
if (range.end.line > range.start.line && (range.end.character === 0 || isWhitespaceOnly(jsDocument.getText().substr(end - range.end.character, range.end.character)))) {
|
||||
end -= range.end.character;
|
||||
lastLineRange = Range.create(Position.create(range.end.line, 0), range.end);
|
||||
}
|
||||
let edits = jsLanguageService.getFormattingEditsForRange(jsDocument.uri, start, end, formatSettings);
|
||||
if (edits) {
|
||||
let result = [];
|
||||
for (let edit of edits) {
|
||||
if (edit.span.start >= start && edit.span.start + edit.span.length <= end) {
|
||||
result.push({
|
||||
range: convertRange(jsDocument, edit.span),
|
||||
newText: edit.newText
|
||||
});
|
||||
}
|
||||
}
|
||||
if (lastLineRange) {
|
||||
result.push({
|
||||
range: lastLineRange,
|
||||
newText: generateIndent(initialIndentLevel, formatParams)
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
async getFoldingRanges(document: TextDocument): Promise<FoldingRange[]> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
let spans = jsLanguageService.getOutliningSpans(jsDocument.uri);
|
||||
let ranges: FoldingRange[] = [];
|
||||
for (let span of spans) {
|
||||
let curr = convertRange(jsDocument, span.textSpan);
|
||||
let startLine = curr.start.line;
|
||||
let endLine = curr.end.line;
|
||||
if (startLine < endLine) {
|
||||
let foldingRange: FoldingRange = { startLine, endLine };
|
||||
let match = document.getText(curr).match(/^\s*\/(?:(\/\s*#(?:end)?region\b)|(\*|\/))/);
|
||||
if (match) {
|
||||
foldingRange.kind = match[1] ? FoldingRangeKind.Region : FoldingRangeKind.Comment;
|
||||
}
|
||||
ranges.push(foldingRange);
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
},
|
||||
onDocumentRemoved(document: TextDocument) {
|
||||
jsDocuments.onDocumentRemoved(document);
|
||||
},
|
||||
async getSemanticTokens(document: TextDocument): Promise<SemanticTokenData[]> {
|
||||
const jsDocument = jsDocuments.get(document);
|
||||
const jsLanguageService = await host.getLanguageService(jsDocument);
|
||||
return getSemanticTokens(jsLanguageService, jsDocument, jsDocument.uri);
|
||||
},
|
||||
getSemanticTokenLegend(): { types: string[], modifiers: string[] } {
|
||||
return getSemanticTokenLegend();
|
||||
},
|
||||
dispose() {
|
||||
host.dispose();
|
||||
jsDocuments.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function convertRange(document: TextDocument, span: { start: number | undefined, length: number | undefined }): Range {
|
||||
if (typeof span.start === 'undefined') {
|
||||
const pos = document.positionAt(0);
|
||||
return Range.create(pos, pos);
|
||||
}
|
||||
const startPosition = document.positionAt(span.start);
|
||||
const endPosition = document.positionAt(span.start + (span.length || 0));
|
||||
return Range.create(startPosition, endPosition);
|
||||
}
|
||||
|
||||
function convertKind(kind: string): CompletionItemKind {
|
||||
switch (kind) {
|
||||
case 'primitive type':
|
||||
case 'keyword':
|
||||
return CompletionItemKind.Keyword;
|
||||
case 'var':
|
||||
case 'local var':
|
||||
return CompletionItemKind.Variable;
|
||||
case 'property':
|
||||
case 'getter':
|
||||
case 'setter':
|
||||
return CompletionItemKind.Field;
|
||||
case 'function':
|
||||
case 'method':
|
||||
case 'construct':
|
||||
case 'call':
|
||||
case 'index':
|
||||
return CompletionItemKind.Function;
|
||||
case 'enum':
|
||||
return CompletionItemKind.Enum;
|
||||
case 'module':
|
||||
return CompletionItemKind.Module;
|
||||
case 'class':
|
||||
return CompletionItemKind.Class;
|
||||
case 'interface':
|
||||
return CompletionItemKind.Interface;
|
||||
case 'warning':
|
||||
return CompletionItemKind.File;
|
||||
}
|
||||
|
||||
return CompletionItemKind.Property;
|
||||
}
|
||||
|
||||
function convertSymbolKind(kind: string): SymbolKind {
|
||||
switch (kind) {
|
||||
case 'var':
|
||||
case 'local var':
|
||||
case 'const':
|
||||
return SymbolKind.Variable;
|
||||
case 'function':
|
||||
case 'local function':
|
||||
return SymbolKind.Function;
|
||||
case 'enum':
|
||||
return SymbolKind.Enum;
|
||||
case 'module':
|
||||
return SymbolKind.Module;
|
||||
case 'class':
|
||||
return SymbolKind.Class;
|
||||
case 'interface':
|
||||
return SymbolKind.Interface;
|
||||
case 'method':
|
||||
return SymbolKind.Method;
|
||||
case 'property':
|
||||
case 'getter':
|
||||
case 'setter':
|
||||
return SymbolKind.Property;
|
||||
}
|
||||
return SymbolKind.Variable;
|
||||
}
|
||||
|
||||
function convertOptions(options: FormattingOptions, formatSettings: any, initialIndentLevel: number): ts.FormatCodeOptions {
|
||||
return {
|
||||
ConvertTabsToSpaces: options.insertSpaces,
|
||||
TabSize: options.tabSize,
|
||||
IndentSize: options.tabSize,
|
||||
IndentStyle: ts.IndentStyle.Smart,
|
||||
NewLineCharacter: '\n',
|
||||
BaseIndentSize: options.tabSize * initialIndentLevel,
|
||||
InsertSpaceAfterCommaDelimiter: Boolean(!formatSettings || formatSettings.insertSpaceAfterCommaDelimiter),
|
||||
InsertSpaceAfterSemicolonInForStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterSemicolonInForStatements),
|
||||
InsertSpaceBeforeAndAfterBinaryOperators: Boolean(!formatSettings || formatSettings.insertSpaceBeforeAndAfterBinaryOperators),
|
||||
InsertSpaceAfterKeywordsInControlFlowStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterKeywordsInControlFlowStatements),
|
||||
InsertSpaceAfterFunctionKeywordForAnonymousFunctions: Boolean(!formatSettings || formatSettings.insertSpaceAfterFunctionKeywordForAnonymousFunctions),
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis),
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets),
|
||||
InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces),
|
||||
InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces),
|
||||
PlaceOpenBraceOnNewLineForControlBlocks: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForFunctions),
|
||||
PlaceOpenBraceOnNewLineForFunctions: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForControlBlocks)
|
||||
};
|
||||
}
|
||||
|
||||
function computeInitialIndent(document: TextDocument, range: Range, options: FormattingOptions) {
|
||||
let lineStart = document.offsetAt(Position.create(range.start.line, 0));
|
||||
let content = document.getText();
|
||||
|
||||
let i = lineStart;
|
||||
let nChars = 0;
|
||||
let tabSize = options.tabSize || 4;
|
||||
while (i < content.length) {
|
||||
let ch = content.charAt(i);
|
||||
if (ch === ' ') {
|
||||
nChars++;
|
||||
} else if (ch === '\t') {
|
||||
nChars += tabSize;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return Math.floor(nChars / tabSize);
|
||||
}
|
||||
|
||||
function generateIndent(level: number, options: FormattingOptions) {
|
||||
if (options.insertSpaces) {
|
||||
return repeat(' ', level * options.tabSize);
|
||||
} else {
|
||||
return repeat('\t', level);
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TextDocument, SemanticTokenData } from './languageModes';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export function getSemanticTokenLegend() {
|
||||
if (tokenTypes.length !== TokenType._) {
|
||||
console.warn('TokenType has added new entries.');
|
||||
}
|
||||
if (tokenModifiers.length !== TokenModifier._) {
|
||||
console.warn('TokenModifier has added new entries.');
|
||||
}
|
||||
return { types: tokenTypes, modifiers: tokenModifiers };
|
||||
}
|
||||
|
||||
export function getSemanticTokens(jsLanguageService: ts.LanguageService, currentTextDocument: TextDocument, fileName: string): SemanticTokenData[] {
|
||||
//https://ts-ast-viewer.com/#code/AQ0g2CmAuwGbALzAJwG4BQZQGNwEMBnQ4AQQEYBmYAb2C22zgEtJwATJVTRxgcwD27AQAp8AGmAAjAJS0A9POB8+7NQ168oscAJz5wANXwAnLug2bsJmAFcTAO2XAA1MHyvgu-UdOeWbOw8ViAAvpagocBAA
|
||||
|
||||
let resultTokens: SemanticTokenData[] = [];
|
||||
const collector = (node: ts.Node, typeIdx: number, modifierSet: number) => {
|
||||
resultTokens.push({ start: currentTextDocument.positionAt(node.getStart()), length: node.getWidth(), typeIdx, modifierSet });
|
||||
};
|
||||
collectTokens(jsLanguageService, fileName, { start: 0, length: currentTextDocument.getText().length }, collector);
|
||||
|
||||
return resultTokens;
|
||||
}
|
||||
|
||||
function collectTokens(jsLanguageService: ts.LanguageService, fileName: string, span: ts.TextSpan, collector: (node: ts.Node, tokenType: number, tokenModifier: number) => void) {
|
||||
|
||||
const program = jsLanguageService.getProgram();
|
||||
if (program) {
|
||||
const typeChecker = program.getTypeChecker();
|
||||
|
||||
function visit(node: ts.Node) {
|
||||
if (!node || !ts.textSpanIntersectsWith(span, node.pos, node.getFullWidth())) {
|
||||
return;
|
||||
}
|
||||
if (ts.isIdentifier(node)) {
|
||||
let symbol = typeChecker.getSymbolAtLocation(node);
|
||||
if (symbol) {
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) {
|
||||
symbol = typeChecker.getAliasedSymbol(symbol);
|
||||
}
|
||||
let typeIdx = classifySymbol(symbol);
|
||||
if (typeIdx !== undefined) {
|
||||
let modifierSet = 0;
|
||||
if (node.parent) {
|
||||
const parentTypeIdx = tokenFromDeclarationMapping[node.parent.kind];
|
||||
if (parentTypeIdx === typeIdx && (<ts.NamedDeclaration>node.parent).name === node) {
|
||||
modifierSet = 1 << TokenModifier.declaration;
|
||||
}
|
||||
}
|
||||
const decl = symbol.valueDeclaration;
|
||||
const modifiers = decl ? ts.getCombinedModifierFlags(decl) : 0;
|
||||
const nodeFlags = decl ? ts.getCombinedNodeFlags(decl) : 0;
|
||||
if (modifiers & ts.ModifierFlags.Static) {
|
||||
modifierSet |= 1 << TokenModifier.static;
|
||||
}
|
||||
if (modifiers & ts.ModifierFlags.Async) {
|
||||
modifierSet |= 1 << TokenModifier.async;
|
||||
}
|
||||
if ((modifiers & ts.ModifierFlags.Readonly) || (nodeFlags & ts.NodeFlags.Const) || (symbol.getFlags() & ts.SymbolFlags.EnumMember)) {
|
||||
modifierSet |= 1 << TokenModifier.readonly;
|
||||
}
|
||||
collector(node, typeIdx, modifierSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
const sourceFile = program.getSourceFile(fileName);
|
||||
if (sourceFile) {
|
||||
visit(sourceFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function classifySymbol(symbol: ts.Symbol) {
|
||||
const flags = symbol.getFlags();
|
||||
if (flags & ts.SymbolFlags.Class) {
|
||||
return TokenType.class;
|
||||
} else if (flags & ts.SymbolFlags.Enum) {
|
||||
return TokenType.enum;
|
||||
} else if (flags & ts.SymbolFlags.TypeAlias) {
|
||||
return TokenType.type;
|
||||
} else if (flags & ts.SymbolFlags.Type) {
|
||||
if (flags & ts.SymbolFlags.Interface) {
|
||||
return TokenType.interface;
|
||||
} if (flags & ts.SymbolFlags.TypeParameter) {
|
||||
return TokenType.typeParameter;
|
||||
}
|
||||
}
|
||||
const decl = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0];
|
||||
return decl && tokenFromDeclarationMapping[decl.kind];
|
||||
}
|
||||
|
||||
export const enum TokenType {
|
||||
class, enum, interface, namespace, typeParameter, type, parameter, variable, property, function, member, _
|
||||
}
|
||||
|
||||
export const enum TokenModifier {
|
||||
declaration, static, async, readonly, _
|
||||
}
|
||||
|
||||
const tokenTypes: string[] = [];
|
||||
tokenTypes[TokenType.class] = 'class';
|
||||
tokenTypes[TokenType.enum] = 'enum';
|
||||
tokenTypes[TokenType.interface] = 'interface';
|
||||
tokenTypes[TokenType.namespace] = 'namespace';
|
||||
tokenTypes[TokenType.typeParameter] = 'typeParameter';
|
||||
tokenTypes[TokenType.type] = 'type';
|
||||
tokenTypes[TokenType.parameter] = 'parameter';
|
||||
tokenTypes[TokenType.variable] = 'variable';
|
||||
tokenTypes[TokenType.property] = 'property';
|
||||
tokenTypes[TokenType.function] = 'function';
|
||||
tokenTypes[TokenType.member] = 'member';
|
||||
|
||||
const tokenModifiers: string[] = [];
|
||||
tokenModifiers[TokenModifier.async] = 'async';
|
||||
tokenModifiers[TokenModifier.declaration] = 'declaration';
|
||||
tokenModifiers[TokenModifier.readonly] = 'readonly';
|
||||
tokenModifiers[TokenModifier.static] = 'static';
|
||||
|
||||
const tokenFromDeclarationMapping: { [name: string]: TokenType } = {
|
||||
[ts.SyntaxKind.VariableDeclaration]: TokenType.variable,
|
||||
[ts.SyntaxKind.Parameter]: TokenType.parameter,
|
||||
[ts.SyntaxKind.PropertyDeclaration]: TokenType.property,
|
||||
[ts.SyntaxKind.ModuleDeclaration]: TokenType.namespace,
|
||||
[ts.SyntaxKind.EnumDeclaration]: TokenType.enum,
|
||||
[ts.SyntaxKind.EnumMember]: TokenType.property,
|
||||
[ts.SyntaxKind.ClassDeclaration]: TokenType.class,
|
||||
[ts.SyntaxKind.MethodDeclaration]: TokenType.member,
|
||||
[ts.SyntaxKind.FunctionDeclaration]: TokenType.function,
|
||||
[ts.SyntaxKind.MethodSignature]: TokenType.member,
|
||||
[ts.SyntaxKind.GetAccessor]: TokenType.property,
|
||||
[ts.SyntaxKind.PropertySignature]: TokenType.property,
|
||||
[ts.SyntaxKind.InterfaceDeclaration]: TokenType.interface,
|
||||
[ts.SyntaxKind.TypeAliasDeclaration]: TokenType.type,
|
||||
[ts.SyntaxKind.TypeParameter]: TokenType.typeParameter
|
||||
};
|
@ -0,0 +1,162 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 {
|
||||
ClientCapabilities, DocumentContext, getLanguageService as getHTMLLanguageService, IHTMLDataProvider, SelectionRange,
|
||||
CompletionItem, CompletionList, Definition, Diagnostic, DocumentHighlight, DocumentLink, FoldingRange, FormattingOptions,
|
||||
Hover, Location, Position, Range, SignatureHelp, SymbolInformation, TextDocument, TextEdit,
|
||||
Color, ColorInformation, ColorPresentation, WorkspaceEdit
|
||||
} from 'vscode-html-languageservice';
|
||||
import { WorkspaceFolder } from 'vscode-languageserver';
|
||||
import { getLanguageModelCache, LanguageModelCache } from '../languageModelCache';
|
||||
import { getCSSMode } from './cssMode';
|
||||
import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport';
|
||||
import { getHTMLMode } from './htmlMode';
|
||||
import { getJavaScriptMode } from './javascriptMode';
|
||||
import { RequestService } from '../requests';
|
||||
|
||||
export * from 'vscode-html-languageservice';
|
||||
export { WorkspaceFolder } from 'vscode-languageserver';
|
||||
|
||||
export interface Settings {
|
||||
css?: any;
|
||||
html?: any;
|
||||
javascript?: any;
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
readonly settings: Settings;
|
||||
readonly folders: WorkspaceFolder[];
|
||||
}
|
||||
|
||||
export interface SemanticTokenData {
|
||||
start: Position;
|
||||
length: number;
|
||||
typeIdx: number;
|
||||
modifierSet: number;
|
||||
}
|
||||
|
||||
export interface LanguageMode {
|
||||
getId(): string;
|
||||
getSelectionRange?: (document: TextDocument, position: Position) => Promise<SelectionRange>;
|
||||
doValidation?: (document: TextDocument, settings?: Settings) => Promise<Diagnostic[]>;
|
||||
doComplete?: (document: TextDocument, position: Position, documentContext: DocumentContext, settings?: Settings) => Promise<CompletionList>;
|
||||
doResolve?: (document: TextDocument, item: CompletionItem) => Promise<CompletionItem>;
|
||||
doHover?: (document: TextDocument, position: Position) => Promise<Hover | null>;
|
||||
doSignatureHelp?: (document: TextDocument, position: Position) => Promise<SignatureHelp | null>;
|
||||
doRename?: (document: TextDocument, position: Position, newName: string) => Promise<WorkspaceEdit | null>;
|
||||
doOnTypeRename?: (document: TextDocument, position: Position) => Promise<Range[] | null>;
|
||||
findDocumentHighlight?: (document: TextDocument, position: Position) => Promise<DocumentHighlight[]>;
|
||||
findDocumentSymbols?: (document: TextDocument) => Promise<SymbolInformation[]>;
|
||||
findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext) => Promise<DocumentLink[]>;
|
||||
findDefinition?: (document: TextDocument, position: Position) => Promise<Definition | null>;
|
||||
findReferences?: (document: TextDocument, position: Position) => Promise<Location[]>;
|
||||
format?: (document: TextDocument, range: Range, options: FormattingOptions, settings?: Settings) => Promise<TextEdit[]>;
|
||||
findDocumentColors?: (document: TextDocument) => Promise<ColorInformation[]>;
|
||||
getColorPresentations?: (document: TextDocument, color: Color, range: Range) => Promise<ColorPresentation[]>;
|
||||
doAutoClose?: (document: TextDocument, position: Position) => Promise<string | null>;
|
||||
findMatchingTagPosition?: (document: TextDocument, position: Position) => Promise<Position | null>;
|
||||
getFoldingRanges?: (document: TextDocument) => Promise<FoldingRange[]>;
|
||||
onDocumentRemoved(document: TextDocument): void;
|
||||
getSemanticTokens?(document: TextDocument): Promise<SemanticTokenData[]>;
|
||||
getSemanticTokenLegend?(): { types: string[], modifiers: string[] };
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface LanguageModes {
|
||||
updateDataProviders(dataProviders: IHTMLDataProvider[]): void;
|
||||
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(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, requestService: RequestService): LanguageModes {
|
||||
const htmlLanguageService = getHTMLLanguageService({ clientCapabilities, fileSystemProvider: requestService });
|
||||
const cssLanguageService = getCSSLanguageService({ clientCapabilities, fileSystemProvider: requestService });
|
||||
|
||||
let documentRegions = getLanguageModelCache<HTMLDocumentRegions>(10, 60, document => getDocumentRegions(htmlLanguageService, document));
|
||||
|
||||
let modelCaches: LanguageModelCache<any>[] = [];
|
||||
modelCaches.push(documentRegions);
|
||||
|
||||
let modes = Object.create(null);
|
||||
modes['html'] = getHTMLMode(htmlLanguageService, workspace);
|
||||
if (supportedLanguages['css']) {
|
||||
modes['css'] = getCSSMode(cssLanguageService, documentRegions, workspace);
|
||||
}
|
||||
if (supportedLanguages['javascript']) {
|
||||
modes['javascript'] = getJavaScriptMode(documentRegions, 'javascript', workspace);
|
||||
modes['typescript'] = getJavaScriptMode(documentRegions, 'typescript', workspace);
|
||||
}
|
||||
return {
|
||||
async updateDataProviders(dataProviders: IHTMLDataProvider[]): Promise<void> {
|
||||
htmlLanguageService.setDataProviders(true, dataProviders);
|
||||
},
|
||||
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined {
|
||||
let 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 <LanguageModeRange>{
|
||||
start: r.start,
|
||||
end: r.end,
|
||||
mode: r.languageId && modes[r.languageId],
|
||||
attributeValue: r.attributeValue
|
||||
};
|
||||
});
|
||||
},
|
||||
getAllModesInDocument(document: TextDocument): LanguageMode[] {
|
||||
let result = [];
|
||||
for (let languageId of documentRegions.get(document).getLanguagesInDocument()) {
|
||||
let mode = modes[languageId];
|
||||
if (mode) {
|
||||
result.push(mode);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
getAllModes(): LanguageMode[] {
|
||||
let result = [];
|
||||
for (let languageId in modes) {
|
||||
let 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 (let mode in modes) {
|
||||
modes[mode].onDocumentRemoved(document);
|
||||
}
|
||||
},
|
||||
dispose(): void {
|
||||
modelCaches.forEach(mc => mc.dispose());
|
||||
modelCaches = [];
|
||||
for (let mode in modes) {
|
||||
modes[mode].dispose();
|
||||
}
|
||||
modes = {};
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LanguageModes, TextDocument, Position, Range, SelectionRange } from './languageModes';
|
||||
import { insideRangeButNotSame } from '../utils/positions';
|
||||
|
||||
export async function getSelectionRanges(languageModes: LanguageModes, document: TextDocument, positions: Position[]) {
|
||||
const htmlMode = languageModes.getMode('html');
|
||||
return Promise.all(positions.map(async position => {
|
||||
const htmlRange = await htmlMode!.getSelectionRange!(document, position);
|
||||
const mode = languageModes.getModeAtPosition(document, position);
|
||||
if (mode && mode.getSelectionRange) {
|
||||
let range = await mode.getSelectionRange(document, position);
|
||||
let top = range;
|
||||
while (top.parent && insideRangeButNotSame(htmlRange.range, top.parent.range)) {
|
||||
top = top.parent;
|
||||
}
|
||||
top.parent = htmlRange;
|
||||
return range;
|
||||
}
|
||||
return htmlRange || SelectionRange.create(Range.create(position, position));
|
||||
}));
|
||||
}
|
||||
|
@ -0,0 +1,139 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SemanticTokenData, Range, TextDocument, LanguageModes, Position } from './languageModes';
|
||||
import { beforeOrSame } from '../utils/positions';
|
||||
|
||||
interface LegendMapping {
|
||||
types: number[] | undefined;
|
||||
modifiers: number[] | undefined;
|
||||
}
|
||||
|
||||
export interface SemanticTokenProvider {
|
||||
readonly legend: { types: string[]; modifiers: string[] };
|
||||
getSemanticTokens(document: TextDocument, ranges?: Range[]): Promise<number[]>;
|
||||
}
|
||||
|
||||
|
||||
export function newSemanticTokenProvider(languageModes: LanguageModes): SemanticTokenProvider {
|
||||
|
||||
// combined legend across modes
|
||||
const legend: { types: string[], modifiers: string[] } = { types: [], modifiers: [] };
|
||||
const legendMappings: { [modeId: string]: LegendMapping } = {};
|
||||
|
||||
for (let mode of languageModes.getAllModes()) {
|
||||
if (mode.getSemanticTokenLegend && mode.getSemanticTokens) {
|
||||
const modeLegend = mode.getSemanticTokenLegend();
|
||||
legendMappings[mode.getId()] = { types: createMapping(modeLegend.types, legend.types), modifiers: createMapping(modeLegend.modifiers, legend.modifiers) };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
legend,
|
||||
async getSemanticTokens(document: TextDocument, ranges?: Range[]): Promise<number[]> {
|
||||
const allTokens: SemanticTokenData[] = [];
|
||||
for (let mode of languageModes.getAllModesInDocument(document)) {
|
||||
if (mode.getSemanticTokens) {
|
||||
const mapping = legendMappings[mode.getId()];
|
||||
const tokens = await mode.getSemanticTokens(document);
|
||||
applyTypesMapping(tokens, mapping.types);
|
||||
applyModifiersMapping(tokens, mapping.modifiers);
|
||||
for (let token of tokens) {
|
||||
allTokens.push(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
return encodeTokens(allTokens, ranges);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createMapping(origLegend: string[], newLegend: string[]): number[] | undefined {
|
||||
const mapping: number[] = [];
|
||||
let needsMapping = false;
|
||||
for (let origIndex = 0; origIndex < origLegend.length; origIndex++) {
|
||||
const entry = origLegend[origIndex];
|
||||
let newIndex = newLegend.indexOf(entry);
|
||||
if (newIndex === -1) {
|
||||
newIndex = newLegend.length;
|
||||
newLegend.push(entry);
|
||||
}
|
||||
mapping.push(newIndex);
|
||||
needsMapping = needsMapping || (newIndex !== origIndex);
|
||||
}
|
||||
return needsMapping ? mapping : undefined;
|
||||
}
|
||||
|
||||
function applyTypesMapping(tokens: SemanticTokenData[], typesMapping: number[] | undefined): void {
|
||||
if (typesMapping) {
|
||||
for (let token of tokens) {
|
||||
token.typeIdx = typesMapping[token.typeIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyModifiersMapping(tokens: SemanticTokenData[], modifiersMapping: number[] | undefined): void {
|
||||
if (modifiersMapping) {
|
||||
for (let token of tokens) {
|
||||
let modifierSet = token.modifierSet;
|
||||
if (modifierSet) {
|
||||
let index = 0;
|
||||
let result = 0;
|
||||
while (modifierSet > 0) {
|
||||
if ((modifierSet & 1) !== 0) {
|
||||
result = result + (1 << modifiersMapping[index]);
|
||||
}
|
||||
index++;
|
||||
modifierSet = modifierSet >> 1;
|
||||
}
|
||||
token.modifierSet = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fullRange = [Range.create(Position.create(0, 0), Position.create(Number.MAX_VALUE, 0))];
|
||||
|
||||
function encodeTokens(tokens: SemanticTokenData[], ranges?: Range[]): number[] {
|
||||
|
||||
const resultTokens = tokens.sort((d1, d2) => d1.start.line - d2.start.line || d1.start.character - d2.start.character);
|
||||
if (ranges) {
|
||||
ranges = ranges.sort((d1, d2) => d1.start.line - d2.start.line || d1.start.character - d2.start.character);
|
||||
} else {
|
||||
ranges = fullRange;
|
||||
}
|
||||
|
||||
let rangeIndex = 0;
|
||||
let currRange = ranges[rangeIndex++];
|
||||
|
||||
let prefLine = 0;
|
||||
let prevChar = 0;
|
||||
|
||||
let encodedResult: number[] = [];
|
||||
|
||||
for (let k = 0; k < resultTokens.length && currRange; k++) {
|
||||
const curr = resultTokens[k];
|
||||
const start = curr.start;
|
||||
while (currRange && beforeOrSame(currRange.end, start)) {
|
||||
currRange = ranges[rangeIndex++];
|
||||
}
|
||||
if (currRange && beforeOrSame(currRange.start, start) && beforeOrSame({ line: start.line, character: start.character + curr.length }, currRange.end)) {
|
||||
// token inside a range
|
||||
|
||||
if (prefLine !== start.line) {
|
||||
prevChar = 0;
|
||||
}
|
||||
encodedResult.push(start.line - prefLine); // line delta
|
||||
encodedResult.push(start.character - prevChar); // line delta
|
||||
encodedResult.push(curr.length); // length
|
||||
encodedResult.push(curr.typeIdx); // tokenType
|
||||
encodedResult.push(curr.modifierSet); // tokenModifier
|
||||
|
||||
prefLine = start.line;
|
||||
prevChar = start.character;
|
||||
}
|
||||
}
|
||||
return encodedResult;
|
||||
}
|
Reference in New Issue
Block a user