Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createConnection, BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver/browser';
|
||||
import { startServer } from '../htmlServer';
|
||||
|
||||
declare let self: any;
|
||||
|
||||
const messageReader = new BrowserMessageReader(self);
|
||||
const messageWriter = new BrowserMessageWriter(self);
|
||||
|
||||
const connection = createConnection(messageReader, messageWriter);
|
||||
|
||||
startServer(connection, {});
|
@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IHTMLDataProvider, newHTMLDataProvider } from 'vscode-html-languageservice';
|
||||
import { RequestService } from './requests';
|
||||
|
||||
export function fetchHTMLDataProviders(dataPaths: string[], requestService: RequestService): Promise<IHTMLDataProvider[]> {
|
||||
const providers = dataPaths.map(async p => {
|
||||
try {
|
||||
const content = await requestService.getContent(p);
|
||||
return parseHTMLData(p, content);
|
||||
} catch (e) {
|
||||
return newHTMLDataProvider(p, { version: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(providers);
|
||||
}
|
||||
|
||||
function parseHTMLData(id: string, source: string): IHTMLDataProvider {
|
||||
let rawData: any;
|
||||
|
||||
try {
|
||||
rawData = JSON.parse(source);
|
||||
} catch (err) {
|
||||
return newHTMLDataProvider(id, { version: 1 });
|
||||
}
|
||||
|
||||
return newHTMLDataProvider(id, {
|
||||
version: rawData.version || 1,
|
||||
tags: rawData.tags || [],
|
||||
globalAttributes: rawData.globalAttributes || [],
|
||||
valueSets: rawData.valueSets || []
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,559 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
Connection, TextDocuments, InitializeParams, InitializeResult, RequestType,
|
||||
DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities,
|
||||
ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification,
|
||||
DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind, NotificationType
|
||||
} from 'vscode-languageserver';
|
||||
import {
|
||||
getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation,
|
||||
Range, DocumentLink, SymbolInformation, TextDocumentIdentifier
|
||||
} from './modes/languageModes';
|
||||
|
||||
import { format } from './modes/formatting';
|
||||
import { pushAll } from './utils/arrays';
|
||||
import { getDocumentContext } from './utils/documentContext';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { formatError, runSafe } from './utils/runner';
|
||||
|
||||
import { getFoldingRanges } from './modes/htmlFolding';
|
||||
import { fetchHTMLDataProviders } from './customData';
|
||||
import { getSelectionRanges } from './modes/selectionRanges';
|
||||
import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens';
|
||||
import { RequestService, getRequestService } from './requests';
|
||||
|
||||
namespace CustomDataChangedNotification {
|
||||
export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged');
|
||||
}
|
||||
|
||||
namespace TagCloseRequest {
|
||||
export const type: RequestType<TextDocumentPositionParams, string | null, any, any> = new RequestType('html/tag');
|
||||
}
|
||||
namespace OnTypeRenameRequest {
|
||||
export const type: RequestType<TextDocumentPositionParams, Range[] | null, any, any> = new RequestType('html/onTypeRename');
|
||||
}
|
||||
|
||||
// experimental: semantic tokens
|
||||
interface SemanticTokenParams {
|
||||
textDocument: TextDocumentIdentifier;
|
||||
ranges?: Range[];
|
||||
}
|
||||
namespace SemanticTokenRequest {
|
||||
export const type: RequestType<SemanticTokenParams, number[] | null, any, any> = new RequestType('html/semanticTokens');
|
||||
}
|
||||
namespace SemanticTokenLegendRequest {
|
||||
export const type: RequestType<void, { types: string[]; modifiers: string[] } | null, any, any> = new RequestType('html/semanticTokenLegend');
|
||||
}
|
||||
|
||||
export interface RuntimeEnvironment {
|
||||
file?: RequestService;
|
||||
http?: RequestService
|
||||
configureHttpRequests?(proxy: string, strictSSL: boolean): void;
|
||||
}
|
||||
|
||||
export function startServer(connection: Connection, runtime: RuntimeEnvironment) {
|
||||
|
||||
// Create a text document manager.
|
||||
const documents = new TextDocuments(TextDocument);
|
||||
// Make the text document manager listen on the connection
|
||||
// for open, change and close text document events
|
||||
documents.listen(connection);
|
||||
|
||||
let workspaceFolders: WorkspaceFolder[] = [];
|
||||
|
||||
let languageModes: LanguageModes;
|
||||
|
||||
let clientSnippetSupport = false;
|
||||
let dynamicFormatterRegistration = false;
|
||||
let scopedSettingsSupport = false;
|
||||
let workspaceFoldersSupport = false;
|
||||
let foldingRangeLimit = Number.MAX_VALUE;
|
||||
|
||||
const notReady = () => Promise.reject('Not Ready');
|
||||
let requestService: RequestService = { getContent: notReady, stat: notReady, readDirectory: notReady };
|
||||
|
||||
|
||||
|
||||
let globalSettings: Settings = {};
|
||||
let documentSettings: { [key: string]: Thenable<Settings> } = {};
|
||||
// remove document settings on close
|
||||
documents.onDidClose(e => {
|
||||
delete documentSettings[e.document.uri];
|
||||
});
|
||||
|
||||
function getDocumentSettings(textDocument: TextDocument, needsDocumentSettings: () => boolean): Thenable<Settings | undefined> {
|
||||
if (scopedSettingsSupport && needsDocumentSettings()) {
|
||||
let promise = documentSettings[textDocument.uri];
|
||||
if (!promise) {
|
||||
const scopeUri = textDocument.uri;
|
||||
const configRequestParam: ConfigurationParams = { items: [{ scopeUri, section: 'css' }, { scopeUri, section: 'html' }, { scopeUri, section: 'javascript' }] };
|
||||
promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => ({ css: s[0], html: s[1], javascript: s[2] }));
|
||||
documentSettings[textDocument.uri] = promise;
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// After the server has started the client sends an initialize request. The server receives
|
||||
// in the passed params the rootPath of the workspace plus the client capabilities
|
||||
connection.onInitialize((params: InitializeParams): InitializeResult => {
|
||||
const initializationOptions = params.initializationOptions;
|
||||
|
||||
workspaceFolders = (<any>params).workspaceFolders;
|
||||
if (!Array.isArray(workspaceFolders)) {
|
||||
workspaceFolders = [];
|
||||
if (params.rootPath) {
|
||||
workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() });
|
||||
}
|
||||
}
|
||||
|
||||
requestService = getRequestService(initializationOptions?.handledSchemas || ['file'], connection, runtime);
|
||||
|
||||
const workspace = {
|
||||
get settings() { return globalSettings; },
|
||||
get folders() { return workspaceFolders; }
|
||||
};
|
||||
|
||||
languageModes = getLanguageModes(initializationOptions?.embeddedLanguages || { css: true, javascript: true }, workspace, params.capabilities, requestService);
|
||||
|
||||
const dataPaths: string[] = initializationOptions?.dataPaths || [];
|
||||
fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => {
|
||||
languageModes.updateDataProviders(dataProviders);
|
||||
});
|
||||
|
||||
documents.onDidClose(e => {
|
||||
languageModes.onDocumentRemoved(e.document);
|
||||
});
|
||||
connection.onShutdown(() => {
|
||||
languageModes.dispose();
|
||||
});
|
||||
|
||||
function getClientCapability<T>(name: string, def: T) {
|
||||
const keys = name.split('.');
|
||||
let c: any = params.capabilities;
|
||||
for (let i = 0; c && i < keys.length; i++) {
|
||||
if (!c.hasOwnProperty(keys[i])) {
|
||||
return def;
|
||||
}
|
||||
c = c[keys[i]];
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false);
|
||||
dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof initializationOptions?.provideFormatter !== 'boolean');
|
||||
scopedSettingsSupport = getClientCapability('workspace.configuration', false);
|
||||
workspaceFoldersSupport = getClientCapability('workspace.workspaceFolders', false);
|
||||
foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE);
|
||||
const capabilities: ServerCapabilities = {
|
||||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||
completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined,
|
||||
hoverProvider: true,
|
||||
documentHighlightProvider: true,
|
||||
documentRangeFormattingProvider: initializationOptions?.provideFormatter === true,
|
||||
documentLinkProvider: { resolveProvider: false },
|
||||
documentSymbolProvider: true,
|
||||
definitionProvider: true,
|
||||
signatureHelpProvider: { triggerCharacters: ['('] },
|
||||
referencesProvider: true,
|
||||
colorProvider: {},
|
||||
foldingRangeProvider: true,
|
||||
selectionRangeProvider: true,
|
||||
renameProvider: true
|
||||
};
|
||||
return { capabilities };
|
||||
});
|
||||
|
||||
connection.onInitialized(() => {
|
||||
if (workspaceFoldersSupport) {
|
||||
connection.client.register(DidChangeWorkspaceFoldersNotification.type);
|
||||
|
||||
connection.onNotification(DidChangeWorkspaceFoldersNotification.type, e => {
|
||||
const toAdd = e.event.added;
|
||||
const toRemove = e.event.removed;
|
||||
const updatedFolders = [];
|
||||
if (workspaceFolders) {
|
||||
for (const folder of workspaceFolders) {
|
||||
if (!toRemove.some(r => r.uri === folder.uri) && !toAdd.some(r => r.uri === folder.uri)) {
|
||||
updatedFolders.push(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
workspaceFolders = updatedFolders.concat(toAdd);
|
||||
documents.all().forEach(triggerValidation);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let formatterRegistration: Thenable<Disposable> | null = null;
|
||||
|
||||
// The settings have changed. Is send on server activation as well.
|
||||
connection.onDidChangeConfiguration((change) => {
|
||||
globalSettings = change.settings;
|
||||
documentSettings = {}; // reset all document settings
|
||||
documents.all().forEach(triggerValidation);
|
||||
|
||||
// dynamically enable & disable the formatter
|
||||
if (dynamicFormatterRegistration) {
|
||||
const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable;
|
||||
if (enableFormatter) {
|
||||
if (!formatterRegistration) {
|
||||
const documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }];
|
||||
formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector });
|
||||
}
|
||||
} else if (formatterRegistration) {
|
||||
formatterRegistration.then(r => r.dispose());
|
||||
formatterRegistration = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {};
|
||||
const validationDelayMs = 500;
|
||||
|
||||
// The content of a text document has changed. This event is emitted
|
||||
// when the text document first opened or when its content has changed.
|
||||
documents.onDidChangeContent(change => {
|
||||
triggerValidation(change.document);
|
||||
});
|
||||
|
||||
// a document has closed: clear all diagnostics
|
||||
documents.onDidClose(event => {
|
||||
cleanPendingValidation(event.document);
|
||||
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
|
||||
});
|
||||
|
||||
function cleanPendingValidation(textDocument: TextDocument): void {
|
||||
const request = pendingValidationRequests[textDocument.uri];
|
||||
if (request) {
|
||||
clearTimeout(request);
|
||||
delete pendingValidationRequests[textDocument.uri];
|
||||
}
|
||||
}
|
||||
|
||||
function triggerValidation(textDocument: TextDocument): void {
|
||||
cleanPendingValidation(textDocument);
|
||||
pendingValidationRequests[textDocument.uri] = setTimeout(() => {
|
||||
delete pendingValidationRequests[textDocument.uri];
|
||||
validateTextDocument(textDocument);
|
||||
}, validationDelayMs);
|
||||
}
|
||||
|
||||
function isValidationEnabled(languageId: string, settings: Settings = globalSettings) {
|
||||
const validationSettings = settings && settings.html && settings.html.validate;
|
||||
if (validationSettings) {
|
||||
return languageId === 'css' && validationSettings.styles !== false || languageId === 'javascript' && validationSettings.scripts !== false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function validateTextDocument(textDocument: TextDocument) {
|
||||
try {
|
||||
const version = textDocument.version;
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
if (textDocument.languageId === 'html') {
|
||||
const modes = languageModes.getAllModesInDocument(textDocument);
|
||||
const settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation));
|
||||
const latestTextDocument = documents.get(textDocument.uri);
|
||||
if (latestTextDocument && latestTextDocument.version === version) { // check no new version has come in after in after the async op
|
||||
for (const mode of modes) {
|
||||
if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) {
|
||||
pushAll(diagnostics, await mode.doValidation(latestTextDocument, settings));
|
||||
}
|
||||
}
|
||||
connection.sendDiagnostics({ uri: latestTextDocument.uri, diagnostics });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e));
|
||||
}
|
||||
}
|
||||
|
||||
connection.onCompletion(async (textDocumentPosition, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
|
||||
if (!mode || !mode.doComplete) {
|
||||
return { isIncomplete: true, items: [] };
|
||||
}
|
||||
const doComplete = mode.doComplete!;
|
||||
|
||||
if (mode.getId() !== 'html') {
|
||||
/* __GDPR__
|
||||
"html.embbedded.complete" : {
|
||||
"languageId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } });
|
||||
}
|
||||
|
||||
const settings = await getDocumentSettings(document, () => doComplete.length > 2);
|
||||
const documentContext = getDocumentContext(document.uri, workspaceFolders);
|
||||
return doComplete(document, textDocumentPosition.position, documentContext, settings);
|
||||
|
||||
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onCompletionResolve((item, token) => {
|
||||
return runSafe(async () => {
|
||||
const data = item.data;
|
||||
if (data && data.languageId && data.uri) {
|
||||
const mode = languageModes.getMode(data.languageId);
|
||||
const document = documents.get(data.uri);
|
||||
if (mode && mode.doResolve && document) {
|
||||
return mode.doResolve(document, item);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}, item, `Error while resolving completion proposal`, token);
|
||||
});
|
||||
|
||||
connection.onHover((textDocumentPosition, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
|
||||
if (mode && mode.doHover) {
|
||||
return mode.doHover(document, textDocumentPosition.position);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentHighlight((documentHighlightParams, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(documentHighlightParams.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, documentHighlightParams.position);
|
||||
if (mode && mode.findDocumentHighlight) {
|
||||
return mode.findDocumentHighlight(document, documentHighlightParams.position);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDefinition((definitionParams, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(definitionParams.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, definitionParams.position);
|
||||
if (mode && mode.findDefinition) {
|
||||
return mode.findDefinition(document, definitionParams.position);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onReferences((referenceParams, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(referenceParams.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, referenceParams.position);
|
||||
if (mode && mode.findReferences) {
|
||||
return mode.findReferences(document, referenceParams.position);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onSignatureHelp((signatureHelpParms, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(signatureHelpParms.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, signatureHelpParms.position);
|
||||
if (mode && mode.doSignatureHelp) {
|
||||
return mode.doSignatureHelp(document, signatureHelpParms.position);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentRangeFormatting(async (formatParams, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(formatParams.textDocument.uri);
|
||||
if (document) {
|
||||
let settings = await getDocumentSettings(document, () => true);
|
||||
if (!settings) {
|
||||
settings = globalSettings;
|
||||
}
|
||||
const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || '';
|
||||
const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) };
|
||||
|
||||
return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes);
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentLinks((documentLinkParam, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(documentLinkParam.textDocument.uri);
|
||||
const links: DocumentLink[] = [];
|
||||
if (document) {
|
||||
const documentContext = getDocumentContext(document.uri, workspaceFolders);
|
||||
for (const m of languageModes.getAllModesInDocument(document)) {
|
||||
if (m.findDocumentLinks) {
|
||||
pushAll(links, await m.findDocumentLinks(document, documentContext));
|
||||
}
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}, [], `Error while document links for ${documentLinkParam.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentSymbol((documentSymbolParms, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(documentSymbolParms.textDocument.uri);
|
||||
const symbols: SymbolInformation[] = [];
|
||||
if (document) {
|
||||
for (const m of languageModes.getAllModesInDocument(document)) {
|
||||
if (m.findDocumentSymbols) {
|
||||
pushAll(symbols, await m.findDocumentSymbols(document));
|
||||
}
|
||||
}
|
||||
}
|
||||
return symbols;
|
||||
}, [], `Error while computing document symbols for ${documentSymbolParms.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(DocumentColorRequest.type, (params, token) => {
|
||||
return runSafe(async () => {
|
||||
const infos: ColorInformation[] = [];
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
for (const m of languageModes.getAllModesInDocument(document)) {
|
||||
if (m.findDocumentColors) {
|
||||
pushAll(infos, await m.findDocumentColors(document));
|
||||
}
|
||||
}
|
||||
}
|
||||
return infos;
|
||||
}, [], `Error while computing document colors for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(ColorPresentationRequest.type, (params, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, params.range.start);
|
||||
if (mode && mode.getColorPresentations) {
|
||||
return mode.getColorPresentations(document, params.color, params.range);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing color presentations for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(TagCloseRequest.type, (params, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
const pos = params.position;
|
||||
if (pos.character > 0) {
|
||||
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
|
||||
if (mode && mode.doAutoClose) {
|
||||
return mode.doAutoClose(document, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing tag close actions for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onFoldingRanges((params, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return getFoldingRanges(languageModes, document, foldingRangeLimit, token);
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing folding regions for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onSelectionRanges((params, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return getSelectionRanges(languageModes, document, params.positions);
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRenameRequest((params, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
const position: Position = params.position;
|
||||
|
||||
if (document) {
|
||||
const htmlMode = languageModes.getMode('html');
|
||||
if (htmlMode && htmlMode.doRename) {
|
||||
return htmlMode.doRename(document, position, params.newName);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing rename for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(OnTypeRenameRequest.type, (params, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
const pos = params.position;
|
||||
if (pos.character > 0) {
|
||||
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
|
||||
if (mode && mode.doOnTypeRename) {
|
||||
return mode.doOnTypeRename(document, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing synced regions for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
let semanticTokensProvider: SemanticTokenProvider | undefined;
|
||||
function getSemanticTokenProvider() {
|
||||
if (!semanticTokensProvider) {
|
||||
semanticTokensProvider = newSemanticTokenProvider(languageModes);
|
||||
}
|
||||
return semanticTokensProvider;
|
||||
}
|
||||
|
||||
connection.onRequest(SemanticTokenRequest.type, (params, token) => {
|
||||
return runSafe(async () => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return getSemanticTokenProvider().getSemanticTokens(document, params.ranges);
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing semantic tokens for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(SemanticTokenLegendRequest.type, (_params, token) => {
|
||||
return runSafe(async () => {
|
||||
return getSemanticTokenProvider().legend;
|
||||
}, null, `Error while computing semantic tokens legend`, token);
|
||||
});
|
||||
|
||||
connection.onNotification(CustomDataChangedNotification.type, dataPaths => {
|
||||
fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => {
|
||||
languageModes.updateDataProviders(dataProviders);
|
||||
});
|
||||
});
|
||||
|
||||
// Listen on the connection
|
||||
connection.listen();
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TextDocument } from 'vscode-html-languageservice';
|
||||
|
||||
export interface LanguageModelCache<T> {
|
||||
get(document: TextDocument): T;
|
||||
onDocumentRemoved(document: TextDocument): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export function getLanguageModelCache<T>(maxEntries: number, cleanupIntervalTimeInSec: number, parse: (document: TextDocument) => T): LanguageModelCache<T> {
|
||||
let languageModels: { [uri: string]: { version: number, languageId: string, cTime: number, languageModel: T } } = {};
|
||||
let nModels = 0;
|
||||
|
||||
let cleanupInterval: NodeJS.Timer | undefined = undefined;
|
||||
if (cleanupIntervalTimeInSec > 0) {
|
||||
cleanupInterval = setInterval(() => {
|
||||
const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000;
|
||||
const uris = Object.keys(languageModels);
|
||||
for (const uri of uris) {
|
||||
const languageModelInfo = languageModels[uri];
|
||||
if (languageModelInfo.cTime < cutoffTime) {
|
||||
delete languageModels[uri];
|
||||
nModels--;
|
||||
}
|
||||
}
|
||||
}, cleanupIntervalTimeInSec * 1000);
|
||||
}
|
||||
|
||||
return {
|
||||
get(document: TextDocument): T {
|
||||
const version = document.version;
|
||||
const languageId = document.languageId;
|
||||
const languageModelInfo = languageModels[document.uri];
|
||||
if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) {
|
||||
languageModelInfo.cTime = Date.now();
|
||||
return languageModelInfo.languageModel;
|
||||
}
|
||||
const languageModel = parse(document);
|
||||
languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() };
|
||||
if (!languageModelInfo) {
|
||||
nModels++;
|
||||
}
|
||||
|
||||
if (nModels === maxEntries) {
|
||||
let oldestTime = Number.MAX_VALUE;
|
||||
let oldestUri = null;
|
||||
for (const uri in languageModels) {
|
||||
const languageModelInfo = languageModels[uri];
|
||||
if (languageModelInfo.cTime < oldestTime) {
|
||||
oldestUri = uri;
|
||||
oldestTime = languageModelInfo.cTime;
|
||||
}
|
||||
}
|
||||
if (oldestUri) {
|
||||
delete languageModels[oldestUri];
|
||||
nModels--;
|
||||
}
|
||||
}
|
||||
return languageModel;
|
||||
|
||||
},
|
||||
onDocumentRemoved(document: TextDocument) {
|
||||
const uri = document.uri;
|
||||
if (languageModels[uri]) {
|
||||
delete languageModels[uri];
|
||||
nModels--;
|
||||
}
|
||||
},
|
||||
dispose() {
|
||||
if (typeof cleanupInterval !== 'undefined') {
|
||||
clearInterval(cleanupInterval);
|
||||
cleanupInterval = undefined;
|
||||
languageModels = {};
|
||||
nModels = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createConnection, Connection } from 'vscode-languageserver/node';
|
||||
import { formatError } from '../utils/runner';
|
||||
import { startServer } from '../htmlServer';
|
||||
import { getNodeFSRequestService } from './nodeFs';
|
||||
|
||||
|
||||
// Create a connection for the server.
|
||||
const connection: Connection = createConnection();
|
||||
|
||||
console.log = connection.console.log.bind(connection.console);
|
||||
console.error = connection.console.error.bind(connection.console);
|
||||
|
||||
process.on('unhandledRejection', (e: any) => {
|
||||
connection.console.error(formatError(`Unhandled exception`, e));
|
||||
});
|
||||
|
||||
|
||||
startServer(connection, { file: getNodeFSRequestService() });
|
@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RequestService, getScheme } from '../requests';
|
||||
import { URI as Uri } from 'vscode-uri';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { FileType } from 'vscode-css-languageservice';
|
||||
|
||||
export function getNodeFSRequestService(): RequestService {
|
||||
function ensureFileUri(location: string) {
|
||||
if (getScheme(location) !== 'file') {
|
||||
throw new Error('fileRequestService can only handle file URLs');
|
||||
}
|
||||
}
|
||||
return {
|
||||
getContent(location: string, encoding?: string) {
|
||||
ensureFileUri(location);
|
||||
return new Promise((c, e) => {
|
||||
const uri = Uri.parse(location);
|
||||
fs.readFile(uri.fsPath, encoding, (err, buf) => {
|
||||
if (err) {
|
||||
return e(err);
|
||||
}
|
||||
c(buf.toString());
|
||||
|
||||
});
|
||||
});
|
||||
},
|
||||
stat(location: string) {
|
||||
ensureFileUri(location);
|
||||
return new Promise((c, e) => {
|
||||
const uri = Uri.parse(location);
|
||||
fs.stat(uri.fsPath, (err, stats) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 });
|
||||
} else {
|
||||
return e(err);
|
||||
}
|
||||
}
|
||||
|
||||
let type = FileType.Unknown;
|
||||
if (stats.isFile()) {
|
||||
type = FileType.File;
|
||||
} else if (stats.isDirectory()) {
|
||||
type = FileType.Directory;
|
||||
} else if (stats.isSymbolicLink()) {
|
||||
type = FileType.SymbolicLink;
|
||||
}
|
||||
|
||||
c({
|
||||
type,
|
||||
ctime: stats.ctime.getTime(),
|
||||
mtime: stats.mtime.getTime(),
|
||||
size: stats.size
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
readDirectory(location: string) {
|
||||
ensureFileUri(location);
|
||||
return new Promise((c, e) => {
|
||||
const path = Uri.parse(location).fsPath;
|
||||
|
||||
fs.readdir(path, { withFileTypes: true }, (err, children) => {
|
||||
if (err) {
|
||||
return e(err);
|
||||
}
|
||||
c(children.map(stat => {
|
||||
if (stat.isSymbolicLink()) {
|
||||
return [stat.name, FileType.SymbolicLink];
|
||||
} else if (stat.isDirectory()) {
|
||||
return [stat.name, FileType.Directory];
|
||||
} else if (stat.isFile()) {
|
||||
return [stat.name, FileType.File];
|
||||
} else {
|
||||
return [stat.name, FileType.Unknown];
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vscode-uri';
|
||||
import { RequestType, Connection } from 'vscode-languageserver';
|
||||
import { RuntimeEnvironment } from './htmlServer';
|
||||
|
||||
export namespace FsContentRequest {
|
||||
export const type: RequestType<{ uri: string; encoding?: string; }, string, any, any> = new RequestType('fs/content');
|
||||
}
|
||||
export namespace FsStatRequest {
|
||||
export const type: RequestType<string, FileStat, any, any> = new RequestType('fs/stat');
|
||||
}
|
||||
|
||||
export namespace FsReadDirRequest {
|
||||
export const type: RequestType<string, [string, FileType][], any, any> = new RequestType('fs/readDir');
|
||||
}
|
||||
|
||||
export enum FileType {
|
||||
/**
|
||||
* The file type is unknown.
|
||||
*/
|
||||
Unknown = 0,
|
||||
/**
|
||||
* A regular file.
|
||||
*/
|
||||
File = 1,
|
||||
/**
|
||||
* A directory.
|
||||
*/
|
||||
Directory = 2,
|
||||
/**
|
||||
* A symbolic link to a file.
|
||||
*/
|
||||
SymbolicLink = 64
|
||||
}
|
||||
export interface FileStat {
|
||||
/**
|
||||
* The type of the file, e.g. is a regular file, a directory, or symbolic link
|
||||
* to a file.
|
||||
*/
|
||||
type: FileType;
|
||||
/**
|
||||
* The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
|
||||
*/
|
||||
ctime: number;
|
||||
/**
|
||||
* The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
|
||||
*/
|
||||
mtime: number;
|
||||
/**
|
||||
* The size in bytes.
|
||||
*/
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface RequestService {
|
||||
getContent(uri: string, encoding?: string): Promise<string>;
|
||||
|
||||
stat(uri: string): Promise<FileStat>;
|
||||
readDirectory(uri: string): Promise<[string, FileType][]>;
|
||||
}
|
||||
|
||||
|
||||
export function getRequestService(handledSchemas: string[], connection: Connection, runtime: RuntimeEnvironment): RequestService {
|
||||
const builtInHandlers: { [protocol: string]: RequestService | undefined } = {};
|
||||
for (let protocol of handledSchemas) {
|
||||
if (protocol === 'file') {
|
||||
builtInHandlers[protocol] = runtime.file;
|
||||
} else if (protocol === 'http' || protocol === 'https') {
|
||||
builtInHandlers[protocol] = runtime.http;
|
||||
}
|
||||
}
|
||||
return {
|
||||
async stat(uri: string): Promise<FileStat> {
|
||||
const handler = builtInHandlers[getScheme(uri)];
|
||||
if (handler) {
|
||||
return handler.stat(uri);
|
||||
}
|
||||
const res = await connection.sendRequest(FsStatRequest.type, uri.toString());
|
||||
return res;
|
||||
},
|
||||
readDirectory(uri: string): Promise<[string, FileType][]> {
|
||||
const handler = builtInHandlers[getScheme(uri)];
|
||||
if (handler) {
|
||||
return handler.readDirectory(uri);
|
||||
}
|
||||
return connection.sendRequest(FsReadDirRequest.type, uri.toString());
|
||||
},
|
||||
getContent(uri: string, encoding?: string): Promise<string> {
|
||||
const handler = builtInHandlers[getScheme(uri)];
|
||||
if (handler) {
|
||||
return handler.getContent(uri, encoding);
|
||||
}
|
||||
return connection.sendRequest(FsContentRequest.type, { uri: uri.toString(), encoding });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getScheme(uri: string) {
|
||||
return uri.substr(0, uri.indexOf(':'));
|
||||
}
|
||||
|
||||
export function dirname(uri: string) {
|
||||
const lastIndexOfSlash = uri.lastIndexOf('/');
|
||||
return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : '';
|
||||
}
|
||||
|
||||
export function basename(uri: string) {
|
||||
const lastIndexOfSlash = uri.lastIndexOf('/');
|
||||
return uri.substr(lastIndexOfSlash + 1);
|
||||
}
|
||||
|
||||
|
||||
const Slash = '/'.charCodeAt(0);
|
||||
const Dot = '.'.charCodeAt(0);
|
||||
|
||||
export function extname(uri: string) {
|
||||
for (let i = uri.length - 1; i >= 0; i--) {
|
||||
const ch = uri.charCodeAt(i);
|
||||
if (ch === Dot) {
|
||||
if (i > 0 && uri.charCodeAt(i - 1) !== Slash) {
|
||||
return uri.substr(i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if (ch === Slash) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function isAbsolutePath(path: string) {
|
||||
return path.charCodeAt(0) === Slash;
|
||||
}
|
||||
|
||||
export function resolvePath(uriString: string, path: string): string {
|
||||
if (isAbsolutePath(path)) {
|
||||
const uri = URI.parse(uriString);
|
||||
const parts = path.split('/');
|
||||
return uri.with({ path: normalizePath(parts) }).toString();
|
||||
}
|
||||
return joinPath(uriString, path);
|
||||
}
|
||||
|
||||
export function normalizePath(parts: string[]): string {
|
||||
const newParts: string[] = [];
|
||||
for (const part of parts) {
|
||||
if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) {
|
||||
// ignore
|
||||
} else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) {
|
||||
newParts.pop();
|
||||
} else {
|
||||
newParts.push(part);
|
||||
}
|
||||
}
|
||||
if (parts.length > 1 && parts[parts.length - 1].length === 0) {
|
||||
newParts.push('');
|
||||
}
|
||||
let res = newParts.join('/');
|
||||
if (parts[0].length === 0) {
|
||||
res = '/' + res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function joinPath(uriString: string, ...paths: string[]): string {
|
||||
const uri = URI.parse(uriString);
|
||||
const parts = uri.path.split('/');
|
||||
for (let path of paths) {
|
||||
parts.push(...path.split('/'));
|
||||
}
|
||||
return uri.with({ path: normalizePath(parts) }).toString();
|
||||
}
|
@ -0,0 +1,319 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import * as path from 'path';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { getLanguageModes, WorkspaceFolder, TextDocument, CompletionList, CompletionItemKind, ClientCapabilities, TextEdit } from '../modes/languageModes';
|
||||
import { getNodeFSRequestService } from '../node/nodeFs';
|
||||
import { getDocumentContext } from '../utils/documentContext';
|
||||
export interface ItemDescription {
|
||||
label: string;
|
||||
documentation?: string;
|
||||
kind?: CompletionItemKind;
|
||||
resultText?: string;
|
||||
command?: { title: string, command: string };
|
||||
notAvailable?: boolean;
|
||||
}
|
||||
|
||||
export function assertCompletion(completions: CompletionList, expected: ItemDescription, document: TextDocument) {
|
||||
let matches = completions.items.filter(completion => {
|
||||
return completion.label === expected.label;
|
||||
});
|
||||
if (expected.notAvailable) {
|
||||
assert.equal(matches.length, 0, `${expected.label} should not existing is results`);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.equal(matches.length, 1, `${expected.label} should only existing once: Actual: ${completions.items.map(c => c.label).join(', ')}`);
|
||||
let match = matches[0];
|
||||
if (expected.documentation) {
|
||||
assert.equal(match.documentation, expected.documentation);
|
||||
}
|
||||
if (expected.kind) {
|
||||
assert.equal(match.kind, expected.kind);
|
||||
}
|
||||
if (expected.resultText && match.textEdit) {
|
||||
const edit = TextEdit.is(match.textEdit) ? match.textEdit : TextEdit.replace(match.textEdit.replace, match.textEdit.newText);
|
||||
assert.equal(TextDocument.applyEdits(document, [edit]), expected.resultText);
|
||||
}
|
||||
if (expected.command) {
|
||||
assert.deepEqual(match.command, expected.command);
|
||||
}
|
||||
}
|
||||
|
||||
const testUri = 'test://test/test.html';
|
||||
|
||||
export async function testCompletionFor(value: string, expected: { count?: number, items?: ItemDescription[] }, uri = testUri, workspaceFolders?: WorkspaceFolder[]): Promise<void> {
|
||||
let offset = value.indexOf('|');
|
||||
value = value.substr(0, offset) + value.substr(offset + 1);
|
||||
|
||||
let workspace = {
|
||||
settings: {},
|
||||
folders: workspaceFolders || [{ name: 'x', uri: uri.substr(0, uri.lastIndexOf('/')) }]
|
||||
};
|
||||
|
||||
let document = TextDocument.create(uri, 'html', 0, value);
|
||||
let position = document.positionAt(offset);
|
||||
const context = getDocumentContext(uri, workspace.folders);
|
||||
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
|
||||
const mode = languageModes.getModeAtPosition(document, position)!;
|
||||
|
||||
let list = await mode.doComplete!(document, position, context);
|
||||
|
||||
if (expected.count) {
|
||||
assert.equal(list.items.length, expected.count);
|
||||
}
|
||||
if (expected.items) {
|
||||
for (let item of expected.items) {
|
||||
assertCompletion(list, item, document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suite('HTML Completion', () => {
|
||||
test('HTML JavaScript Completions', async () => {
|
||||
await testCompletionFor('<html><script>window.|</script></html>', {
|
||||
items: [
|
||||
{ label: 'location', resultText: '<html><script>window.location</script></html>' },
|
||||
]
|
||||
});
|
||||
await testCompletionFor('<html><script>$.|</script></html>', {
|
||||
items: [
|
||||
{ label: 'getJSON', resultText: '<html><script>$.getJSON</script></html>' },
|
||||
]
|
||||
});
|
||||
await testCompletionFor('<html><script>const x = { a: 1 };</script><script>x.|</script></html>', {
|
||||
items: [
|
||||
{ label: 'a', resultText: '<html><script>const x = { a: 1 };</script><script>x.a</script></html>' },
|
||||
]
|
||||
}, 'test://test/test2.html');
|
||||
});
|
||||
});
|
||||
|
||||
suite('HTML Path Completion', () => {
|
||||
const triggerSuggestCommand = {
|
||||
title: 'Suggest',
|
||||
command: 'editor.action.triggerSuggest'
|
||||
};
|
||||
|
||||
const fixtureRoot = path.resolve(__dirname, '../../src/test/pathCompletionFixtures');
|
||||
const fixtureWorkspace = { name: 'fixture', uri: URI.file(fixtureRoot).toString() };
|
||||
const indexHtmlUri = URI.file(path.resolve(fixtureRoot, 'index.html')).toString();
|
||||
const aboutHtmlUri = URI.file(path.resolve(fixtureRoot, 'about/about.html')).toString();
|
||||
|
||||
test('Basics - Correct label/kind/result/command', async () => {
|
||||
await testCompletionFor('<script src="./|">', {
|
||||
items: [
|
||||
{ label: 'about/', kind: CompletionItemKind.Folder, resultText: '<script src="./about/">', command: triggerSuggestCommand },
|
||||
{ label: 'index.html', kind: CompletionItemKind.File, resultText: '<script src="./index.html">' },
|
||||
{ label: 'src/', kind: CompletionItemKind.Folder, resultText: '<script src="./src/">', command: triggerSuggestCommand }
|
||||
]
|
||||
}, indexHtmlUri);
|
||||
});
|
||||
|
||||
test('Basics - Single Quote', async () => {
|
||||
await testCompletionFor(`<script src='./|'>`, {
|
||||
items: [
|
||||
{ label: 'about/', kind: CompletionItemKind.Folder, resultText: `<script src='./about/'>`, command: triggerSuggestCommand },
|
||||
{ label: 'index.html', kind: CompletionItemKind.File, resultText: `<script src='./index.html'>` },
|
||||
{ label: 'src/', kind: CompletionItemKind.Folder, resultText: `<script src='./src/'>`, command: triggerSuggestCommand }
|
||||
]
|
||||
}, indexHtmlUri);
|
||||
});
|
||||
|
||||
test('No completion for remote paths', async () => {
|
||||
await testCompletionFor('<script src="http:">', { items: [] });
|
||||
await testCompletionFor('<script src="http:/|">', { items: [] });
|
||||
await testCompletionFor('<script src="http://|">', { items: [] });
|
||||
await testCompletionFor('<script src="https:|">', { items: [] });
|
||||
await testCompletionFor('<script src="https:/|">', { items: [] });
|
||||
await testCompletionFor('<script src="https://|">', { items: [] });
|
||||
await testCompletionFor('<script src="//|">', { items: [] });
|
||||
});
|
||||
|
||||
test('Relative Path', async () => {
|
||||
await testCompletionFor('<script src="../|">', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="../about/">' },
|
||||
{ label: 'index.html', resultText: '<script src="../index.html">' },
|
||||
{ label: 'src/', resultText: '<script src="../src/">' }
|
||||
]
|
||||
}, aboutHtmlUri);
|
||||
|
||||
await testCompletionFor('<script src="../src/|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="../src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="../src/test.js">' },
|
||||
]
|
||||
}, aboutHtmlUri);
|
||||
});
|
||||
|
||||
test('Absolute Path', async () => {
|
||||
await testCompletionFor('<script src="/|">', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="/about/">' },
|
||||
{ label: 'index.html', resultText: '<script src="/index.html">' },
|
||||
{ label: 'src/', resultText: '<script src="/src/">' },
|
||||
]
|
||||
}, indexHtmlUri);
|
||||
|
||||
await testCompletionFor('<script src="/src/|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="/src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="/src/test.js">' },
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
test('Empty Path Value', async () => {
|
||||
// document: index.html
|
||||
await testCompletionFor('<script src="|">', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="about/">' },
|
||||
{ label: 'index.html', resultText: '<script src="index.html">' },
|
||||
{ label: 'src/', resultText: '<script src="src/">' },
|
||||
]
|
||||
}, indexHtmlUri);
|
||||
// document: about.html
|
||||
await testCompletionFor('<script src="|">', {
|
||||
items: [
|
||||
{ label: 'about.css', resultText: '<script src="about.css">' },
|
||||
{ label: 'about.html', resultText: '<script src="about.html">' },
|
||||
{ label: 'media/', resultText: '<script src="media/">' },
|
||||
]
|
||||
}, aboutHtmlUri);
|
||||
});
|
||||
test('Incomplete Path', async () => {
|
||||
await testCompletionFor('<script src="/src/f|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="/src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="/src/test.js">' },
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
await testCompletionFor('<script src="../src/f|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="../src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="../src/test.js">' },
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
test('No leading dot or slash', async () => {
|
||||
// document: index.html
|
||||
await testCompletionFor('<script src="s|">', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="about/">' },
|
||||
{ label: 'index.html', resultText: '<script src="index.html">' },
|
||||
{ label: 'src/', resultText: '<script src="src/">' },
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
await testCompletionFor('<script src="src/|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="src/test.js">' },
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
await testCompletionFor('<script src="src/f|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="src/test.js">' },
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
// document: about.html
|
||||
await testCompletionFor('<script src="s|">', {
|
||||
items: [
|
||||
{ label: 'about.css', resultText: '<script src="about.css">' },
|
||||
{ label: 'about.html', resultText: '<script src="about.html">' },
|
||||
{ label: 'media/', resultText: '<script src="media/">' },
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
await testCompletionFor('<script src="media/|">', {
|
||||
items: [
|
||||
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
await testCompletionFor('<script src="media/f|">', {
|
||||
items: [
|
||||
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
test('Trigger completion in middle of path', async () => {
|
||||
// document: index.html
|
||||
await testCompletionFor('<script src="src/f|eature.js">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="src/test.js">' },
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
await testCompletionFor('<script src="s|rc/feature.js">', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="about/">' },
|
||||
{ label: 'index.html', resultText: '<script src="index.html">' },
|
||||
{ label: 'src/', resultText: '<script src="src/">' },
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
// document: about.html
|
||||
await testCompletionFor('<script src="media/f|eature.js">', {
|
||||
items: [
|
||||
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
await testCompletionFor('<script src="m|edia/feature.js">', {
|
||||
items: [
|
||||
{ label: 'about.css', resultText: '<script src="about.css">' },
|
||||
{ label: 'about.html', resultText: '<script src="about.html">' },
|
||||
{ label: 'media/', resultText: '<script src="media/">' },
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
|
||||
test('Trigger completion in middle of path and with whitespaces', async () => {
|
||||
await testCompletionFor('<script src="./| about/about.html>', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="./about/ about/about.html>' },
|
||||
{ label: 'index.html', resultText: '<script src="./index.html about/about.html>' },
|
||||
{ label: 'src/', resultText: '<script src="./src/ about/about.html>' },
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
await testCompletionFor('<script src="./a|bout /about.html>', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="./about/ /about.html>' },
|
||||
{ label: 'index.html', resultText: '<script src="./index.html /about.html>' },
|
||||
{ label: 'src/', resultText: '<script src="./src/ /about.html>' },
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
test('Completion should ignore files/folders starting with dot', async () => {
|
||||
await testCompletionFor('<script src="./|"', {
|
||||
count: 3
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
test('Unquoted Path', async () => {
|
||||
/* Unquoted value is not supported in html language service yet
|
||||
testCompletionFor(`<div><a href=about/|>`, {
|
||||
items: [
|
||||
{ label: 'about.html', resultText: `<div><a href=about/about.html>` }
|
||||
]
|
||||
}, testUri);
|
||||
*/
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { getDocumentContext } from '../utils/documentContext';
|
||||
|
||||
suite('HTML Document Context', () => {
|
||||
|
||||
test('Context', function (): any {
|
||||
const docURI = 'file:///users/test/folder/test.html';
|
||||
const rootFolders = [{ name: '', uri: 'file:///users/test/' }];
|
||||
|
||||
let context = getDocumentContext(docURI, rootFolders);
|
||||
assert.equal(context.resolveReference('/', docURI), 'file:///users/test/');
|
||||
assert.equal(context.resolveReference('/message.html', docURI), 'file:///users/test/message.html');
|
||||
assert.equal(context.resolveReference('message.html', docURI), 'file:///users/test/folder/message.html');
|
||||
assert.equal(context.resolveReference('message.html', 'file:///users/test/'), 'file:///users/test/message.html');
|
||||
});
|
||||
});
|
@ -0,0 +1,126 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import * as embeddedSupport from '../modes/embeddedSupport';
|
||||
import { getLanguageService } from 'vscode-html-languageservice';
|
||||
import { TextDocument } from '../modes/languageModes';
|
||||
|
||||
suite('HTML Embedded Support', () => {
|
||||
|
||||
const htmlLanguageService = getLanguageService();
|
||||
|
||||
function assertLanguageId(value: string, expectedLanguageId: string | undefined): void {
|
||||
const offset = value.indexOf('|');
|
||||
value = value.substr(0, offset) + value.substr(offset + 1);
|
||||
|
||||
const document = TextDocument.create('test://test/test.html', 'html', 0, value);
|
||||
|
||||
const position = document.positionAt(offset);
|
||||
|
||||
const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document);
|
||||
const languageId = docRegions.getLanguageAtPosition(position);
|
||||
|
||||
assert.equal(languageId, expectedLanguageId);
|
||||
}
|
||||
|
||||
function assertEmbeddedLanguageContent(value: string, languageId: string, expectedContent: string): void {
|
||||
const document = TextDocument.create('test://test/test.html', 'html', 0, value);
|
||||
|
||||
const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document);
|
||||
const content = docRegions.getEmbeddedDocument(languageId);
|
||||
assert.equal(content.getText(), expectedContent);
|
||||
}
|
||||
|
||||
test('Styles', function (): any {
|
||||
assertLanguageId('|<html><style>foo { }</style></html>', 'html');
|
||||
assertLanguageId('<html|><style>foo { }</style></html>', 'html');
|
||||
assertLanguageId('<html><st|yle>foo { }</style></html>', 'html');
|
||||
assertLanguageId('<html><style>|foo { }</style></html>', 'css');
|
||||
assertLanguageId('<html><style>foo| { }</style></html>', 'css');
|
||||
assertLanguageId('<html><style>foo { }|</style></html>', 'css');
|
||||
assertLanguageId('<html><style>foo { }</sty|le></html>', 'html');
|
||||
});
|
||||
|
||||
test('Styles - Incomplete HTML', function (): any {
|
||||
assertLanguageId('|<html><style>foo { }', 'html');
|
||||
assertLanguageId('<html><style>fo|o { }', 'css');
|
||||
assertLanguageId('<html><style>foo { }|', 'css');
|
||||
});
|
||||
|
||||
test('Style in attribute', function (): any {
|
||||
assertLanguageId('<div id="xy" |style="color: red"/>', 'html');
|
||||
assertLanguageId('<div id="xy" styl|e="color: red"/>', 'html');
|
||||
assertLanguageId('<div id="xy" style=|"color: red"/>', 'html');
|
||||
assertLanguageId('<div id="xy" style="|color: red"/>', 'css');
|
||||
assertLanguageId('<div id="xy" style="color|: red"/>', 'css');
|
||||
assertLanguageId('<div id="xy" style="color: red|"/>', 'css');
|
||||
assertLanguageId('<div id="xy" style="color: red"|/>', 'html');
|
||||
assertLanguageId('<div id="xy" style=\'color: r|ed\'/>', 'css');
|
||||
assertLanguageId('<div id="xy" style|=color:red/>', 'html');
|
||||
assertLanguageId('<div id="xy" style=|color:red/>', 'css');
|
||||
assertLanguageId('<div id="xy" style=color:r|ed/>', 'css');
|
||||
assertLanguageId('<div id="xy" style=color:red|/>', 'css');
|
||||
assertLanguageId('<div id="xy" style=color:red/|>', 'html');
|
||||
});
|
||||
|
||||
test('Style content', function (): any {
|
||||
assertEmbeddedLanguageContent('<html><style>foo { }</style></html>', 'css', ' foo { } ');
|
||||
assertEmbeddedLanguageContent('<html><script>var i = 0;</script></html>', 'css', ' ');
|
||||
assertEmbeddedLanguageContent('<html><style>foo { }</style>Hello<style>foo { }</style></html>', 'css', ' foo { } foo { } ');
|
||||
assertEmbeddedLanguageContent('<html>\n <style>\n foo { } \n </style>\n</html>\n', 'css', '\n \n foo { } \n \n\n');
|
||||
|
||||
assertEmbeddedLanguageContent('<div style="color: red"></div>', 'css', ' __{color: red} ');
|
||||
assertEmbeddedLanguageContent('<div style=color:red></div>', 'css', ' __{color:red} ');
|
||||
});
|
||||
|
||||
test('Scripts', function (): any {
|
||||
assertLanguageId('|<html><script>var i = 0;</script></html>', 'html');
|
||||
assertLanguageId('<html|><script>var i = 0;</script></html>', 'html');
|
||||
assertLanguageId('<html><scr|ipt>var i = 0;</script></html>', 'html');
|
||||
assertLanguageId('<html><script>|var i = 0;</script></html>', 'javascript');
|
||||
assertLanguageId('<html><script>var| i = 0;</script></html>', 'javascript');
|
||||
assertLanguageId('<html><script>var i = 0;|</script></html>', 'javascript');
|
||||
assertLanguageId('<html><script>var i = 0;</scr|ipt></html>', 'html');
|
||||
|
||||
assertLanguageId('<script type="text/javascript">var| i = 0;</script>', 'javascript');
|
||||
assertLanguageId('<script type="text/ecmascript">var| i = 0;</script>', 'javascript');
|
||||
assertLanguageId('<script type="application/javascript">var| i = 0;</script>', 'javascript');
|
||||
assertLanguageId('<script type="application/ecmascript">var| i = 0;</script>', 'javascript');
|
||||
assertLanguageId('<script type="application/typescript">var| i = 0;</script>', undefined);
|
||||
assertLanguageId('<script type=\'text/javascript\'>var| i = 0;</script>', 'javascript');
|
||||
});
|
||||
|
||||
test('Scripts in attribute', function (): any {
|
||||
assertLanguageId('<div |onKeyUp="foo()" onkeydown=\'bar()\'/>', 'html');
|
||||
assertLanguageId('<div onKeyUp=|"foo()" onkeydown=\'bar()\'/>', 'html');
|
||||
assertLanguageId('<div onKeyUp="|foo()" onkeydown=\'bar()\'/>', 'javascript');
|
||||
assertLanguageId('<div onKeyUp="foo(|)" onkeydown=\'bar()\'/>', 'javascript');
|
||||
assertLanguageId('<div onKeyUp="foo()|" onkeydown=\'bar()\'/>', 'javascript');
|
||||
assertLanguageId('<div onKeyUp="foo()"| onkeydown=\'bar()\'/>', 'html');
|
||||
assertLanguageId('<div onKeyUp="foo()" onkeydown=|\'bar()\'/>', 'html');
|
||||
assertLanguageId('<div onKeyUp="foo()" onkeydown=\'|bar()\'/>', 'javascript');
|
||||
assertLanguageId('<div onKeyUp="foo()" onkeydown=\'bar()|\'/>', 'javascript');
|
||||
assertLanguageId('<div onKeyUp="foo()" onkeydown=\'bar()\'|/>', 'html');
|
||||
|
||||
assertLanguageId('<DIV ONKEYUP|=foo()</DIV>', 'html');
|
||||
assertLanguageId('<DIV ONKEYUP=|foo()</DIV>', 'javascript');
|
||||
assertLanguageId('<DIV ONKEYUP=f|oo()</DIV>', 'javascript');
|
||||
assertLanguageId('<DIV ONKEYUP=foo(|)</DIV>', 'javascript');
|
||||
assertLanguageId('<DIV ONKEYUP=foo()|</DIV>', 'javascript');
|
||||
assertLanguageId('<DIV ONKEYUP=foo()<|/DIV>', 'html');
|
||||
|
||||
assertLanguageId('<label data-content="|Checkbox"/>', 'html');
|
||||
assertLanguageId('<label on="|Checkbox"/>', 'html');
|
||||
});
|
||||
|
||||
test('Script content', function (): any {
|
||||
assertEmbeddedLanguageContent('<html><script>var i = 0;</script></html>', 'javascript', ' var i = 0; ');
|
||||
assertEmbeddedLanguageContent('<script type="text/javascript">var i = 0;</script>', 'javascript', ' var i = 0; ');
|
||||
|
||||
assertEmbeddedLanguageContent('<div onKeyUp="foo()" onkeydown="bar()"/>', 'javascript', ' foo(); bar(); ');
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
Polymer({
|
||||
is: "chat-messages",
|
||||
properties: {
|
||||
user: {},
|
||||
friend: {
|
||||
observer: "_friendChanged"
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
|
||||
</html>
|
22
lib/vscode/extensions/html-language-features/server/src/test/fixtures/expected/19813-tab.html
vendored
Normal file
22
lib/vscode/extensions/html-language-features/server/src/test/fixtures/expected/19813-tab.html
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
Polymer({
|
||||
is: "chat-messages",
|
||||
properties: {
|
||||
user: {},
|
||||
friend: {
|
||||
observer: "_friendChanged"
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
|
||||
</html>
|
22
lib/vscode/extensions/html-language-features/server/src/test/fixtures/expected/19813.html
vendored
Normal file
22
lib/vscode/extensions/html-language-features/server/src/test/fixtures/expected/19813.html
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
Polymer({
|
||||
is: "chat-messages",
|
||||
properties: {
|
||||
user: {},
|
||||
friend: {
|
||||
observer: "_friendChanged"
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
</body>
|
||||
|
||||
</html>
|
6
lib/vscode/extensions/html-language-features/server/src/test/fixtures/expected/21634.html
vendored
Normal file
6
lib/vscode/extensions/html-language-features/server/src/test/fixtures/expected/21634.html
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
<app-route path="/module" element="page-module" bindRouter onUrlChange="updateModel"></app-route>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
});
|
||||
</script>
|
19
lib/vscode/extensions/html-language-features/server/src/test/fixtures/inputs/19813.html
vendored
Normal file
19
lib/vscode/extensions/html-language-features/server/src/test/fixtures/inputs/19813.html
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
Polymer({
|
||||
is: "chat-messages",
|
||||
properties: {
|
||||
user: {},
|
||||
friend: {
|
||||
observer: "_friendChanged"
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
6
lib/vscode/extensions/html-language-features/server/src/test/fixtures/inputs/21634.html
vendored
Normal file
6
lib/vscode/extensions/html-language-features/server/src/test/fixtures/inputs/21634.html
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
<app-route path="/module" element="page-module" bindRouter onUrlChange="updateModel"></app-route>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
});
|
||||
</script>
|
@ -0,0 +1,215 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { getFoldingRanges } from '../modes/htmlFolding';
|
||||
import { TextDocument, getLanguageModes } from '../modes/languageModes';
|
||||
import { ClientCapabilities } from 'vscode-css-languageservice';
|
||||
import { getNodeFSRequestService } from '../node/nodeFs';
|
||||
|
||||
interface ExpectedIndentRange {
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
async function assertRanges(lines: string[], expected: ExpectedIndentRange[], message?: string, nRanges?: number): Promise<void> {
|
||||
const document = TextDocument.create('test://foo/bar.html', 'html', 1, lines.join('\n'));
|
||||
const workspace = {
|
||||
settings: {},
|
||||
folders: [{ name: 'foo', uri: 'test://foo' }]
|
||||
};
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
|
||||
const actual = await getFoldingRanges(languageModes, document, nRanges, null);
|
||||
|
||||
let actualRanges = [];
|
||||
for (let i = 0; i < actual.length; i++) {
|
||||
actualRanges[i] = r(actual[i].startLine, actual[i].endLine, actual[i].kind);
|
||||
}
|
||||
actualRanges = actualRanges.sort((r1, r2) => r1.startLine - r2.startLine);
|
||||
assert.deepEqual(actualRanges, expected, message);
|
||||
}
|
||||
|
||||
function r(startLine: number, endLine: number, kind?: string): ExpectedIndentRange {
|
||||
return { startLine, endLine, kind };
|
||||
}
|
||||
|
||||
suite('HTML Folding', async () => {
|
||||
|
||||
test('Embedded JavaScript', async () => {
|
||||
const input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'<script>',
|
||||
/*3*/'function f() {',
|
||||
/*4*/'}',
|
||||
/*5*/'</script>',
|
||||
/*6*/'</head>',
|
||||
/*7*/'</html>',
|
||||
];
|
||||
await await assertRanges(input, [r(0, 6), r(1, 5), r(2, 4), r(3, 4)]);
|
||||
});
|
||||
|
||||
test('Embedded JavaScript - multiple areas', async () => {
|
||||
const input = [
|
||||
/* 0*/'<html>',
|
||||
/* 1*/'<head>',
|
||||
/* 2*/'<script>',
|
||||
/* 3*/' var x = {',
|
||||
/* 4*/' foo: true,',
|
||||
/* 5*/' bar: {}',
|
||||
/* 6*/' };',
|
||||
/* 7*/'</script>',
|
||||
/* 8*/'<script>',
|
||||
/* 9*/' test(() => { // hello',
|
||||
/*10*/' f();',
|
||||
/*11*/' });',
|
||||
/*12*/'</script>',
|
||||
/*13*/'</head>',
|
||||
/*14*/'</html>',
|
||||
];
|
||||
await assertRanges(input, [r(0, 13), r(1, 12), r(2, 6), r(3, 6), r(8, 11), r(9, 11), r(9, 11)]);
|
||||
});
|
||||
|
||||
test('Embedded JavaScript - incomplete', async () => {
|
||||
const input = [
|
||||
/* 0*/'<html>',
|
||||
/* 1*/'<head>',
|
||||
/* 2*/'<script>',
|
||||
/* 3*/' var x = {',
|
||||
/* 4*/'</script>',
|
||||
/* 5*/'<script>',
|
||||
/* 6*/' });',
|
||||
/* 7*/'</script>',
|
||||
/* 8*/'</head>',
|
||||
/* 9*/'</html>',
|
||||
];
|
||||
await assertRanges(input, [r(0, 8), r(1, 7), r(2, 3), r(5, 6)]);
|
||||
});
|
||||
|
||||
test('Embedded JavaScript - regions', async () => {
|
||||
const input = [
|
||||
/* 0*/'<html>',
|
||||
/* 1*/'<head>',
|
||||
/* 2*/'<script>',
|
||||
/* 3*/' // #region Lalala',
|
||||
/* 4*/' // #region',
|
||||
/* 5*/' x = 9;',
|
||||
/* 6*/' // #endregion',
|
||||
/* 7*/' // #endregion Lalala',
|
||||
/* 8*/'</script>',
|
||||
/* 9*/'</head>',
|
||||
/*10*/'</html>',
|
||||
];
|
||||
await assertRanges(input, [r(0, 9), r(1, 8), r(2, 7), r(3, 7, 'region'), r(4, 6, 'region')]);
|
||||
});
|
||||
|
||||
test('Embedded CSS', async () => {
|
||||
const input = [
|
||||
/* 0*/'<html>',
|
||||
/* 1*/'<head>',
|
||||
/* 2*/'<style>',
|
||||
/* 3*/' foo {',
|
||||
/* 4*/' display: block;',
|
||||
/* 5*/' color: black;',
|
||||
/* 6*/' }',
|
||||
/* 7*/'</style>',
|
||||
/* 8*/'</head>',
|
||||
/* 9*/'</html>',
|
||||
];
|
||||
await assertRanges(input, [r(0, 8), r(1, 7), r(2, 6), r(3, 5)]);
|
||||
});
|
||||
|
||||
test('Embedded CSS - multiple areas', async () => {
|
||||
const input = [
|
||||
/* 0*/'<html>',
|
||||
/* 1*/'<head style="color:red">',
|
||||
/* 2*/'<style>',
|
||||
/* 3*/' /*',
|
||||
/* 4*/' foo: true,',
|
||||
/* 5*/' bar: {}',
|
||||
/* 6*/' */',
|
||||
/* 7*/'</style>',
|
||||
/* 8*/'<style>',
|
||||
/* 9*/' @keyframes mymove {',
|
||||
/*10*/' from {top: 0px;}',
|
||||
/*11*/' }',
|
||||
/*12*/'</style>',
|
||||
/*13*/'</head>',
|
||||
/*14*/'</html>',
|
||||
];
|
||||
await assertRanges(input, [r(0, 13), r(1, 12), r(2, 6), r(3, 6, 'comment'), r(8, 11), r(9, 10)]);
|
||||
});
|
||||
|
||||
test('Embedded CSS - regions', async () => {
|
||||
const input = [
|
||||
/* 0*/'<html>',
|
||||
/* 1*/'<head>',
|
||||
/* 2*/'<style>',
|
||||
/* 3*/' /* #region Lalala */',
|
||||
/* 4*/' /* #region*/',
|
||||
/* 5*/' x = 9;',
|
||||
/* 6*/' /* #endregion*/',
|
||||
/* 7*/' /* #endregion Lalala*/',
|
||||
/* 8*/'</style>',
|
||||
/* 9*/'</head>',
|
||||
/*10*/'</html>',
|
||||
];
|
||||
await assertRanges(input, [r(0, 9), r(1, 8), r(2, 7), r(3, 7, 'region'), r(4, 6, 'region')]);
|
||||
});
|
||||
|
||||
|
||||
// test('Embedded JavaScript - multi line comment', async () => {
|
||||
// const input = [
|
||||
// /* 0*/'<html>',
|
||||
// /* 1*/'<head>',
|
||||
// /* 2*/'<script>',
|
||||
// /* 3*/' /*',
|
||||
// /* 4*/' * Hello',
|
||||
// /* 5*/' */',
|
||||
// /* 6*/'</script>',
|
||||
// /* 7*/'</head>',
|
||||
// /* 8*/'</html>',
|
||||
// ];
|
||||
// await assertRanges(input, [r(0, 7), r(1, 6), r(2, 5), r(3, 5, 'comment')]);
|
||||
// });
|
||||
|
||||
test('Test limit', async () => {
|
||||
const input = [
|
||||
/* 0*/'<div>',
|
||||
/* 1*/' <span>',
|
||||
/* 2*/' <b>',
|
||||
/* 3*/' ',
|
||||
/* 4*/' </b>,',
|
||||
/* 5*/' <b>',
|
||||
/* 6*/' <pre>',
|
||||
/* 7*/' ',
|
||||
/* 8*/' </pre>,',
|
||||
/* 9*/' <pre>',
|
||||
/*10*/' ',
|
||||
/*11*/' </pre>,',
|
||||
/*12*/' </b>,',
|
||||
/*13*/' <b>',
|
||||
/*14*/' ',
|
||||
/*15*/' </b>,',
|
||||
/*16*/' <b>',
|
||||
/*17*/' ',
|
||||
/*18*/' </b>',
|
||||
/*19*/' </span>',
|
||||
/*20*/'</div>',
|
||||
];
|
||||
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(6, 7), r(9, 10), r(13, 14), r(16, 17)], 'no limit', undefined);
|
||||
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(6, 7), r(9, 10), r(13, 14), r(16, 17)], 'limit 8', 8);
|
||||
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(6, 7), r(13, 14), r(16, 17)], 'limit 7', 7);
|
||||
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(13, 14), r(16, 17)], 'limit 6', 6);
|
||||
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(13, 14)], 'limit 5', 5);
|
||||
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11)], 'limit 4', 4);
|
||||
await assertRanges(input, [r(0, 19), r(1, 18), r(2, 3)], 'limit 3', 3);
|
||||
await assertRanges(input, [r(0, 19), r(1, 18)], 'limit 2', 2);
|
||||
await assertRanges(input, [r(0, 19)], 'limit 1', 1);
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,210 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import 'mocha';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { getLanguageModes, TextDocument, Range, FormattingOptions, ClientCapabilities } from '../modes/languageModes';
|
||||
|
||||
import { format } from '../modes/formatting';
|
||||
import { getNodeFSRequestService } from '../node/nodeFs';
|
||||
|
||||
suite('HTML Embedded Formatting', () => {
|
||||
|
||||
async function assertFormat(value: string, expected: string, options?: any, formatOptions?: FormattingOptions, message?: string): Promise<void> {
|
||||
let workspace = {
|
||||
settings: options,
|
||||
folders: [{ name: 'foo', uri: 'test://foo' }]
|
||||
};
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
|
||||
|
||||
let rangeStartOffset = value.indexOf('|');
|
||||
let rangeEndOffset;
|
||||
if (rangeStartOffset !== -1) {
|
||||
value = value.substr(0, rangeStartOffset) + value.substr(rangeStartOffset + 1);
|
||||
|
||||
rangeEndOffset = value.indexOf('|');
|
||||
value = value.substr(0, rangeEndOffset) + value.substr(rangeEndOffset + 1);
|
||||
} else {
|
||||
rangeStartOffset = 0;
|
||||
rangeEndOffset = value.length;
|
||||
}
|
||||
let document = TextDocument.create('test://test/test.html', 'html', 0, value);
|
||||
let range = Range.create(document.positionAt(rangeStartOffset), document.positionAt(rangeEndOffset));
|
||||
if (!formatOptions) {
|
||||
formatOptions = FormattingOptions.create(2, true);
|
||||
}
|
||||
|
||||
let result = await format(languageModes, document, range, formatOptions, undefined, { css: true, javascript: true });
|
||||
|
||||
let actual = TextDocument.applyEdits(document, result);
|
||||
assert.equal(actual, expected, message);
|
||||
}
|
||||
|
||||
async function assertFormatWithFixture(fixtureName: string, expectedPath: string, options?: any, formatOptions?: FormattingOptions): Promise<void> {
|
||||
let input = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'test', 'fixtures', 'inputs', fixtureName)).toString().replace(/\r\n/mg, '\n');
|
||||
let expected = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'test', 'fixtures', 'expected', expectedPath)).toString().replace(/\r\n/mg, '\n');
|
||||
await assertFormat(input, expected, options, formatOptions, expectedPath);
|
||||
}
|
||||
|
||||
test('HTML only', async () => {
|
||||
await assertFormat('<html><body><p>Hello</p></body></html>', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>');
|
||||
await assertFormat('|<html><body><p>Hello</p></body></html>|', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>');
|
||||
await assertFormat('<html>|<body><p>Hello</p></body>|</html>', '<html><body>\n <p>Hello</p>\n</body></html>');
|
||||
});
|
||||
|
||||
test('HTML & Scripts', async () => {
|
||||
await assertFormat('<html><head><script></script></head></html>', '<html>\n\n<head>\n <script></script>\n</head>\n\n</html>');
|
||||
await assertFormat('<html><head><script>var x=1;</script></head></html>', '<html>\n\n<head>\n <script>var x = 1;</script>\n</head>\n\n</html>');
|
||||
await assertFormat('<html><head><script>\nvar x=2;\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 2;\n </script>\n</head>\n\n</html>');
|
||||
await assertFormat('<html><head>\n <script>\nvar x=3;\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 3;\n </script>\n</head>\n\n</html>');
|
||||
await assertFormat('<html><head>\n <script>\nvar x=4;\nconsole.log("Hi");\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 4;\n console.log("Hi");\n </script>\n</head>\n\n</html>');
|
||||
await assertFormat('<html><head>\n |<script>\nvar x=5;\n</script>|</head></html>', '<html><head>\n <script>\n var x = 5;\n </script></head></html>');
|
||||
});
|
||||
|
||||
test('HTLM & Scripts - Fixtures', async () => {
|
||||
assertFormatWithFixture('19813.html', '19813.html');
|
||||
assertFormatWithFixture('19813.html', '19813-4spaces.html', undefined, FormattingOptions.create(4, true));
|
||||
assertFormatWithFixture('19813.html', '19813-tab.html', undefined, FormattingOptions.create(1, false));
|
||||
assertFormatWithFixture('21634.html', '21634.html');
|
||||
});
|
||||
|
||||
test('Script end tag', async () => {
|
||||
await assertFormat('<html>\n<head>\n <script>\nvar x = 0;\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 0;\n </script>\n</head>\n\n</html>');
|
||||
});
|
||||
|
||||
test('HTML & Multiple Scripts', async () => {
|
||||
await assertFormat('<html><head>\n<script>\nif(x){\nbar(); }\n</script><script>\nfunction(x){ }\n</script></head></html>', '<html>\n\n<head>\n <script>\n if (x) {\n bar();\n }\n </script>\n <script>\n function(x) {}\n </script>\n</head>\n\n</html>');
|
||||
});
|
||||
|
||||
test('HTML & Styles', async () => {
|
||||
await assertFormat('<html><head>\n<style>\n.foo{display:none;}\n</style></head></html>', '<html>\n\n<head>\n <style>\n .foo {\n display: none;\n }\n </style>\n</head>\n\n</html>');
|
||||
});
|
||||
|
||||
test('EndWithNewline', async () => {
|
||||
let options = {
|
||||
html: {
|
||||
format: {
|
||||
endWithNewline: true
|
||||
}
|
||||
}
|
||||
};
|
||||
await assertFormat('<html><body><p>Hello</p></body></html>', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>\n', options);
|
||||
await assertFormat('<html>|<body><p>Hello</p></body>|</html>', '<html><body>\n <p>Hello</p>\n</body></html>', options);
|
||||
await assertFormat('<html><head><script>\nvar x=1;\n</script></head></html>', '<html>\n\n<head>\n <script>\n var x = 1;\n </script>\n</head>\n\n</html>\n', options);
|
||||
});
|
||||
|
||||
test('Inside script', async () => {
|
||||
await assertFormat('<html><head>\n <script>\n|var x=6;|\n</script></head></html>', '<html><head>\n <script>\n var x = 6;\n</script></head></html>');
|
||||
await assertFormat('<html><head>\n <script>\n|var x=6;\nvar y= 9;|\n</script></head></html>', '<html><head>\n <script>\n var x = 6;\n var y = 9;\n</script></head></html>');
|
||||
});
|
||||
|
||||
test('Range after new line', async () => {
|
||||
await assertFormat('<html><head>\n |<script>\nvar x=6;\n</script>\n|</head></html>', '<html><head>\n <script>\n var x = 6;\n </script>\n</head></html>');
|
||||
});
|
||||
|
||||
test('bug 36574', async () => {
|
||||
await assertFormat('<script src="/js/main.js"> </script>', '<script src="/js/main.js"> </script>');
|
||||
});
|
||||
|
||||
test('bug 48049', async () => {
|
||||
await assertFormat(
|
||||
[
|
||||
'<html>',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'',
|
||||
'<body>',
|
||||
'',
|
||||
' <script>',
|
||||
' function f(x) {}',
|
||||
' f(function () {',
|
||||
' // ',
|
||||
'',
|
||||
' console.log(" vsc crashes on formatting")',
|
||||
' });',
|
||||
' </script>',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
' </body>',
|
||||
'',
|
||||
'</html>'
|
||||
].join('\n'),
|
||||
[
|
||||
'<html>',
|
||||
'',
|
||||
'<head>',
|
||||
'</head>',
|
||||
'',
|
||||
'<body>',
|
||||
'',
|
||||
' <script>',
|
||||
' function f(x) {}',
|
||||
' f(function () {',
|
||||
' // ',
|
||||
'',
|
||||
' console.log(" vsc crashes on formatting")',
|
||||
' });',
|
||||
' </script>',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'</body>',
|
||||
'',
|
||||
'</html>'
|
||||
].join('\n')
|
||||
);
|
||||
});
|
||||
test('#58435', async () => {
|
||||
let options = {
|
||||
html: {
|
||||
format: {
|
||||
contentUnformatted: 'textarea'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const content = [
|
||||
'<html>',
|
||||
'',
|
||||
'<body>',
|
||||
' <textarea name= "" id ="" cols="30" rows="10">',
|
||||
' </textarea>',
|
||||
'</body>',
|
||||
'',
|
||||
'</html>',
|
||||
].join('\n');
|
||||
|
||||
const expected = [
|
||||
'<html>',
|
||||
'',
|
||||
'<body>',
|
||||
' <textarea name="" id="" cols="30" rows="10">',
|
||||
' </textarea>',
|
||||
'</body>',
|
||||
'',
|
||||
'</html>',
|
||||
].join('\n');
|
||||
|
||||
await assertFormat(content, expected, options);
|
||||
});
|
||||
|
||||
}); /*
|
||||
content_unformatted: Array(4)["pre", "code", "textarea", …]
|
||||
end_with_newline: false
|
||||
eol: "\n"
|
||||
extra_liners: Array(3)["head", "body", "/html"]
|
||||
indent_char: "\t"
|
||||
indent_handlebars: false
|
||||
indent_inner_html: false
|
||||
indent_size: 1
|
||||
max_preserve_newlines: 32786
|
||||
preserve_newlines: true
|
||||
unformatted: Array(1)["wbr"]
|
||||
wrap_attributes: "auto"
|
||||
wrap_attributes_indent_size: undefined
|
||||
wrap_line_length: 120*/
|
@ -0,0 +1,4 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
@ -0,0 +1,4 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
@ -0,0 +1,4 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
@ -0,0 +1,4 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
@ -0,0 +1,4 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
@ -0,0 +1,81 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { getLanguageModes, ClientCapabilities, TextDocument, SelectionRange} from '../modes/languageModes';
|
||||
import { getSelectionRanges } from '../modes/selectionRanges';
|
||||
import { getNodeFSRequestService } from '../node/nodeFs';
|
||||
|
||||
async function assertRanges(content: string, expected: (number | string)[][]): Promise<void> {
|
||||
let message = `${content} gives selection range:\n`;
|
||||
|
||||
const offset = content.indexOf('|');
|
||||
content = content.substr(0, offset) + content.substr(offset + 1);
|
||||
|
||||
let workspace = {
|
||||
settings: {},
|
||||
folders: [{ name: 'foo', uri: 'test://foo' }]
|
||||
};
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
|
||||
|
||||
const document = TextDocument.create('test://foo.html', 'html', 1, content);
|
||||
const actualRanges = await getSelectionRanges(languageModes, document, [document.positionAt(offset)]);
|
||||
assert.equal(actualRanges.length, 1);
|
||||
const offsetPairs: [number, string][] = [];
|
||||
let curr: SelectionRange | undefined = actualRanges[0];
|
||||
while (curr) {
|
||||
offsetPairs.push([document.offsetAt(curr.range.start), document.getText(curr.range)]);
|
||||
curr = curr.parent;
|
||||
}
|
||||
|
||||
message += `${JSON.stringify(offsetPairs)}\n but should give:\n${JSON.stringify(expected)}\n`;
|
||||
assert.deepEqual(offsetPairs, expected, message);
|
||||
}
|
||||
|
||||
suite('HTML SelectionRange', () => {
|
||||
test('Embedded JavaScript', async () => {
|
||||
await assertRanges('<html><head><script> function foo() { return ((1|+2)*6) }</script></head></html>', [
|
||||
[48, '1'],
|
||||
[48, '1+2'],
|
||||
[47, '(1+2)'],
|
||||
[47, '(1+2)*6'],
|
||||
[46, '((1+2)*6)'],
|
||||
[39, 'return ((1+2)*6)'],
|
||||
[22, 'function foo() { return ((1+2)*6) }'],
|
||||
[20, ' function foo() { return ((1+2)*6) }'],
|
||||
[12, '<script> function foo() { return ((1+2)*6) }</script>'],
|
||||
[6, '<head><script> function foo() { return ((1+2)*6) }</script></head>'],
|
||||
[0, '<html><head><script> function foo() { return ((1+2)*6) }</script></head></html>'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('Embedded CSS', async () => {
|
||||
await assertRanges('<html><head><style>foo { display: |none; } </style></head></html>', [
|
||||
[34, 'none'],
|
||||
[25, 'display: none'],
|
||||
[24, ' display: none; '],
|
||||
[23, '{ display: none; }'],
|
||||
[19, 'foo { display: none; }'],
|
||||
[19, 'foo { display: none; } '],
|
||||
[12, '<style>foo { display: none; } </style>'],
|
||||
[6, '<head><style>foo { display: none; } </style></head>'],
|
||||
[0, '<html><head><style>foo { display: none; } </style></head></html>'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('Embedded style', async () => {
|
||||
await assertRanges('<div style="color: |red"></div>', [
|
||||
[19, 'red'],
|
||||
[12, 'color: red'],
|
||||
[11, '"color: red"'],
|
||||
[5, 'style="color: red"'],
|
||||
[1, 'div style="color: red"'],
|
||||
[0, '<div style="color: red"></div>']
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
});
|
@ -0,0 +1,228 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { TextDocument, getLanguageModes, ClientCapabilities, Range, Position } from '../modes/languageModes';
|
||||
import { newSemanticTokenProvider } from '../modes/semanticTokens';
|
||||
import { getNodeFSRequestService } from '../node/nodeFs';
|
||||
|
||||
interface ExpectedToken {
|
||||
startLine: number;
|
||||
character: number;
|
||||
length: number;
|
||||
tokenClassifiction: string;
|
||||
}
|
||||
|
||||
async function assertTokens(lines: string[], expected: ExpectedToken[], ranges?: Range[], message?: string): Promise<void> {
|
||||
const document = TextDocument.create('test://foo/bar.html', 'html', 1, lines.join('\n'));
|
||||
const workspace = {
|
||||
settings: {},
|
||||
folders: [{ name: 'foo', uri: 'test://foo' }]
|
||||
};
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
|
||||
const semanticTokensProvider = newSemanticTokenProvider(languageModes);
|
||||
|
||||
const legend = semanticTokensProvider.legend;
|
||||
const actual = await semanticTokensProvider.getSemanticTokens(document, ranges);
|
||||
|
||||
let actualRanges = [];
|
||||
let lastLine = 0;
|
||||
let lastCharacter = 0;
|
||||
for (let i = 0; i < actual.length; i += 5) {
|
||||
const lineDelta = actual[i], charDelta = actual[i + 1], len = actual[i + 2], typeIdx = actual[i + 3], modSet = actual[i + 4];
|
||||
const line = lastLine + lineDelta;
|
||||
const character = lineDelta === 0 ? lastCharacter + charDelta : charDelta;
|
||||
const tokenClassifiction = [legend.types[typeIdx], ...legend.modifiers.filter((_, i) => modSet & 1 << i)].join('.');
|
||||
actualRanges.push(t(line, character, len, tokenClassifiction));
|
||||
lastLine = line;
|
||||
lastCharacter = character;
|
||||
}
|
||||
assert.deepEqual(actualRanges, expected, message);
|
||||
}
|
||||
|
||||
function t(startLine: number, character: number, length: number, tokenClassifiction: string): ExpectedToken {
|
||||
return { startLine, character, length, tokenClassifiction };
|
||||
}
|
||||
|
||||
suite('HTML Semantic Tokens', () => {
|
||||
|
||||
test('Variables', async () => {
|
||||
const input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'<script>',
|
||||
/*3*/' var x = 9, y1 = [x];',
|
||||
/*4*/' try {',
|
||||
/*5*/' for (const s of y1) { x = s }',
|
||||
/*6*/' } catch (e) {',
|
||||
/*7*/' throw y1;',
|
||||
/*8*/' }',
|
||||
/*9*/'</script>',
|
||||
/*10*/'</head>',
|
||||
/*11*/'</html>',
|
||||
];
|
||||
await assertTokens(input, [
|
||||
t(3, 6, 1, 'variable.declaration'), t(3, 13, 2, 'variable.declaration'), t(3, 19, 1, 'variable'),
|
||||
t(5, 15, 1, 'variable.declaration.readonly'), t(5, 20, 2, 'variable'), t(5, 26, 1, 'variable'), t(5, 30, 1, 'variable.readonly'),
|
||||
t(6, 11, 1, 'variable.declaration'),
|
||||
t(7, 10, 2, 'variable')
|
||||
]);
|
||||
});
|
||||
|
||||
test('Functions', async () => {
|
||||
const input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'<script>',
|
||||
/*3*/' function foo(p1) {',
|
||||
/*4*/' return foo(Math.abs(p1))',
|
||||
/*5*/' }',
|
||||
/*6*/' `/${window.location}`.split("/").forEach(s => foo(s));',
|
||||
/*7*/'</script>',
|
||||
/*8*/'</head>',
|
||||
/*9*/'</html>',
|
||||
];
|
||||
await assertTokens(input, [
|
||||
t(3, 11, 3, 'function.declaration'), t(3, 15, 2, 'parameter.declaration'),
|
||||
t(4, 11, 3, 'function'), t(4, 15, 4, 'interface'), t(4, 20, 3, 'member'), t(4, 24, 2, 'parameter'),
|
||||
t(6, 6, 6, 'variable'), t(6, 13, 8, 'property'), t(6, 24, 5, 'member'), t(6, 35, 7, 'member'), t(6, 43, 1, 'parameter.declaration'), t(6, 48, 3, 'function'), t(6, 52, 1, 'parameter')
|
||||
]);
|
||||
});
|
||||
|
||||
test('Members', async () => {
|
||||
const input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'<script>',
|
||||
/*3*/' class A {',
|
||||
/*4*/' static x = 9;',
|
||||
/*5*/' f = 9;',
|
||||
/*6*/' async m() { return A.x + await this.m(); };',
|
||||
/*7*/' get s() { return this.f; ',
|
||||
/*8*/' static t() { return new A().f; };',
|
||||
/*9*/' constructor() {}',
|
||||
/*10*/' }',
|
||||
/*11*/'</script>',
|
||||
/*12*/'</head>',
|
||||
/*13*/'</html>',
|
||||
];
|
||||
|
||||
|
||||
await assertTokens(input, [
|
||||
t(3, 8, 1, 'class.declaration'),
|
||||
t(4, 11, 1, 'property.declaration.static'),
|
||||
t(5, 4, 1, 'property.declaration'),
|
||||
t(6, 10, 1, 'member.declaration.async'), t(6, 23, 1, 'class'), t(6, 25, 1, 'property.static'), t(6, 40, 1, 'member.async'),
|
||||
t(7, 8, 1, 'property.declaration'), t(7, 26, 1, 'property'),
|
||||
t(8, 11, 1, 'member.declaration.static'), t(8, 28, 1, 'class'), t(8, 32, 1, 'property'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('Interfaces', async () => {
|
||||
const input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'<script type="text/typescript">',
|
||||
/*3*/' interface Position { x: number, y: number };',
|
||||
/*4*/' const p = { x: 1, y: 2 } as Position;',
|
||||
/*5*/' const foo = (o: Position) => o.x + o.y;',
|
||||
/*6*/'</script>',
|
||||
/*7*/'</head>',
|
||||
/*8*/'</html>',
|
||||
];
|
||||
await assertTokens(input, [
|
||||
t(3, 12, 8, 'interface.declaration'), t(3, 23, 1, 'property.declaration'), t(3, 34, 1, 'property.declaration'),
|
||||
t(4, 8, 1, 'variable.declaration.readonly'), t(4, 30, 8, 'interface'),
|
||||
t(5, 8, 3, 'variable.declaration.readonly'), t(5, 15, 1, 'parameter.declaration'), t(5, 18, 8, 'interface'), t(5, 31, 1, 'parameter'), t(5, 33, 1, 'property'), t(5, 37, 1, 'parameter'), t(5, 39, 1, 'property')
|
||||
]);
|
||||
});
|
||||
|
||||
test('Readonly', async () => {
|
||||
const input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'<script type="text/typescript">',
|
||||
/*3*/' const f = 9;',
|
||||
/*4*/' class A { static readonly t = 9; static url: URL; }',
|
||||
/*5*/' const enum E { A = 9, B = A + 1 }',
|
||||
/*6*/' console.log(f + A.t + A.url.origin);',
|
||||
/*7*/'</script>',
|
||||
/*8*/'</head>',
|
||||
/*9*/'</html>',
|
||||
];
|
||||
await assertTokens(input, [
|
||||
t(3, 8, 1, 'variable.declaration.readonly'),
|
||||
t(4, 8, 1, 'class.declaration'), t(4, 28, 1, 'property.declaration.static.readonly'), t(4, 42, 3, 'property.declaration.static'), t(4, 47, 3, 'interface'),
|
||||
t(5, 13, 1, 'enum.declaration'), t(5, 17, 1, 'property.declaration.readonly'), t(5, 24, 1, 'property.declaration.readonly'), t(5, 28, 1, 'property.readonly'),
|
||||
t(6, 2, 7, 'variable'), t(6, 10, 3, 'member'), t(6, 14, 1, 'variable.readonly'), t(6, 18, 1, 'class'), t(6, 20, 1, 'property.static.readonly'), t(6, 24, 1, 'class'), t(6, 26, 3, 'property.static'), t(6, 30, 6, 'property.readonly'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
test('Type aliases and type parameters', async () => {
|
||||
const input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'<script type="text/typescript">',
|
||||
/*3*/' type MyMap = Map<string, number>;',
|
||||
/*4*/' function f<T extends MyMap>(t: T | number) : T { ',
|
||||
/*5*/' return <T> <unknown> new Map<string, MyMap>();',
|
||||
/*6*/' }',
|
||||
/*7*/'</script>',
|
||||
/*8*/'</head>',
|
||||
/*9*/'</html>',
|
||||
];
|
||||
await assertTokens(input, [
|
||||
t(3, 7, 5, 'type.declaration'), t(3, 15, 3, 'interface') /* to investiagte */,
|
||||
t(4, 11, 1, 'function.declaration'), t(4, 13, 1, 'typeParameter.declaration'), t(4, 23, 5, 'type'), t(4, 30, 1, 'parameter.declaration'), t(4, 33, 1, 'typeParameter'), t(4, 47, 1, 'typeParameter'),
|
||||
t(5, 12, 1, 'typeParameter'), t(5, 29, 3, 'interface'), t(5, 41, 5, 'type'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('TS and JS', async () => {
|
||||
const input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'<script type="text/typescript">',
|
||||
/*3*/' function f<T>(p1: T): T[] { return [ p1 ]; }',
|
||||
/*4*/'</script>',
|
||||
/*5*/'<script>',
|
||||
/*6*/' window.alert("Hello");',
|
||||
/*7*/'</script>',
|
||||
/*8*/'</head>',
|
||||
/*9*/'</html>',
|
||||
];
|
||||
await assertTokens(input, [
|
||||
t(3, 11, 1, 'function.declaration'), t(3, 13, 1, 'typeParameter.declaration'), t(3, 16, 2, 'parameter.declaration'), t(3, 20, 1, 'typeParameter'), t(3, 24, 1, 'typeParameter'), t(3, 39, 2, 'parameter'),
|
||||
t(6, 2, 6, 'variable'), t(6, 9, 5, 'member')
|
||||
]);
|
||||
});
|
||||
|
||||
test('Ranges', async () => {
|
||||
const input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'<script>',
|
||||
/*3*/' window.alert("Hello");',
|
||||
/*4*/'</script>',
|
||||
/*5*/'<script>',
|
||||
/*6*/' window.alert("World");',
|
||||
/*7*/'</script>',
|
||||
/*8*/'</head>',
|
||||
/*9*/'</html>',
|
||||
];
|
||||
await assertTokens(input, [
|
||||
t(3, 2, 6, 'variable'), t(3, 9, 5, 'member')
|
||||
], [Range.create(Position.create(2, 0), Position.create(4, 0))]);
|
||||
|
||||
await assertTokens(input, [
|
||||
t(6, 2, 6, 'variable'),
|
||||
], [Range.create(Position.create(6, 2), Position.create(6, 8))]);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
@ -0,0 +1,44 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import * as words from '../utils/strings';
|
||||
|
||||
suite('HTML Words', () => {
|
||||
|
||||
let wordRegex = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
|
||||
|
||||
function assertWord(value: string, expected: string): void {
|
||||
let offset = value.indexOf('|');
|
||||
value = value.substr(0, offset) + value.substr(offset + 1);
|
||||
|
||||
let actualRange = words.getWordAtText(value, offset, wordRegex);
|
||||
assert(actualRange.start <= offset);
|
||||
assert(actualRange.start + actualRange.length >= offset);
|
||||
assert.equal(value.substr(actualRange.start, actualRange.length), expected);
|
||||
}
|
||||
|
||||
|
||||
test('Basic', function (): any {
|
||||
assertWord('|var x1 = new F<A>(a, b);', 'var');
|
||||
assertWord('v|ar x1 = new F<A>(a, b);', 'var');
|
||||
assertWord('var| x1 = new F<A>(a, b);', 'var');
|
||||
assertWord('var |x1 = new F<A>(a, b);', 'x1');
|
||||
assertWord('var x1| = new F<A>(a, b);', 'x1');
|
||||
assertWord('var x1 = new |F<A>(a, b);', 'F');
|
||||
assertWord('var x1 = new F<|A>(a, b);', 'A');
|
||||
assertWord('var x1 = new F<A>(|a, b);', 'a');
|
||||
assertWord('var x1 = new F<A>(a, b|);', 'b');
|
||||
assertWord('var x1 = new F<A>(a, b)|;', '');
|
||||
assertWord('var x1 = new F<A>(a, b)|;|', '');
|
||||
assertWord('var x1 = | new F<A>(a, b)|;|', '');
|
||||
});
|
||||
|
||||
test('Multiline', function (): any {
|
||||
assertWord('console.log("hello");\n|var x1 = new F<A>(a, b);', 'var');
|
||||
assertWord('console.log("hello");\n|\nvar x1 = new F<A>(a, b);', '');
|
||||
assertWord('console.log("hello");\n\r |var x1 = new F<A>(a, b);', 'var');
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function pushAll<T>(to: T[], from: T[]) {
|
||||
if (from) {
|
||||
for (const e of from) {
|
||||
to.push(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function contains<T>(arr: T[], val: T) {
|
||||
return arr.indexOf(val) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort`
|
||||
* so only use this when actually needing stable sort.
|
||||
*/
|
||||
export function mergeSort<T>(data: T[], compare: (a: T, b: T) => number): T[] {
|
||||
_divideAndMerge(data, compare);
|
||||
return data;
|
||||
}
|
||||
|
||||
function _divideAndMerge<T>(data: T[], compare: (a: T, b: T) => number): void {
|
||||
if (data.length <= 1) {
|
||||
// sorted
|
||||
return;
|
||||
}
|
||||
const p = (data.length / 2) | 0;
|
||||
const left = data.slice(0, p);
|
||||
const right = data.slice(p);
|
||||
|
||||
_divideAndMerge(left, compare);
|
||||
_divideAndMerge(right, compare);
|
||||
|
||||
let leftIdx = 0;
|
||||
let rightIdx = 0;
|
||||
let i = 0;
|
||||
while (leftIdx < left.length && rightIdx < right.length) {
|
||||
let ret = compare(left[leftIdx], right[rightIdx]);
|
||||
if (ret <= 0) {
|
||||
// smaller_equal -> take left to preserve order
|
||||
data[i++] = left[leftIdx++];
|
||||
} else {
|
||||
// greater -> take right
|
||||
data[i++] = right[rightIdx++];
|
||||
}
|
||||
}
|
||||
while (leftIdx < left.length) {
|
||||
data[i++] = left[leftIdx++];
|
||||
}
|
||||
while (rightIdx < right.length) {
|
||||
data[i++] = right[rightIdx++];
|
||||
}
|
||||
}
|
||||
|
||||
export function binarySearch<T>(array: T[], key: T, comparator: (op1: T, op2: T) => number): number {
|
||||
let low = 0,
|
||||
high = array.length - 1;
|
||||
|
||||
while (low <= high) {
|
||||
let mid = ((low + high) / 2) | 0;
|
||||
let comp = comparator(array[mid], key);
|
||||
if (comp < 0) {
|
||||
low = mid + 1;
|
||||
} else if (comp > 0) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
return mid;
|
||||
}
|
||||
}
|
||||
return -(low + 1);
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DocumentContext } from 'vscode-css-languageservice';
|
||||
import { endsWith, startsWith } from '../utils/strings';
|
||||
import { WorkspaceFolder } from 'vscode-languageserver';
|
||||
import { resolvePath } from '../requests';
|
||||
|
||||
export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext {
|
||||
function getRootFolder(): string | undefined {
|
||||
for (let folder of workspaceFolders) {
|
||||
let folderURI = folder.uri;
|
||||
if (!endsWith(folderURI, '/')) {
|
||||
folderURI = folderURI + '/';
|
||||
}
|
||||
if (startsWith(documentUri, folderURI)) {
|
||||
return folderURI;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
resolveReference: (ref: string, base = documentUri) => {
|
||||
if (ref[0] === '/') { // resolve absolute path against the current workspace folder
|
||||
let folderUri = getRootFolder();
|
||||
if (folderUri) {
|
||||
return folderUri + ref.substr(1);
|
||||
}
|
||||
}
|
||||
base = base.substr(0, base.lastIndexOf('/') + 1);
|
||||
return resolvePath(base, ref);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Position, Range } from '../modes/languageModes';
|
||||
|
||||
export function beforeOrSame(p1: Position, p2: Position) {
|
||||
return p1.line < p2.line || p1.line === p2.line && p1.character <= p2.character;
|
||||
}
|
||||
export function insideRangeButNotSame(r1: Range, r2: Range) {
|
||||
return beforeOrSame(r1.start, r2.start) && beforeOrSame(r2.end, r1.end) && !equalRange(r1, r2);
|
||||
}
|
||||
export function equalRange(r1: Range, r2: Range) {
|
||||
return r1.start.line === r2.start.line && r1.start.character === r2.start.character && r1.end.line === r2.end.line && r1.end.character === r2.end.character;
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResponseError, ErrorCodes, CancellationToken } from 'vscode-languageserver';
|
||||
|
||||
export function formatError(message: string, err: any): string {
|
||||
if (err instanceof Error) {
|
||||
let error = <Error>err;
|
||||
return `${message}: ${error.message}\n${error.stack}`;
|
||||
} else if (typeof err === 'string') {
|
||||
return `${message}: ${err}`;
|
||||
} else if (err) {
|
||||
return `${message}: ${err.toString()}`;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
export function runSafe<T>(func: () => Thenable<T>, errorVal: T, errorMessage: string, token: CancellationToken): Thenable<T | ResponseError<any>> {
|
||||
return new Promise<T | ResponseError<any>>((resolve) => {
|
||||
setImmediate(() => {
|
||||
if (token.isCancellationRequested) {
|
||||
resolve(cancelValue());
|
||||
}
|
||||
return func().then(result => {
|
||||
if (token.isCancellationRequested) {
|
||||
resolve(cancelValue());
|
||||
return;
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
}, e => {
|
||||
console.error(formatError(errorMessage, e));
|
||||
resolve(errorVal);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function cancelValue<E>() {
|
||||
return new ResponseError<E>(ErrorCodes.RequestCancelled, 'Request cancelled');
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function getWordAtText(text: string, offset: number, wordDefinition: RegExp): { start: number, length: number } {
|
||||
let lineStart = offset;
|
||||
while (lineStart > 0 && !isNewlineCharacter(text.charCodeAt(lineStart - 1))) {
|
||||
lineStart--;
|
||||
}
|
||||
let offsetInLine = offset - lineStart;
|
||||
let lineText = text.substr(lineStart);
|
||||
|
||||
// make a copy of the regex as to not keep the state
|
||||
let flags = wordDefinition.ignoreCase ? 'gi' : 'g';
|
||||
wordDefinition = new RegExp(wordDefinition.source, flags);
|
||||
|
||||
let match = wordDefinition.exec(lineText);
|
||||
while (match && match.index + match[0].length < offsetInLine) {
|
||||
match = wordDefinition.exec(lineText);
|
||||
}
|
||||
if (match && match.index <= offsetInLine) {
|
||||
return { start: match.index + lineStart, length: match[0].length };
|
||||
}
|
||||
|
||||
return { start: offset, length: 0 };
|
||||
}
|
||||
|
||||
export function startsWith(haystack: string, needle: string): boolean {
|
||||
if (haystack.length < needle.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < needle.length; i++) {
|
||||
if (haystack[i] !== needle[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function endsWith(haystack: string, needle: string): boolean {
|
||||
let diff = haystack.length - needle.length;
|
||||
if (diff > 0) {
|
||||
return haystack.indexOf(needle, diff) === diff;
|
||||
} else if (diff === 0) {
|
||||
return haystack === needle;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function repeat(value: string, count: number) {
|
||||
let s = '';
|
||||
while (count > 0) {
|
||||
if ((count & 1) === 1) {
|
||||
s += value;
|
||||
}
|
||||
value += value;
|
||||
count = count >>> 1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export function isWhitespaceOnly(str: string) {
|
||||
return /^\s*$/.test(str);
|
||||
}
|
||||
|
||||
export function isEOL(content: string, offset: number) {
|
||||
return isNewlineCharacter(content.charCodeAt(offset));
|
||||
}
|
||||
|
||||
const CR = '\r'.charCodeAt(0);
|
||||
const NL = '\n'.charCodeAt(0);
|
||||
export function isNewlineCharacter(charCode: number) {
|
||||
return charCode === CR || charCode === NL;
|
||||
}
|
Reference in New Issue
Block a user