Archived
1
0

Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@ -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();
}
};
}

View File

@ -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';
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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
};

View File

@ -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 = {};
}
};
}

View File

@ -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));
}));
}

View File

@ -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;
}